diff --git a/.gitignore b/.gitignore index 349500c11..eefe67487 100644 --- a/.gitignore +++ b/.gitignore @@ -30,4 +30,5 @@ yarn-error.log* .env* !.env.example .mp4 -public/config.dev.json \ No newline at end of file +public/config.dev.json +public/config.local.json \ No newline at end of file diff --git a/package.json b/package.json index ba3f6d762..90c9e6c73 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "antlr4": "^4.13.1-patch-1", "axios": "^1.6.0", "buffer": "^6.0.3", + "canvas": "^2.11.2", "client-oauth2": "^4.2.4", "d3": "^7.8.2", "d3-cloud": "^1.2.5", @@ -41,6 +42,7 @@ "react-router": "^6.9.0", "react-router-dom": "^6.9.0", "react-use-websocket": "4.8.1", + "react-window": "^1.8.10", "redux": "^4.2.1", "redux-devtools-extension": "^2.13.9", "redux-logger": "^3.0.6", @@ -60,7 +62,7 @@ "build": "vite build", "serve": "vite preview", "lint": "eslint --ext .jsx,.js,.ts,.tsx src/", - "test": "vitest", + "test": "vitest", "coverage": "vitest run --coverage", "ui": "vitest --ui" }, @@ -98,6 +100,7 @@ "@types/react-redux": "^7.1.25", "@types/react-router": "^5.1.20", "@types/react-router-dom": "^5.3.3", + "@types/react-window": "^1.8.8", "@types/redux-logger": "^3.0.9", "@types/uuid": "^8.3.4", "@typescript-eslint/eslint-plugin": "^5.57.0", diff --git a/src/__tests__/data/cohortCreation/claimCriteria.ts b/src/__tests__/data/cohortCreation/claimCriteria.ts index 10eee8303..e73d45f80 100644 --- a/src/__tests__/data/cohortCreation/claimCriteria.ts +++ b/src/__tests__/data/cohortCreation/claimCriteria.ts @@ -62,12 +62,14 @@ export const completeClaimCriteria: GhmDataType = { full_path: 'APHP-ASSISTANCE PUBLIQUE AP-HP/H01-GH RCP', id: '8312016825', inferior_levels_ids: '8312016826', + label: 'GH RCP', name: 'GH RCP', parent_id: '8312002244', source_value: 'H01', status: undefined, subItems: undefined, - type: 'Groupe hospitalier (GH)' + type: 'Groupe hospitalier (GH)', + system: 'nan' } ] } diff --git a/src/__tests__/data/cohortCreation/cohortCreation.ts b/src/__tests__/data/cohortCreation/cohortCreation.ts index 14298159b..8dbe99d94 100644 --- a/src/__tests__/data/cohortCreation/cohortCreation.ts +++ b/src/__tests__/data/cohortCreation/cohortCreation.ts @@ -16,7 +16,6 @@ const defaultProcedureCriteria: SelectedCriteriaType = { encounterEndDate: [null, null], includeEncounterEndDateNull: true, encounterStatus: [], - hierarchy: undefined, code: [], source: null, label: undefined diff --git a/src/__tests__/data/cohortCreation/procedureCriteria.ts b/src/__tests__/data/cohortCreation/procedureCriteria.ts index 9901739f8..ee624158b 100644 --- a/src/__tests__/data/cohortCreation/procedureCriteria.ts +++ b/src/__tests__/data/cohortCreation/procedureCriteria.ts @@ -14,7 +14,6 @@ export const defaultProcedureCriteria: CcamDataType = { encounterEndDate: [null, null], includeEncounterEndDateNull: false, encounterStatus: [], - hierarchy: undefined, code: [], source: null, label: undefined, diff --git a/src/components/CreationCohort/DataList_Criteria.tsx b/src/components/CreationCohort/DataList_Criteria.tsx index 3fcf1d6d9..3c494a797 100644 --- a/src/components/CreationCohort/DataList_Criteria.tsx +++ b/src/components/CreationCohort/DataList_Criteria.tsx @@ -1,12 +1,12 @@ -import { CriteriaItemType } from 'types' +import { Back_API_Response, CriteriaItemType } from 'types' import RequestForm from './DiagramView/components/LogicalOperator/components/CriteriaRightPanel/RequestForm/RequestForm' import IPPForm from './DiagramView/components/LogicalOperator/components/CriteriaRightPanel/IPPForm/IPPForm' import DocumentsForm from './DiagramView/components/LogicalOperator/components/CriteriaRightPanel/DocumentsForm/DocumentsForm' import EncounterForm from './DiagramView/components/LogicalOperator/components/CriteriaRightPanel/EncounterForm' -import CCAMForm from './DiagramView/components/LogicalOperator/components/CriteriaRightPanel/CCAM' +import CCAMForm from './DiagramView/components/LogicalOperator/components/CriteriaRightPanel/CcamForm' import Cim10Form from './DiagramView/components/LogicalOperator/components/CriteriaRightPanel/Cim10Form' -import GhmForm from './DiagramView/components/LogicalOperator/components/CriteriaRightPanel/GHM' +import GhmForm from './DiagramView/components/LogicalOperator/components/CriteriaRightPanel/GHMForm' import MedicationForm from './DiagramView/components/LogicalOperator/components/CriteriaRightPanel/MedicationForm' import BiologyForm from './DiagramView/components/LogicalOperator/components/CriteriaRightPanel/BiologyForm' import DemographicForm from './DiagramView/components/LogicalOperator/components/CriteriaRightPanel/DemographicForm' @@ -14,10 +14,15 @@ import ImagingForm from './DiagramView/components/LogicalOperator/components/Cri import PregnantForm from './DiagramView/components/LogicalOperator/components/CriteriaRightPanel/PregnantForm' import HospitForm from './DiagramView/components/LogicalOperator/components/CriteriaRightPanel/HospitForm' -import services from 'services/aphp' - import { CriteriaType, CriteriaTypeLabels } from 'types/requestCriterias' import { getConfig } from 'config' +import { getChildrenFromCodes, getCodeList } from 'services/aphp/serviceValueSets' +import { FhirItem, Hierarchy } from 'types/hierarchy' +import docTypes from 'assets/docTypes.json' +import { birthStatusData, booleanFieldsData, booleanOpenChoiceFieldsData, vmeData } from 'data/questionnaire_data' +import { VitalStatusLabel } from 'types/searchCriterias' + +const async = (fetch: () => Promise>) => async () => (await fetch()).results const criteriaList: () => CriteriaItemType[] = () => { const ODD_QUESTIONNAIRE = getConfig().features.questionnaires.enabled @@ -46,7 +51,21 @@ const criteriaList: () => CriteriaItemType[] = () => { color: '#0063AF', fontWeight: 'bold', components: DemographicForm, - fetch: { gender: services.cohortCreation.fetchGender, status: services.cohortCreation.fetchStatus } + fetch: { + gender: async(() => getCodeList(getConfig().core.valueSets.demographicGender.url)), + status: () => [ + { + id: 'false', + label: VitalStatusLabel.ALIVE, + system: 'status' + }, + { + id: 'true', + label: VitalStatusLabel.DECEASED, + system: 'status' + } + ] + } }, { id: CriteriaType.ENCOUNTER, @@ -55,17 +74,17 @@ const criteriaList: () => CriteriaItemType[] = () => { fontWeight: 'bold', components: EncounterForm, fetch: { - admissionModes: services.cohortCreation.fetchAdmissionModes, - entryModes: services.cohortCreation.fetchEntryModes, - exitModes: services.cohortCreation.fetchExitModes, - priseEnChargeType: services.cohortCreation.fetchPriseEnChargeType, - typeDeSejour: services.cohortCreation.fetchTypeDeSejour, - fileStatus: services.cohortCreation.fetchFileStatus, - reason: services.cohortCreation.fetchReason, - destination: services.cohortCreation.fetchDestination, - provenance: services.cohortCreation.fetchProvenance, - admission: services.cohortCreation.fetchAdmission, - encounterStatus: services.cohortCreation.fetchEncounterStatus + admissionModes: async(() => getCodeList(getConfig().core.valueSets.encounterAdmissionMode.url)), + entryModes: async(() => getCodeList(getConfig().core.valueSets.encounterEntryMode.url)), + exitModes: async(() => getCodeList(getConfig().core.valueSets.encounterExitMode.url)), + priseEnChargeType: async(() => getCodeList(getConfig().core.valueSets.encounterVisitType.url)), + typeDeSejour: async(() => getCodeList(getConfig().core.valueSets.encounterSejourType.url)), + fileStatus: async(() => getCodeList(getConfig().core.valueSets.encounterFileStatus.url)), + reason: async(() => getCodeList(getConfig().core.valueSets.encounterExitType.url)), + destination: async(() => getCodeList(getConfig().core.valueSets.encounterDestination.url)), + provenance: async(() => getCodeList(getConfig().core.valueSets.encounterProvenance.url)), + admission: async(() => getCodeList(getConfig().core.valueSets.encounterAdmission.url)), + encounterStatus: async(() => getCodeList(getConfig().core.valueSets.encounterStatus.url)) } }, { @@ -75,8 +94,8 @@ const criteriaList: () => CriteriaItemType[] = () => { fontWeight: 'bold', components: DocumentsForm, fetch: { - docTypes: services.cohortCreation.fetchDocTypes, - encounterStatus: services.cohortCreation.fetchEncounterStatus + docTypes: () => (docTypes && docTypes.docTypes.length > 0 ? docTypes.docTypes : []), + encounterStatus: async(() => getCodeList(getConfig().core.valueSets.encounterStatus.url)) } }, { @@ -93,10 +112,9 @@ const criteriaList: () => CriteriaItemType[] = () => { fontWeight: 'normal', components: Cim10Form, fetch: { - statusDiagnostic: services.cohortCreation.fetchStatusDiagnostic, - diagnosticTypes: services.cohortCreation.fetchDiagnosticTypes, - cim10Diagnostic: services.cohortCreation.fetchCim10Diagnostic, - encounterStatus: services.cohortCreation.fetchEncounterStatus + diagnosticTypes: async(() => getCodeList(getConfig().features.condition.valueSets.conditionStatus.url)), + cim10Diagnostic: /* services.cohortCreation.fetchCim10Diagnostic,*/ [], + encounterStatus: async(() => getCodeList(getConfig().core.valueSets.encounterStatus.url)) } }, { @@ -106,8 +124,8 @@ const criteriaList: () => CriteriaItemType[] = () => { fontWeight: 'normal', components: CCAMForm, fetch: { - ccamData: services.cohortCreation.fetchCcamData, - encounterStatus: services.cohortCreation.fetchEncounterStatus + ccamData: /*services.cohortCreation.fetchCcamData*/ [], + encounterStatus: async(() => getCodeList(getConfig().core.valueSets.encounterStatus.url)) } }, { @@ -117,8 +135,8 @@ const criteriaList: () => CriteriaItemType[] = () => { fontWeight: 'normal', components: GhmForm, fetch: { - ghmData: services.cohortCreation.fetchGhmData, - encounterStatus: services.cohortCreation.fetchEncounterStatus + ghmData: /*services.cohortCreation.fetchGhmData*/ [], + encounterStatus: async(() => getCodeList(getConfig().core.valueSets.encounterStatus.url)) } } ] @@ -131,10 +149,25 @@ const criteriaList: () => CriteriaItemType[] = () => { components: MedicationForm, disabled: !ODD_MEDICATION, fetch: { - medicationData: services.cohortCreation.fetchMedicationData, - prescriptionTypes: services.cohortCreation.fetchPrescriptionTypes, - administrations: services.cohortCreation.fetchAdministrations, - encounterStatus: services.cohortCreation.fetchEncounterStatus + medicationData: async (code: string) => { + let results: Hierarchy[] = [] + try { + results = (await getChildrenFromCodes(getConfig().features.medication.valueSets.medicationUcd.url, [code])) + .results + } catch (e) { + results = (await getChildrenFromCodes(getConfig().features.medication.valueSets.medicationAtc.url, [code])) + .results + } finally { + return results + } + }, + prescriptionTypes: async(() => + getCodeList(getConfig().features.medication.valueSets.medicationPrescriptionTypes.url) + ), + administrations: async(() => + getCodeList(getConfig().features.medication.valueSets.medicationAdministrations.url) + ), + encounterStatus: async(() => getCodeList(getConfig().core.valueSets.encounterStatus.url)) } }, { @@ -152,8 +185,8 @@ const criteriaList: () => CriteriaItemType[] = () => { components: BiologyForm, disabled: !ODD_BIOLOGY, fetch: { - biologyData: services.cohortCreation.fetchBiologyData, - encounterStatus: services.cohortCreation.fetchEncounterStatus + biologyData: /*services.cohortCreation.fetchBiologyData*/ [], + encounterStatus: async(() => getCodeList(getConfig().core.valueSets.encounterStatus.url)) } }, { @@ -188,14 +221,22 @@ const criteriaList: () => CriteriaItemType[] = () => { disabled: !ODD_QUESTIONNAIRE, components: PregnantForm, fetch: { - pregnancyMode: services.cohortCreation.fetchPregnancyMode, - maternalRisks: services.cohortCreation.fetchMaternalRisks, - risksRelatedToObstetricHistory: services.cohortCreation.fetchRisksRelatedToObstetricHistory, - risksOrComplicationsOfPregnancy: services.cohortCreation.fetchRisksOrComplicationsOfPregnancy, - corticotherapie: services.cohortCreation.fetchCorticotherapie, - prenatalDiagnosis: services.cohortCreation.fetchPrenatalDiagnosis, - ultrasoundMonitoring: services.cohortCreation.fetchUltrasoundMonitoring, - encounterStatus: services.cohortCreation.fetchEncounterStatus + pregnancyMode: async(() => + getCodeList(getConfig().features.questionnaires.valueSets.pregnancyMode.url) + ), + maternalRisks: async(() => + getCodeList(getConfig().features.questionnaires.valueSets.maternalRisks.url) + ), + risksRelatedToObstetricHistory: async(() => + getCodeList(getConfig().features.questionnaires.valueSets.risksRelatedToObstetricHistory.url) + ), + risksOrComplicationsOfPregnancy: async(() => + getCodeList(getConfig().features.questionnaires.valueSets.risksOrComplicationsOfPregnancy.url) + ), + corticotherapie: () => booleanFieldsData, + prenatalDiagnosis: () => booleanFieldsData, + ultrasoundMonitoring: () => booleanFieldsData, + encounterStatus: async(() => getCodeList(getConfig().core.valueSets.encounterStatus.url)) } }, { @@ -206,36 +247,70 @@ const criteriaList: () => CriteriaItemType[] = () => { disabled: !ODD_QUESTIONNAIRE, components: HospitForm, fetch: { - inUteroTransfer: services.cohortCreation.fetchInUteroTransfer, - pregnancyMonitoring: services.cohortCreation.fetchPregnancyMonitoring, - maturationCorticotherapie: services.cohortCreation.fetchMaturationCorticotherapie, - chirurgicalGesture: services.cohortCreation.fetchChirurgicalGesture, - vme: services.cohortCreation.fetchVme, - childbirth: services.cohortCreation.fetchChildbirth, - hospitalChildBirthPlace: services.cohortCreation.fetchHospitalChildBirthPlace, - otherHospitalChildBirthPlace: services.cohortCreation.fetchOtherHospitalChildBirthPlace, - homeChildBirthPlace: services.cohortCreation.fetchHomeChildBirthPlace, - childbirthMode: services.cohortCreation.fetchChildbirthMode, - maturationReason: services.cohortCreation.fetchMaturationReason, - maturationModality: services.cohortCreation.fetchMaturationModality, - imgIndication: services.cohortCreation.fetchImgIndication, - laborOrCesareanEntry: services.cohortCreation.fetchLaborOrCesareanEntry, - pathologyDuringLabor: services.cohortCreation.fetchPathologyDuringLabor, - obstetricalGestureDuringLabor: services.cohortCreation.fetchObstetricalGestureDuringLabor, - analgesieType: services.cohortCreation.fetchAnalgesieType, - birthDeliveryWay: services.cohortCreation.fetchBirthDeliveryWay, - instrumentType: services.cohortCreation.fetchInstrumentType, - cSectionModality: services.cohortCreation.fetchCSectionModality, - presentationAtDelivery: services.cohortCreation.fetchPresentationAtDelivery, - birthStatus: services.cohortCreation.fetchBirthStatus, - postpartumHemorrhage: services.cohortCreation.fetchSetPostpartumHemorrhage, - conditionPerineum: services.cohortCreation.fetchConditionPerineum, - exitPlaceType: services.cohortCreation.fetchExitPlaceType, - feedingType: services.cohortCreation.fetchFeedingType, - complication: services.cohortCreation.fetchComplication, - exitFeedingMode: services.cohortCreation.fetchExitFeedingMode, - exitDiagnostic: services.cohortCreation.fetchExitDiagnostic, - encounterStatus: services.cohortCreation.fetchEncounterStatus + inUteroTransfer: () => booleanOpenChoiceFieldsData, + pregnancyMonitoring: () => booleanFieldsData, + maturationCorticotherapie: () => booleanOpenChoiceFieldsData, + chirurgicalGesture: async(() => + getCodeList(getConfig().features.questionnaires.valueSets.chirurgicalGesture.url) + ), + vme: () => vmeData, + childbirth: () => booleanOpenChoiceFieldsData, + hospitalChildBirthPlace: () => booleanFieldsData, + otherHospitalChildBirthPlace: () => booleanFieldsData, + homeChildBirthPlace: () => booleanFieldsData, + childbirthMode: async(() => + getCodeList(getConfig().features.questionnaires.valueSets.childBirthMode.url) + ), + maturationReason: async(() => + getCodeList(getConfig().features.questionnaires.valueSets.maturationReason.url) + ), + maturationModality: async(() => + getCodeList(getConfig().features.questionnaires.valueSets.maturationModality.url) + ), + imgIndication: async(() => + getCodeList(getConfig().features.questionnaires.valueSets.imgIndication.url) + ), + laborOrCesareanEntry: async(() => + getCodeList(getConfig().features.questionnaires.valueSets.laborOrCesareanEntry.url) + ), + pathologyDuringLabor: async(() => + getCodeList(getConfig().features.questionnaires.valueSets.pathologyDuringLabor.url) + ), + obstetricalGestureDuringLabor: async(() => + getCodeList(getConfig().features.questionnaires.valueSets.obstetricalGestureDuringLabor.url) + ), + analgesieType: async(() => + getCodeList(getConfig().features.questionnaires.valueSets.analgesieType.url) + ), + birthDeliveryWay: async(() => + getCodeList(getConfig().features.questionnaires.valueSets.birthDeliveryWay.url) + ), + instrumentType: async(() => + getCodeList(getConfig().features.questionnaires.valueSets.instrumentType.url) + ), + cSectionModality: async(() => + getCodeList(getConfig().features.questionnaires.valueSets.cSectionModality.url) + ), + presentationAtDelivery: async(() => + getCodeList(getConfig().features.questionnaires.valueSets.presentationAtDelivery.url) + ), + birthStatus: () => birthStatusData, + postpartumHemorrhage: () => booleanOpenChoiceFieldsData, + conditionPerineum: async(() => + getCodeList(getConfig().features.questionnaires.valueSets.conditionPerineum.url) + ), + exitPlaceType: async(() => + getCodeList(getConfig().features.questionnaires.valueSets.exitPlaceType.url) + ), + feedingType: async(() => getCodeList(getConfig().features.questionnaires.valueSets.feedingType.url)), + complication: () => booleanFieldsData, + exitFeedingMode: async(() => + getCodeList(getConfig().features.questionnaires.valueSets.exitFeedingMode.url) + ), + exitDiagnostic: async(() => + getCodeList(getConfig().features.questionnaires.valueSets.exitDiagnostic.url) + ), + encounterStatus: async(() => getCodeList(getConfig().core.valueSets.encounterStatus.url)) } } ] @@ -250,8 +325,8 @@ const criteriaList: () => CriteriaItemType[] = () => { components: ImagingForm, disabled: !ODD_IMAGING, fetch: { - modalities: services.cohortCreation.fetchModalities, - encounterStatus: services.cohortCreation.fetchEncounterStatus + modalities: async(() => getCodeList(getConfig().features.imaging.valueSets.imagingModalities.url, true)), + encounterStatus: async(() => getCodeList(getConfig().core.valueSets.encounterStatus.url)) } }, { diff --git a/src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/AdvancedInputs/AdvancedInputs.tsx b/src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/AdvancedInputs/AdvancedInputs.tsx deleted file mode 100644 index c3d0a4079..000000000 --- a/src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/AdvancedInputs/AdvancedInputs.tsx +++ /dev/null @@ -1,140 +0,0 @@ -import React, { useState } from 'react' - -import { Collapse, FormLabel, Grid, IconButton, Typography } from '@mui/material' - -import KeyboardArrowUpIcon from '@mui/icons-material/KeyboardArrowUp' -import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown' - -import { SourceType } from 'types/scope' -import { Hierarchy } from 'types/hierarchy' -import { ScopeElement } from 'types' -import CalendarRange from 'components/ui/Inputs/CalendarRange' - -import { CriteriaType, SelectedCriteriaTypesWithAdvancedInputs } from 'types/requestCriterias' -import { BlockWrapper } from 'components/ui/Layout' -import { DurationRangeType } from 'types/searchCriterias' -import { CriteriaLabel } from 'components/ui/CriteriaLabel' -import { getOccurenceDateLabel } from 'utils/requestCriterias' -import ExecutiveUnitsInput from 'components/ui/Inputs/ExecutiveUnit' - -type AdvancedInputsProps = { - sourceType: SourceType - selectedCriteria: SelectedCriteriaTypesWithAdvancedInputs - onChangeValue: (key: string, value: ScopeElement[] | DurationRangeType | boolean | string | null | undefined) => void - onError: (error: boolean) => void -} - -const AdvancedInputs = ({ sourceType, selectedCriteria, onChangeValue, onError }: AdvancedInputsProps) => { - const optionsIsUsed = - (selectedCriteria.encounterService && selectedCriteria.encounterService.length > 0) || - selectedCriteria.startOccurrence?.[0] !== null || - selectedCriteria.startOccurrence?.[1] !== null || - selectedCriteria.endOccurrence?.[0] !== null || - selectedCriteria.endOccurrence?.[1] !== null || - selectedCriteria.encounterStartDate[0] !== null || - selectedCriteria.encounterStartDate[1] !== null || - selectedCriteria.encounterEndDate[0] !== null || - selectedCriteria.encounterEndDate[1] !== null - - const [checked, setChecked] = useState(optionsIsUsed) - - const _onSubmitExecutiveUnits = (_selectedExecutiveUnits: Hierarchy[]) => { - onChangeValue('encounterService', _selectedExecutiveUnits) - } - - return ( - - setChecked(!checked)} - > - - Options avancées - - - - {checked ? : } - - - - - - - - - - - - Début de prise en charge - - { - onChangeValue('encounterStartDate', newDate) - }} - onError={(isError) => onError(isError)} - includeNullValues={selectedCriteria.includeEncounterStartDateNull} - onChangeIncludeNullValues={(includeNullValues) => - onChangeValue('includeEncounterStartDateNull', includeNullValues) - } - /> - - Fin de prise en charge - - { - onChangeValue('encounterEndDate', newDate) - }} - onError={(isError) => onError(isError)} - includeNullValues={selectedCriteria.includeEncounterEndDateNull} - onChangeIncludeNullValues={(includeNullValues) => - onChangeValue('includeEncounterEndDateNull', includeNullValues) - } - /> - - - {selectedCriteria.type !== CriteriaType.IMAGING && ( - <> - - - { - onChangeValue('startOccurrence', newDate) - }} - onError={(isError) => onError(isError)} - /> - - {selectedCriteria.type === CriteriaType.MEDICATION_REQUEST && ( - - - { - onChangeValue('endOccurrence', newDate) - }} - onError={(isError) => onError(isError)} - /> - - )} - - )} - - - ) -} - -export default AdvancedInputs diff --git a/src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/AdvancedInputs/index.tsx b/src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/AdvancedInputs/index.tsx new file mode 100644 index 000000000..3026469ee --- /dev/null +++ b/src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/AdvancedInputs/index.tsx @@ -0,0 +1,142 @@ +import React, { useState } from 'react' + +import { Collapse, FormLabel, Grid, IconButton, Typography } from '@mui/material' + +import KeyboardArrowUpIcon from '@mui/icons-material/KeyboardArrowUp' +import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown' + +import { SourceType } from 'types/scope' +import { Hierarchy } from 'types/hierarchy' +import { ScopeElement } from 'types' +import CalendarRange from 'components/ui/Inputs/CalendarRange' + +import { CriteriaType, SelectedCriteriaTypesWithAdvancedInputs } from 'types/requestCriterias' +import { DurationRangeType } from 'types/searchCriterias' +import { CriteriaLabel } from 'components/ui/CriteriaLabel' +import { getOccurenceDateLabel } from 'utils/requestCriterias' +import ExecutiveUnitsInput from 'components/ui/Inputs/ExecutiveUnits' +import { InputWrapper } from 'components/ui/Inputs' + +type AdvancedInputsProps = { + sourceType: SourceType + selectedCriteria: SelectedCriteriaTypesWithAdvancedInputs + onChangeValue: (key: string, value: ScopeElement[] | DurationRangeType | boolean | string | null | undefined) => void + onError: (error: boolean) => void +} + +const AdvancedInputs = ({ sourceType, selectedCriteria, onChangeValue, onError }: AdvancedInputsProps) => { + const optionsIsUsed = + (selectedCriteria.encounterService && selectedCriteria.encounterService.length > 0) || + selectedCriteria.startOccurrence?.[0] !== null || + selectedCriteria.startOccurrence?.[1] !== null || + selectedCriteria.endOccurrence?.[0] !== null || + selectedCriteria.endOccurrence?.[1] !== null || + selectedCriteria.encounterStartDate[0] !== null || + selectedCriteria.encounterStartDate[1] !== null || + selectedCriteria.encounterEndDate[0] !== null || + selectedCriteria.encounterEndDate[1] !== null + + const [checked, setChecked] = useState(optionsIsUsed) + + const _onSubmitExecutiveUnits = (_selectedExecutiveUnits: Hierarchy[]) => { + onChangeValue('encounterService', _selectedExecutiveUnits) + } + + return ( + <> + setChecked(!checked)} + > + + Options avancées + + + + {checked ? : } + + + + + + + + + + + + + Début de prise en charge + + { + onChangeValue('encounterStartDate', newDate) + }} + onError={(isError) => onError(isError)} + includeNullValues={selectedCriteria.includeEncounterStartDateNull} + onChangeIncludeNullValues={(includeNullValues) => + onChangeValue('includeEncounterStartDateNull', includeNullValues) + } + /> + + Fin de prise en charge + + { + onChangeValue('encounterEndDate', newDate) + }} + onError={(isError) => onError(isError)} + includeNullValues={selectedCriteria.includeEncounterEndDateNull} + onChangeIncludeNullValues={(includeNullValues) => + onChangeValue('includeEncounterEndDateNull', includeNullValues) + } + /> + + + {selectedCriteria.type !== CriteriaType.IMAGING && ( + <> + + + { + onChangeValue('startOccurrence', newDate) + }} + onError={(isError) => onError(isError)} + /> + + {selectedCriteria.type === CriteriaType.MEDICATION_REQUEST && ( + + + { + onChangeValue('endOccurrence', newDate) + }} + onError={(isError) => onError(isError)} + /> + + )} + + )} + + + + ) +} + +export default AdvancedInputs diff --git a/src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/BiologyForm/components/BiologySearch/BiologySearch.tsx b/src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/BiologyForm/components/BiologySearch/BiologySearch.tsx deleted file mode 100644 index 1f5abaa5d..000000000 --- a/src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/BiologyForm/components/BiologySearch/BiologySearch.tsx +++ /dev/null @@ -1,269 +0,0 @@ -import React, { KeyboardEvent, UIEvent, useEffect, useState } from 'react' - -import { - Breadcrumbs, - Button, - CircularProgress, - Divider, - Grid, - IconButton, - InputAdornment, - InputBase, - Link, - List, - ListItem, - ListItemIcon, - ListItemText, - Tab, - Tabs, - Tooltip, - Typography -} from '@mui/material' - -import ClearIcon from '@mui/icons-material/Clear' -import KeyboardBackspaceIcon from '@mui/icons-material/KeyboardBackspace' -import SearchIcon from 'assets/icones/search.svg?react' - -import useStyles from './styles' -import { useDebounce } from 'utils/debounce' -import { ValueSetWithHierarchy } from 'services/aphp/cohortCreation/fetchObservation' -import services from 'services/aphp' -import { ObservationDataType } from 'types/requestCriterias' -import { ValueSet } from 'types' -import { Hierarchy } from 'types/hierarchy' - -type BiologySearchListItemProps = { - label: string - biologyItem: ValueSet - selectedItems?: ValueSet[] | null - handleClick: (biologyItem: ValueSet[]) => void -} - -const BiologySearchListItem: React.FC = (props) => { - const { label, biologyItem, selectedItems, handleClick } = props - - const { classes, cx } = useStyles() - - const isSelected = selectedItems ? !!selectedItems.find((item) => item.code === biologyItem.code) : false - - const handleClickOnList = (biologyItem: ValueSet) => { - const _selectedItems = selectedItems ? [...selectedItems] : [] - const foundItem = _selectedItems?.find(({ code }) => code === biologyItem.code) - const isAlreadySelected = foundItem ? _selectedItems?.indexOf(foundItem) : -1 - if (isAlreadySelected === -1) { - handleClick([..._selectedItems, biologyItem]) - } else { - _selectedItems.splice(isAlreadySelected, 1) - handleClick([..._selectedItems]) - } - } - - return ( - - - - - - ) -} - -export default BiologySearch diff --git a/src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/BiologyForm/components/BiologySearch/styles.ts b/src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/BiologyForm/components/BiologySearch/styles.ts deleted file mode 100644 index 1eef8f64e..000000000 --- a/src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/BiologyForm/components/BiologySearch/styles.ts +++ /dev/null @@ -1,88 +0,0 @@ -import { makeStyles } from 'tss-react/mui' -import { Theme } from '@mui/material/styles' - -const useStyles = makeStyles()((theme: Theme) => ({ - root: { - display: 'flex', - flexDirection: 'column', - flex: '1 1 auto', - overflow: 'auto' - }, - actionContainer: { - display: 'flex', - alignItems: 'center', - height: 72, - padding: 20, - backgroundColor: '#317EAA', - color: 'white', - marginBottom: 46 - }, - backButton: { color: 'white' }, - divider: { background: 'white' }, - titleLabel: { marginLeft: '1em' }, - indicator: { - width: 20, - height: 20, - border: '2px solid currentColor', - borderRadius: 10, - backgroundColor: '#fff' - }, - selectedIndicator: { - width: 20, - height: 20, - backgroundColor: 'currentColor', - boxShadow: 'inset 0 0 0 2px white', - border: '2px solid currentColor', - borderRadius: 10 - }, - biologyItem: { - padding: '4px 16px' - }, - label: { - color: '#153D8A', - '& > span': { - textOverflow: 'ellipsis', - overflow: 'hidden', - whiteSpace: 'nowrap', - cursor: 'pointer' - } - }, - 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' - } - }, - drawerContentContainer: { - height: 'calc(100vh - 207px)', - overflow: 'auto', - margin: 12 - }, - formContainer: { - overflow: 'auto', - maxHeight: 'calc(100vh - 183px)' - }, - searchBar: { - width: 'calc(100% - 2em)', - backgroundColor: '#FFF', - border: '1px solid #D0D7D8', - boxShadow: '0px 1px 16px #0000000A', - borderRadius: '25px', - margin: '1em' - }, - input: { - marginLeft: theme.spacing(1), - flex: 1 - } -})) - -export default useStyles diff --git a/src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/BiologyForm/components/Form/BiologyForm.tsx b/src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/BiologyForm/components/Form/BiologyForm.tsx deleted file mode 100644 index 80f47e06d..000000000 --- a/src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/BiologyForm/components/Form/BiologyForm.tsx +++ /dev/null @@ -1,403 +0,0 @@ -import React, { useEffect, useState } from 'react' - -import { - Alert, - Autocomplete, - Button, - Checkbox, - Chip, - Divider, - FormLabel, - Grid, - IconButton, - MenuItem, - Select, - Switch, - TextField, - Tooltip, - Typography -} from '@mui/material' - -import KeyboardBackspaceIcon from '@mui/icons-material/KeyboardBackspace' - -import useStyles from './styles' -import { useAppDispatch, useAppSelector } from 'state' -import { fetchBiology } from 'state/biology' -import { CriteriaItemDataCache, HierarchyTree } from 'types' -import AdvancedInputs from '../../../AdvancedInputs/AdvancedInputs' -import { ObservationDataType, Comparators, SelectedCriteriaType } from 'types/requestCriterias' -import services from 'services/aphp' -import { BlockWrapper } from 'components/ui/Layout' -import OccurenceInput from 'components/ui/Inputs/Occurences' -import { ErrorWrapper } from 'components/ui/Searchbar/styles' - -enum Error { - NO_ERROR, - INCOHERENT_VALUE_ERROR, - INVALID_VALUE_ERROR, - MISSING_VALUE_ERROR, - ADVANCED_INPUTS_ERROR -} -import { SourceType } from 'types/scope' -import { Hierarchy } from 'types/hierarchy' -import { CriteriaLabel } from 'components/ui/CriteriaLabel' - -type BiologyFormProps = { - isOpen: boolean - isEdition: boolean - criteriaData: CriteriaItemDataCache - selectedCriteria: ObservationDataType - // eslint-disable-next-line @typescript-eslint/no-explicit-any - onChangeValue: (key: string, value: any) => void - goBack: () => void - onChangeSelectedCriteria: (data: SelectedCriteriaType) => void -} - -const BiologyForm: React.FC = (props) => { - const { isOpen, isEdition, criteriaData, selectedCriteria, onChangeValue, onChangeSelectedCriteria, goBack } = props - - const { classes } = useStyles() - const dispatch = useAppDispatch() - const initialState: HierarchyTree | null = useAppSelector((state) => state.syncHierarchyTable) - const currentState = { ...selectedCriteria, ...initialState } - const [multiFields, setMultiFields] = useState(localStorage.getItem('multiple_fields')) - const [allowSearchByValue, setAllowSearchByValue] = useState( - typeof currentState.searchByValue[0] === 'number' || typeof currentState.searchByValue[1] === 'number' - ) - const [occurrence, setOccurrence] = useState(currentState.occurrence || 1) - const [occurrenceComparator, setOccurrenceComparator] = useState( - currentState.occurrenceComparator || Comparators.GREATER_OR_EQUAL - ) - const [error, setError] = useState(Error.NO_ERROR) - const [_searchByValues, setSearchByValues] = useState<[string, string]>([ - currentState.searchByValue[0] !== null ? currentState.searchByValue[0].toString() : '', - currentState.searchByValue[1] !== null ? currentState.searchByValue[1].toString() : '' - ]) - - const _onSubmit = () => { - const parseSearchByValue = (value: [string, string]): [number | null, number | null] => { - return [value[0] ? parseFloat(value[0]) : null, value[1] ? parseFloat(value[1]) : null] - } - onChangeSelectedCriteria({ - ...currentState, - occurrence: occurrence, - occurrenceComparator: occurrenceComparator, - searchByValue: parseSearchByValue(_searchByValues) - }) - dispatch(fetchBiology()) - } - - const defaultValuesCode = currentState.code - ? currentState.code.map((code) => { - const criteriaCode = criteriaData.data.biologyData - ? criteriaData.data.biologyData.find((g: Hierarchy) => g.id === code.id) - : null - return { - id: code.id, - label: code.label ? code.label : criteriaCode?.label ?? '?' - } - }) - : [] - - useEffect(() => { - const checkChildren = async () => { - try { - const getChildrenResp = await services.cohortCreation.fetchBiologyHierarchy(selectedCriteria.code?.[0].id) - - getChildrenResp?.length > 0 ? onChangeValue('isLeaf', false) : onChangeValue('isLeaf', true) - } catch (error) { - console.error('Erreur lors du check des enfants du code de biologie sélectionné', error) - } - } - - selectedCriteria?.code?.length === 1 && selectedCriteria?.code[0].id !== '*' - ? checkChildren() - : onChangeValue('isLeaf', false) - }, [currentState?.code]) - - useEffect(() => { - if (!currentState.isLeaf) { - setAllowSearchByValue(false) - } - }, [currentState.isLeaf]) - - useEffect(() => { - if (!allowSearchByValue) { - setSearchByValues(['', '']) - onChangeValue('searchByValue', [null, null]) - } - }, [allowSearchByValue]) - - useEffect(() => { - const floatRegex = /^-?\d*\.?\d*$/ // matches numbers, with decimals or not, negative or not - - if ( - (_searchByValues[0] && !_searchByValues[0].match(floatRegex)) || - (_searchByValues[1] && !_searchByValues[1].match(floatRegex)) - ) { - setError(Error.INVALID_VALUE_ERROR) - } else if ( - _searchByValues[0] && - _searchByValues[1] && - parseFloat(_searchByValues[0]) > parseFloat(_searchByValues[1]) - ) { - setError(Error.INCOHERENT_VALUE_ERROR) - } else if ( - allowSearchByValue && - currentState.valueComparator === Comparators.BETWEEN && - (!_searchByValues[0] || !_searchByValues[1]) - ) { - setError(Error.MISSING_VALUE_ERROR) - } else { - setError(Error.NO_ERROR) - } - }, [_searchByValues, currentState.valueComparator, allowSearchByValue]) - - const handleValueChange = (newValue: string, index: number) => { - const invalidCharRegex = /[^0-9.-]/ // matches everything that is not a number, a "," or a "." - - if (!newValue.match(invalidCharRegex)) { - const parsedNewValue = newValue !== '' ? newValue : '' - setSearchByValues(index === 0 ? [parsedNewValue, _searchByValues[1]] : [_searchByValues[0], parsedNewValue]) - } - } - - return isOpen ? ( - - - {!isEdition ? ( - <> - - - - - Ajouter un critère de biologie - - ) : ( - Modifier un critère de biologie - )} - - - - {!multiFields && ( - { - localStorage.setItem('multiple_fields', 'ok') - setMultiFields('ok') - }} - > - Tous les éléments des champs multiples sont liés par une contrainte OU - - )} - - - Les mesures de biologie sont pour l'instant restreintes aux 3870 codes ANABIO correspondants aux analyses les - plus utilisées au niveau national et à l'AP-HP. De plus, les résultats concernent uniquement les analyses - quantitatives enregistrées sur GLIMS, qui ont été validées et mises à jour depuis mars 2020. - - - - Biologie - - onChangeValue('title', e.target.value)} - /> - - - onChangeValue('isInclusive', !currentState.isInclusive)} - style={{ margin: 'auto 1em' }} - component="legend" - > - Exclure les patients qui suivent les règles suivantes - - onChangeValue('isInclusive', !event.target.checked)} - color="secondary" - /> - - - - { - setOccurrence(newOccurence) - setOccurrenceComparator(newComparator) - }} - withHierarchyInfo - /> - - - - - - - {defaultValuesCode.length > 0 ? ( - defaultValuesCode.map((valueCode, index: number) => ( - - - {valueCode.label} - - - } - onDelete={() => - onChangeValue( - 'code', - defaultValuesCode.filter((code) => code !== valueCode) - ) - } - /> - )) - ) : ( - - Veuillez ajouter des codes de biologie via les onglets Hiérarchie ou Recherche. - - )} - - - - - setAllowSearchByValue(!allowSearchByValue)} - disabled={!currentState.isLeaf} - /> - - - - handleValueChange(e.target.value, 0)} - placeholder={currentState.valueComparator === Comparators.BETWEEN ? 'Valeur minimale' : '0'} - disabled={!allowSearchByValue} - error={ - error === Error.INCOHERENT_VALUE_ERROR || - error === Error.INVALID_VALUE_ERROR || - error === Error.MISSING_VALUE_ERROR - } - /> - {currentState.valueComparator === Comparators.BETWEEN && ( - handleValueChange(e.target.value, 1)} - placeholder="Valeur maximale" - disabled={!allowSearchByValue} - error={ - error === Error.INCOHERENT_VALUE_ERROR || - error === Error.INVALID_VALUE_ERROR || - error === Error.MISSING_VALUE_ERROR - } - /> - )} - - - - {error === Error.INCOHERENT_VALUE_ERROR && ( - - La valeur minimale ne peut pas être supérieure à la valeur maximale. - - )} - {error === Error.INVALID_VALUE_ERROR && ( - Veuillez entrer un nombre valide. - )} - {error === Error.MISSING_VALUE_ERROR && ( - Veuillez entrer 2 valeurs avec ce comparateur. - )} - - - - option.label} - isOptionEqualToValue={(option, value) => option.id === value.id} - value={currentState.encounterStatus} - onChange={(e, value) => onChangeValue('encounterStatus', value)} - renderInput={(params) => } - /> - - - setError(isError ? Error.ADVANCED_INPUTS_ERROR : Error.NO_ERROR)} - /> - - - - {!isEdition && ( - - )} - - - - - ) : ( - <> - ) -} - -export default BiologyForm diff --git a/src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/BiologyForm/components/Form/styles.ts b/src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/BiologyForm/components/Form/styles.ts deleted file mode 100644 index 8765a8bcb..000000000 --- a/src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/BiologyForm/components/Form/styles.ts +++ /dev/null @@ -1,53 +0,0 @@ -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/BiologyForm/components/Hierarchy/BiologyHierarchy.tsx b/src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/BiologyForm/components/Hierarchy/BiologyHierarchy.tsx deleted file mode 100644 index 5dbfd2da1..000000000 --- a/src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/BiologyForm/components/Hierarchy/BiologyHierarchy.tsx +++ /dev/null @@ -1,241 +0,0 @@ -import React, { Fragment, useEffect, useState } from 'react' - -import { - Button, - Collapse, - Divider, - Grid, - IconButton, - LinearProgress, - ListItem, - ListItemIcon, - ListItemText, - List, - Skeleton, - Tooltip, - Typography -} from '@mui/material' - -import ExpandLess from '@mui/icons-material/ExpandLess' -import ExpandMore from '@mui/icons-material/ExpandMore' -import KeyboardBackspaceIcon from '@mui/icons-material/KeyboardBackspace' - -import { useAppDispatch, useAppSelector } from 'state' - -import { - checkIfIndeterminated, - expandItem, - findEquivalentRowInItemAndSubItems, - getHierarchySelection -} from 'utils/pmsi' - -import useStyles from './styles' -import { decrementLoadingSyncHierarchyTable, incrementLoadingSyncHierarchyTable } from 'state/syncHierarchyTable' -import { findSelectedInListAndSubItems } from 'utils/cohortCreation' -import { defaultBiology } from '../../index' -import { HierarchyTree } from 'types' -import { Hierarchy } from 'types/hierarchy' - -type BiologyListItemProps = { - biologyItem: Hierarchy - selectedItems?: Hierarchy[] | null - handleClick: (biologyItem: Hierarchy[] | null | undefined, newHierarchy?: Hierarchy[]) => void -} - -const BiologyListItem: React.FC = (props) => { - const { biologyItem, selectedItems, handleClick } = props - const { id, label, subItems } = biologyItem - - const { classes, cx } = useStyles() - const dispatch = useAppDispatch() - - const biologyHierarchy = useAppSelector((state) => state.biology.list) - const isLoadingsyncHierarchyTable = useAppSelector((state) => state.syncHierarchyTable.loading ?? 0) - const isLoadingPmsi = useAppSelector((state) => state.pmsi.syncLoading ?? 0) - - const [open, setOpen] = useState(false) - const isSelected = findSelectedInListAndSubItems(selectedItems ?? [], biologyItem, biologyHierarchy) - const isIndeterminated = checkIfIndeterminated(biologyItem, selectedItems) - const _onExpand = async (biologyCode: string) => { - if (isLoadingsyncHierarchyTable > 0 || isLoadingPmsi > 0) return - dispatch(incrementLoadingSyncHierarchyTable()) - setOpen(!open) - const newHierarchy = await expandItem( - biologyCode, - selectedItems || [], - biologyHierarchy, - defaultBiology.type, - dispatch - ) - handleClick(selectedItems, newHierarchy) - dispatch(decrementLoadingSyncHierarchyTable()) - } - - const handleClickOnHierarchy = async (biologyItem: Hierarchy) => { - if (isLoadingsyncHierarchyTable > 0 || isLoadingPmsi > 0) return - dispatch(incrementLoadingSyncHierarchyTable()) - const newSelectedItems = getHierarchySelection(biologyItem, selectedItems || [], biologyHierarchy) - handleClick(newSelectedItems) - dispatch(decrementLoadingSyncHierarchyTable()) - } - - if (!subItems || (subItems && Array.isArray(subItems) && subItems.length === 0)) { - return ( - - - - )} - - - - ) : ( - <> - ) -} - -export default BiologyHierarchy diff --git a/src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/BiologyForm/components/Hierarchy/styles.ts b/src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/BiologyForm/components/Hierarchy/styles.ts deleted file mode 100644 index 2a3422e47..000000000 --- a/src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/BiologyForm/components/Hierarchy/styles.ts +++ /dev/null @@ -1,102 +0,0 @@ -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' }, - biologyHierarchyActionContainer: { - display: 'flex', - alignItems: 'center', - justifyContent: 'center', - flexWrap: 'wrap', - borderTop: '1px solid grey', - position: 'absolute', - width: '100%', - bottom: 0, - left: 0, - '& > button': { - margin: '12px 8px' - } - }, - biologyItem: { - padding: '2px 16px' - }, - label: { - '& > span': { - textOverflow: 'ellipsis', - overflow: 'hidden', - whiteSpace: 'nowrap', - cursor: 'pointer' - } - }, - indicator: { - width: 20, - height: 20, - border: '2px solid currentColor', - borderRadius: 10, - backgroundColor: '#fff' - }, - loader: { - height: '4px', - marginTop: '2px' - }, - selectedIndicator: { - width: 20, - height: 20, - backgroundColor: 'currentColor', - boxShadow: 'inset 0 0 0 2px white', - border: '2px solid currentColor', - borderRadius: 10 - }, - indeterminateIndicator: { - color: '#555 !important', - width: 20, - height: 20, - backgroundColor: 'currentColor', - boxShadow: 'inset 0 0 0 4px white', - border: '2px solid currentColor', - borderRadius: 10 - }, - drawerContentContainer: { - height: 'calc(100vh - 207px)', - overflow: 'auto', - margin: 12 - }, - subItemsContainer: { - position: 'relative', - marginLeft: 25 - }, - subItemsContainerIndicator: { - content: '""', - position: 'absolute', - width: 2, - height: 'calc(100% + -10px)', - bottom: 15, - background: '#D0D7D8' - }, - subItemsIndicator: { - content: '""', - position: 'absolute', - width: 17, - height: 2, - marginTop: 14.5, - background: '#D0D7D8' - } -})) - -export default useStyles diff --git a/src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/BiologyForm/index.tsx b/src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/BiologyForm/index.tsx index 459cf14d1..90f26fcf1 100644 --- a/src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/BiologyForm/index.tsx +++ b/src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/BiologyForm/index.tsx @@ -1,24 +1,45 @@ -import React, { useState, useEffect, useContext } from 'react' -import { Tabs, Tab } from '@mui/material' +import React, { useState, useMemo, useEffect } from 'react' +import { + TextField, + Grid, + IconButton, + Divider, + Typography, + Alert, + FormLabel, + Switch, + Select, + MenuItem, + Autocomplete, + Button +} from '@mui/material' -import useStyles from './styles' - -import BiologyForm from './components/Form/BiologyForm' -import BiologyHierarchy from './components/Hierarchy/BiologyHierarchy' -import BiologySearch from './components/BiologySearch/BiologySearch' -import { initSyncHierarchyTableEffect, syncOnChangeFormValue } from 'utils/pmsi' -import { fetchBiology } from 'state/biology' -import { useAppDispatch, useAppSelector } from 'state' +import useStyles from '../formStyles' import { Comparators, ObservationDataType, CriteriaType } from 'types/requestCriterias' import { CriteriaDrawerComponentProps } from 'types' -import { Hierarchy } from 'types/hierarchy' -import { AppConfig } from 'config' +import { getConfig } from 'config' +import KeyboardBackspaceIcon from '@mui/icons-material/KeyboardBackspace' +import { BlockWrapper } from 'components/ui/Layout' +import OccurenceInput from 'components/ui/Inputs/Occurences' +import { CriteriaLabel } from 'components/ui/CriteriaLabel' +import ValueSetField from 'components/SearchValueSet/ValueSetField' +import { ErrorWrapper } from 'components/ui/Searchbar/styles' +import AdvancedInputs from '../AdvancedInputs' +import { SourceType } from 'types/scope' +import { getValueSetsFromSystems } from 'utils/valueSets' + +enum Error { + NO_ERROR, + INCOHERENT_VALUE_ERROR, + INVALID_VALUE_ERROR, + MISSING_VALUE_ERROR, + ADVANCED_INPUTS_ERROR +} export const defaultBiology: Omit = { type: CriteriaType.OBSERVATION, title: 'Critères de biologie', code: [], - isLeaf: false, valueComparator: Comparators.GREATER_OR_EQUAL, searchByValue: [null, null], occurrence: 1, @@ -30,95 +51,265 @@ export const defaultBiology: Omit = { encounterStatus: [] } -const Index = (props: CriteriaDrawerComponentProps) => { +const BiologyForm = (props: CriteriaDrawerComponentProps) => { const { criteriaData, selectedCriteria, onChangeSelectedCriteria, goBack } = props - const config = useContext(AppConfig) const { classes } = useStyles() - const [selectedTab, setSelectedTab] = useState<'form' | 'hierarchy' | 'search'>( - selectedCriteria ? 'form' : 'hierarchy' - ) - const [defaultCriteria, setDefaultCriteria] = useState( - (selectedCriteria as ObservationDataType) || defaultBiology + const [currentCriteria, setCurrentCriteria] = useState((selectedCriteria as ObservationDataType) || defaultBiology) + const isEdition = selectedCriteria !== null + const [multiFields, setMultiFields] = useState(localStorage.getItem('multiple_fields')) + const [searchByValuesInput, setSearchByValuesInput] = useState<[string, string]>([ + currentCriteria.searchByValue[0]?.toString() || '', + currentCriteria.searchByValue[1]?.toString() || '' + ]) + const [error, setError] = useState(Error.NO_ERROR) + + const isLeaf = useMemo( + () => currentCriteria.code.length === 1 && currentCriteria.code?.[0].inferior_levels_ids.split(',').length < 2, + [currentCriteria.code] ) - const isEdition = selectedCriteria !== null - const dispatch = useAppDispatch() - const biologyHierarchy = useAppSelector((state) => state.biology.list || {}) - - const _onChangeSelectedHierarchy = ( - newSelectedItems: Hierarchy[] | null | undefined, - newHierarchy?: Hierarchy[] - ) => { - _onChangeFormValue('code', newSelectedItems, newHierarchy) - } - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const _onChangeFormValue = async (key: string, value: any, newHierarchy: Hierarchy[] = biologyHierarchy) => - await syncOnChangeFormValue( - key, - value, - newHierarchy, - setDefaultCriteria, - selectedTab, - defaultBiology.type, - dispatch - ) - const _initSyncHierarchyTableEffect = async () => { - await initSyncHierarchyTableEffect( - biologyHierarchy, - selectedCriteria, - defaultCriteria && defaultCriteria.code ? defaultCriteria.code : [], - fetchBiology, - defaultBiology.type, - dispatch - ) + const handleSearchValue = (newValue: string, index: number) => { + const invalidCharRegex = /[^0-9.-]/ // matches everything that is not a number, a "," or a "." + if (!newValue.match(invalidCharRegex)) + setSearchByValuesInput(index === 0 ? [newValue, searchByValuesInput[1]] : [searchByValuesInput[0], newValue]) } + useEffect(() => { - _initSyncHierarchyTableEffect() + const floatRegex = /^-?\d*\.?\d*$/ // matches numbers, with decimals or not, negative or not + if (!searchByValuesInput[0].match(floatRegex) || !searchByValuesInput[1].match(floatRegex)) { + setError(Error.INVALID_VALUE_ERROR) + } else if ( + searchByValuesInput[0] && + searchByValuesInput[1] && + parseFloat(searchByValuesInput[0]) > parseFloat(searchByValuesInput[1]) + ) { + setError(Error.INCOHERENT_VALUE_ERROR) + } else if ( + currentCriteria.valueComparator === Comparators.BETWEEN && + (!searchByValuesInput[0] || !searchByValuesInput[1]) + ) { + setError(Error.MISSING_VALUE_ERROR) + } else { + const first = isLeaf && searchByValuesInput[0] ? parseFloat(searchByValuesInput[0]) : null + const second = + isLeaf && searchByValuesInput[1] && currentCriteria.valueComparator === Comparators.BETWEEN + ? parseFloat(searchByValuesInput[1]) + : null + setCurrentCriteria({ + ...currentCriteria, + searchByValue: [first, second] + }) + setError(Error.NO_ERROR) + } + }, [searchByValuesInput, currentCriteria.valueComparator, isLeaf]) + + const biologyReferences = useMemo(() => { + return getValueSetsFromSystems([ + getConfig().features.observation.valueSets.biologyHierarchyAnabio.url, + getConfig().features.observation.valueSets.biologyHierarchyLoinc.url + ]) }, []) + return ( - <> - setSelectedTab(tab)} - > - - - - - - { - - } - {selectedTab === 'search' && ( - setSelectedTab('form')} - goBack={goBack} - /> - )} - { - setSelectedTab('form')} - goBack={goBack} - /> - } - + + + {!isEdition ? ( + <> + + + + + Ajouter un critère de biologie + + ) : ( + Modifier un critère de biologie + )} + + + + {!multiFields && ( + { + localStorage.setItem('multiple_fields', 'ok') + setMultiFields('ok') + }} + > + Tous les éléments des champs multiples sont liés par une contrainte OU + + )} + + + Les mesures de biologie sont pour l'instant restreintes aux 3870 codes ANABIO correspondants aux analyses les + plus utilisées au niveau national et à l'AP-HP. De plus, les résultats concernent uniquement les analyses + quantitatives enregistrées sur GLIMS, qui ont été validées et mises à jour depuis mars 2020. + + + + Biologie + + setCurrentCriteria({ ...currentCriteria, title: e.target.value })} + /> + + + setCurrentCriteria({ ...currentCriteria, isInclusive: !currentCriteria.isInclusive })} + style={{ margin: 'auto 1em' }} + component="legend" + > + Exclure les patients qui suivent les règles suivantes + + setCurrentCriteria({ ...currentCriteria, isInclusive: !e.target.checked })} + color="secondary" + /> + + + + + setCurrentCriteria({ + ...currentCriteria, + occurrence: newOccurence, + occurrenceComparator: newComparator + }) + } + withHierarchyInfo + /> + + + + + + + setCurrentCriteria({ ...currentCriteria, code: value })} + placeholder="Sélectionner les codes" + /> + + + + + + + handleSearchValue(e.target.value, 0)} + placeholder={currentCriteria.valueComparator === Comparators.BETWEEN ? 'Valeur minimale' : '0'} + disabled={!isLeaf} + error={error !== Error.NO_ERROR && error !== Error.ADVANCED_INPUTS_ERROR} + /> + {currentCriteria.valueComparator === Comparators.BETWEEN && ( + handleSearchValue(e.target.value, 1)} + sx={{ marginLeft: '10px' }} + placeholder="Valeur maximale" + disabled={!isLeaf} + error={error !== Error.NO_ERROR && error !== Error.ADVANCED_INPUTS_ERROR} + /> + )} + + + + {error === Error.INCOHERENT_VALUE_ERROR && ( + + La valeur minimale ne peut pas être supérieure à la valeur maximale. + + )} + {error === Error.INVALID_VALUE_ERROR && ( + Veuillez entrer un nombre valide. + )} + {error === Error.MISSING_VALUE_ERROR && ( + Veuillez entrer 2 valeurs avec ce comparateur. + )} + + + + option.label} + isOptionEqualToValue={(option, value) => option.id === value.id} + value={currentCriteria.encounterStatus} + onChange={(e, value) => setCurrentCriteria({ ...currentCriteria, encounterStatus: value })} + renderInput={(params) => } + /> + + + setCurrentCriteria((prevValues) => ({ ...prevValues, [key]: value }))} + onError={(isError) => setError(isError ? Error.ADVANCED_INPUTS_ERROR : Error.NO_ERROR)} + /> + + + + {!isEdition && ( + + )} + + + + ) } -export default Index +export default BiologyForm diff --git a/src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/BiologyForm/styles.ts b/src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/BiologyForm/styles.ts deleted file mode 100644 index 45d225af8..000000000 --- a/src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/BiologyForm/styles.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { makeStyles } from 'tss-react/mui' - -const useStyles = makeStyles()(() => ({ - tabs: { - marginTop: 72, - position: 'absolute', - width: '100%' - } -})) - -export default useStyles diff --git a/src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/CCAM/components/Form/styles.ts b/src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/CCAM/components/Form/styles.ts deleted file mode 100644 index 8765a8bcb..000000000 --- a/src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/CCAM/components/Form/styles.ts +++ /dev/null @@ -1,53 +0,0 @@ -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/CCAM/components/Hierarchy/CCAMHierarchy.tsx b/src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/CCAM/components/Hierarchy/CCAMHierarchy.tsx deleted file mode 100644 index a07744a8b..000000000 --- a/src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/CCAM/components/Hierarchy/CCAMHierarchy.tsx +++ /dev/null @@ -1,240 +0,0 @@ -import React, { Fragment, useEffect, useState } from 'react' - -import { - Button, - Collapse, - Divider, - Grid, - IconButton, - LinearProgress, - List, - ListItem, - ListItemIcon, - ListItemText, - Skeleton, - Tooltip, - Typography -} from '@mui/material' - -import ExpandLess from '@mui/icons-material/ExpandLess' -import ExpandMore from '@mui/icons-material/ExpandMore' -import KeyboardBackspaceIcon from '@mui/icons-material/KeyboardBackspace' - -import { useAppDispatch, useAppSelector } from 'state' - -import { - checkIfIndeterminated, - expandItem, - findEquivalentRowInItemAndSubItems, - getHierarchySelection -} from 'utils/pmsi' - -import useStyles from './styles' -import { decrementLoadingSyncHierarchyTable, incrementLoadingSyncHierarchyTable } from 'state/syncHierarchyTable' -import { findSelectedInListAndSubItems } from 'utils/cohortCreation' -import { defaultProcedure } from '../../index' -import { HierarchyTree } from 'types' -import { CcamDataType } from 'types/requestCriterias' -import { Hierarchy } from 'types/hierarchy' - -type ProcedureListItemProps = { - procedureItem: Hierarchy - selectedItems?: Hierarchy[] | null - handleClick: (procedureItem: Hierarchy[] | null | undefined, newHierarchy?: Hierarchy[]) => void -} - -const ProcedureListItem: React.FC = (props) => { - const { procedureItem, selectedItems, handleClick } = props - const { id, label, subItems } = procedureItem - - const { classes, cx } = useStyles() - const dispatch = useAppDispatch() - - const procedureHierarchy = useAppSelector((state) => state.pmsi.procedure.list) - const isLoadingsyncHierarchyTable = useAppSelector((state) => state.syncHierarchyTable.loading ?? 0) - const isLoadingPmsi = useAppSelector((state) => state.pmsi.syncLoading ?? 0) - - const [open, setOpen] = useState(false) - const isSelected = findSelectedInListAndSubItems(selectedItems ?? [], procedureItem, procedureHierarchy) - const isIndeterminated = checkIfIndeterminated(procedureItem, selectedItems) - const _onExpand = async (procedureCode: string) => { - if (isLoadingsyncHierarchyTable > 0 || isLoadingPmsi > 0) return - dispatch(incrementLoadingSyncHierarchyTable()) - setOpen(!open) - const newHierarchy = await expandItem( - procedureCode, - selectedItems || [], - procedureHierarchy, - defaultProcedure.type, - dispatch - ) - handleClick(selectedItems, newHierarchy) - dispatch(decrementLoadingSyncHierarchyTable()) - } - - const handleClickOnHierarchy = (procedureItem: Hierarchy) => { - if (isLoadingsyncHierarchyTable > 0 || isLoadingPmsi > 0) return - dispatch(incrementLoadingSyncHierarchyTable()) - const newSelectedItems = getHierarchySelection(procedureItem, selectedItems || [], procedureHierarchy) - handleClick(newSelectedItems) - dispatch(decrementLoadingSyncHierarchyTable()) - } - - if (!subItems || (subItems && Array.isArray(subItems) && subItems.length === 0)) { - return ( - - - - )} - - - - ) : ( - <> - ) -} - -export default ProcedureHierarchy diff --git a/src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/CCAM/components/Hierarchy/styles.ts b/src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/CCAM/components/Hierarchy/styles.ts deleted file mode 100644 index f296f54ee..000000000 --- a/src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/CCAM/components/Hierarchy/styles.ts +++ /dev/null @@ -1,102 +0,0 @@ -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' }, - procedureHierarchyActionContainer: { - display: 'flex', - alignItems: 'center', - justifyContent: 'center', - flexWrap: 'wrap', - borderTop: '1px solid grey', - position: 'absolute', - width: '100%', - bottom: 0, - left: 0, - '& > button': { - margin: '12px 8px' - } - }, - procedureItem: { - padding: '2px 16px' - }, - label: { - '& > span': { - textOverflow: 'ellipsis', - overflow: 'hidden', - whiteSpace: 'nowrap', - cursor: 'pointer' - } - }, - indicator: { - width: 20, - height: 20, - border: '2px solid currentColor', - borderRadius: 10, - backgroundColor: '#fff' - }, - loader: { - height: '4px', - marginTop: '2px' - }, - selectedIndicator: { - width: 20, - height: 20, - backgroundColor: 'currentColor', - boxShadow: 'inset 0 0 0 2px white', - border: '2px solid currentColor', - borderRadius: 10 - }, - indeterminateIndicator: { - color: '#555 !important', - width: 20, - height: 20, - backgroundColor: 'currentColor', - boxShadow: 'inset 0 0 0 4px white', - border: '2px solid currentColor', - borderRadius: 10 - }, - drawerContentContainer: { - height: 'calc(100vh - 207px)', - overflow: 'auto', - margin: 12 - }, - subItemsContainer: { - position: 'relative', - marginLeft: 25 - }, - subItemsContainerIndicator: { - content: '""', - position: 'absolute', - width: 2, - height: 'calc(100% + -10px)', - bottom: 15, - background: '#D0D7D8' - }, - subItemsIndicator: { - content: '""', - position: 'absolute', - width: 17, - height: 2, - marginTop: 14.5, - background: '#D0D7D8' - } -})) - -export default useStyles diff --git a/src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/CCAM/index.tsx b/src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/CCAM/index.tsx deleted file mode 100644 index c395cf165..000000000 --- a/src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/CCAM/index.tsx +++ /dev/null @@ -1,112 +0,0 @@ -import React, { useEffect, useState } from 'react' -import { Tab, Tabs } from '@mui/material' - -import useStyles from './styles' - -import CcamForm from './components/Form/CCAMForm' -import CcamHierarchy from './components/Hierarchy/CCAMHierarchy' -import { initSyncHierarchyTableEffect, syncOnChangeFormValue } from 'utils/pmsi' -import { useAppDispatch, useAppSelector } from 'state' -import { fetchProcedure } from 'state/pmsi' -import { EXPLORATION } from '../../../../../../../..//constants' - -import { CriteriaDrawerComponentProps } from 'types' -import { CcamDataType, Comparators, CriteriaType } from 'types/requestCriterias' -import { Hierarchy } from 'types/hierarchy' - -export const defaultProcedure: Omit = { - type: CriteriaType.PROCEDURE, - title: "Critères d'actes CCAM", - label: undefined, - code: [], - source: 'AREM', - occurrence: 1, - hierarchy: undefined, - occurrenceComparator: Comparators.GREATER_OR_EQUAL, - startOccurrence: [null, null], - isInclusive: true, - encounterStartDate: [null, null], - encounterEndDate: [null, null], - encounterStatus: [] -} - -const Index = (props: CriteriaDrawerComponentProps) => { - const { criteriaData, selectedCriteria, onChangeSelectedCriteria, goBack } = props - const [selectedTab, setSelectedTab] = useState<'form' | 'hierarchy'>(selectedCriteria ? 'form' : 'hierarchy') - const [defaultCriteria, setDefaultCriteria] = useState( - (selectedCriteria as CcamDataType) || defaultProcedure - ) - - const isEdition = selectedCriteria !== null - const dispatch = useAppDispatch() - const ccamHierarchy = useAppSelector((state) => state.pmsi.procedure.list || {}) - const { classes } = useStyles() - - const _onChangeSelectedHierarchy = ( - newSelectedItems: Hierarchy[] | null | undefined, - newHierarchy?: Hierarchy[] - ) => { - _onChangeFormValue('code', newSelectedItems, newHierarchy) - } - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const _onChangeFormValue = async (key: string, value: any, newHierarchy: Hierarchy[] = ccamHierarchy) => - await syncOnChangeFormValue( - key, - value, - newHierarchy, - setDefaultCriteria, - selectedTab, - defaultProcedure.type, - dispatch - ) - const _initSyncHierarchyTableEffect = async () => { - await initSyncHierarchyTableEffect( - ccamHierarchy, - selectedCriteria, - defaultCriteria && defaultCriteria.code ? defaultCriteria.code : [], - fetchProcedure, - defaultProcedure.type, - dispatch - ) - } - useEffect(() => { - _initSyncHierarchyTableEffect() - }, []) - - return ( - <> - setSelectedTab(tab)} - > - - - - - { - - } - { - setSelectedTab('form')} - goBack={goBack} - /> - } - - ) -} -export default Index diff --git a/src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/CCAM/styles.ts b/src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/CCAM/styles.ts deleted file mode 100644 index 45d225af8..000000000 --- a/src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/CCAM/styles.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { makeStyles } from 'tss-react/mui' - -const useStyles = makeStyles()(() => ({ - tabs: { - marginTop: 72, - position: 'absolute', - width: '100%' - } -})) - -export default useStyles diff --git a/src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/CCAM/components/Form/CCAMForm.tsx b/src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/CcamForm/index.tsx similarity index 57% rename from src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/CCAM/components/Form/CCAMForm.tsx rename to src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/CcamForm/index.tsx index d7468a75f..6c3968e3b 100644 --- a/src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/CCAM/components/Form/CCAMForm.tsx +++ b/src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/CcamForm/index.tsx @@ -1,91 +1,68 @@ -import React, { useState } from 'react' - +import React, { useMemo, useState } from 'react' import { Alert, + Autocomplete, Button, Divider, + FormControlLabel, FormLabel, Grid, IconButton, - Switch, - TextField, - Typography, Radio, RadioGroup, - FormControlLabel, - Autocomplete + Switch, + TextField, + Typography } from '@mui/material' +import useStyles from '../formStyles' import KeyboardBackspaceIcon from '@mui/icons-material/KeyboardBackspace' -import AdvancedInputs from 'components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/AdvancedInputs/AdvancedInputs' - -import useStyles from './styles' -import { useAppDispatch, useAppSelector } from 'state' -import { fetchProcedure } from 'state/pmsi' -import { CriteriaItemDataCache, HierarchyTree } from 'types' -import AsyncAutocomplete from 'components/ui/Inputs/AsyncAutocomplete' -import services from 'services/aphp' -import { CcamDataType, Comparators, SelectedCriteriaType } from 'types/requestCriterias' +import { CriteriaDrawerComponentProps } from 'types' +import { CcamDataType, Comparators, CriteriaType } from 'types/requestCriterias' import { BlockWrapper } from 'components/ui/Layout' import OccurenceInput from 'components/ui/Inputs/Occurences' +import AdvancedInputs from '../AdvancedInputs' import { SourceType } from 'types/scope' -import { Hierarchy } from 'types/hierarchy' - -type CcamFormProps = { - isOpen: boolean - isEdition: boolean - criteriaData: CriteriaItemDataCache - selectedCriteria: CcamDataType - // eslint-disable-next-line @typescript-eslint/no-explicit-any - onChangeValue: (key: string, value: any) => void - goBack: () => void - onChangeSelectedCriteria: (data: SelectedCriteriaType) => void -} +import ValueSetField from 'components/SearchValueSet/ValueSetField' +import { getConfig } from 'config' +import { getValueSetsFromSystems } from 'utils/valueSets' enum Error { ADVANCED_INPUTS_ERROR, NO_ERROR } -const CcamForm: React.FC = (props) => { - const { isOpen, isEdition, criteriaData, selectedCriteria, onChangeValue, onChangeSelectedCriteria, goBack } = props +export const defaultProcedure: Omit = { + type: CriteriaType.PROCEDURE, + title: "Critères d'actes CCAM", + label: undefined, + code: [], + source: 'AREM', + occurrence: 1, + occurrenceComparator: Comparators.GREATER_OR_EQUAL, + startOccurrence: [null, null], + isInclusive: true, + encounterStartDate: [null, null], + encounterEndDate: [null, null], + encounterStatus: [] +} - const { classes } = useStyles() - const dispatch = useAppDispatch() - const initialState: HierarchyTree | null = useAppSelector((state) => state.syncHierarchyTable) - const currentState = { ...selectedCriteria, ...initialState } - const [multiFields, setMultiFields] = useState(localStorage.getItem('multiple_fields')) - const [occurrence, setOccurrence] = useState(currentState.occurrence || 1) - const [occurrenceComparator, setOccurrenceComparator] = useState( - currentState.occurrenceComparator || Comparators.GREATER_OR_EQUAL +const CcamForm = (props: CriteriaDrawerComponentProps) => { + const { criteriaData, selectedCriteria, onChangeSelectedCriteria, goBack } = props + const [currentCriteria, setCurrentCriteria] = useState( + (selectedCriteria as CcamDataType) || defaultProcedure ) + const [multiFields, setMultiFields] = useState(localStorage.getItem('multiple_fields')) const [error, setError] = useState(Error.NO_ERROR) + const isEdition = selectedCriteria !== null + const { classes } = useStyles() - const _onSubmit = () => { - onChangeSelectedCriteria({ ...currentState, occurrence: occurrence, occurrenceComparator: occurrenceComparator }) - dispatch(fetchProcedure()) - } - - const getCCAMOptions = async (searchValue: string, signal: AbortSignal) => { - const ccamOptions = await services.cohortCreation.fetchCcamData(searchValue, false, signal) - - return ccamOptions && ccamOptions.length > 0 ? ccamOptions : [] - } - - const defaultValuesCode = currentState.code - ? currentState.code.map((code) => { - const criteriaCode = criteriaData.data.ccamData - ? criteriaData.data.ccamData.find((g: Hierarchy) => g.id === code.id) - : null - return { - id: code.id, - label: code.label ? code.label : criteriaCode?.label ?? '?' - } - }) - : [] + const ccamReferences = useMemo(() => { + return getValueSetsFromSystems([getConfig().features.procedure.valueSets.procedureHierarchy.url]) + }, []) - return isOpen ? ( + return ( {!isEdition ? ( @@ -123,13 +100,13 @@ const CcamForm: React.FC = (props) => { id="criteria-name-required" placeholder="Nom du critère" variant="outlined" - value={currentState.title} - onChange={(e) => onChangeValue('title', e.target.value)} + value={currentCriteria.title} + onChange={(e) => setCurrentCriteria({ ...currentCriteria, title: e.target.value })} /> onChangeValue('isInclusive', !currentState.isInclusive)} + onClick={() => setCurrentCriteria({ ...currentCriteria, isInclusive: !currentCriteria.isInclusive })} style={{ margin: 'auto 1em' }} component="legend" > @@ -137,20 +114,22 @@ const CcamForm: React.FC = (props) => { onChangeValue('isInclusive', !event.target.checked)} + checked={!currentCriteria.isInclusive} + onChange={(e) => setCurrentCriteria({ ...currentCriteria, isInclusive: !e.target.checked })} color="secondary" /> - { - setOccurrence(newOccurence) - setOccurrenceComparator(newComparator) - }} + value={currentCriteria.occurrence || 1} + comparator={currentCriteria.occurrenceComparator || Comparators.GREATER_OR_EQUAL} + onchange={(newOccurence, newComparator) => + setCurrentCriteria({ + ...currentCriteria, + occurrence: newOccurence, + occurrenceComparator: newComparator + }) + } withHierarchyInfo /> @@ -162,8 +141,8 @@ const CcamForm: React.FC = (props) => { className={classes.inputItem} aria-label="mode" name="criteria-mode-radio" - value={currentState.source} - onChange={(e, value) => onChangeValue('source', value)} + value={currentCriteria.source} + onChange={(e, value) => setCurrentCriteria({ ...currentCriteria, source: value })} > } label="AREM" /> } label="ORBIS" /> @@ -188,17 +167,14 @@ const CcamForm: React.FC = (props) => { remontées aux tutelles et disponibles dans le SNDS. - - onChangeValue('code', value)} - /> - + + setCurrentCriteria({ ...currentCriteria, code: value })} + placeholder="Sélectionner les codes d'actes CCAM" + /> + = (props) => { noOptionsText="Veuillez entrer un statut de visite associée" getOptionLabel={(option) => option.label} isOptionEqualToValue={(option, value) => option.id === value.id} - value={currentState.encounterStatus} - onChange={(e, value) => onChangeValue('encounterStatus', value)} + value={currentCriteria.encounterStatus} + onChange={(e, value) => setCurrentCriteria({ ...currentCriteria, encounterStatus: value })} renderInput={(params) => } /> setCurrentCriteria((prevValues) => ({ ...prevValues, [key]: value }))} onError={(isError) => setError(isError ? Error.ADVANCED_INPUTS_ERROR : Error.NO_ERROR)} /> @@ -226,7 +202,7 @@ const CcamForm: React.FC = (props) => { )} - )} - - - - - ) : ( - <> - ) -} - -export default Cim10Form diff --git a/src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/Cim10Form/components/Form/styles.ts b/src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/Cim10Form/components/Form/styles.ts deleted file mode 100644 index 8765a8bcb..000000000 --- a/src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/Cim10Form/components/Form/styles.ts +++ /dev/null @@ -1,53 +0,0 @@ -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/Cim10Form/components/Hierarchy/Cim10Hierarchy.tsx b/src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/Cim10Form/components/Hierarchy/Cim10Hierarchy.tsx deleted file mode 100644 index f92b1e2ce..000000000 --- a/src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/Cim10Form/components/Hierarchy/Cim10Hierarchy.tsx +++ /dev/null @@ -1,244 +0,0 @@ -import React, { Fragment, useEffect, useState } from 'react' - -import { - Button, - Collapse, - Divider, - Grid, - IconButton, - LinearProgress, - List, - ListItem, - ListItemIcon, - ListItemText, - Skeleton, - Tooltip, - Typography -} from '@mui/material' - -import ExpandLess from '@mui/icons-material/ExpandLess' -import ExpandMore from '@mui/icons-material/ExpandMore' -import KeyboardBackspaceIcon from '@mui/icons-material/KeyboardBackspace' - -import { useAppDispatch, useAppSelector } from 'state' - -import { - checkIfIndeterminated, - expandItem, - findEquivalentRowInItemAndSubItems, - getHierarchySelection -} from 'utils/pmsi' - -import useStyles from './styles' -import { findSelectedInListAndSubItems } from 'utils/cohortCreation' -import { decrementLoadingSyncHierarchyTable, incrementLoadingSyncHierarchyTable } from 'state/syncHierarchyTable' -import { defaultCondition } from '../../index' -import { HierarchyTree } from 'types' -import { Cim10DataType } from 'types/requestCriterias' -import { Hierarchy } from 'types/hierarchy' - -type CimListItemProps = { - cim10Item: Hierarchy - selectedItems?: Hierarchy[] | null - handleClick: (cimItem: Hierarchy[] | null | undefined, newHierarchy?: Hierarchy[]) => void -} - -const CimListItem: React.FC = (props) => { - const { cim10Item, selectedItems, handleClick } = props - const { id, label, subItems } = cim10Item - - const { classes, cx } = useStyles() - const dispatch = useAppDispatch() - - const cim10Hierarchy = useAppSelector((state) => state.pmsi.condition.list || {}) - const isLoadingsyncHierarchyTable = useAppSelector((state) => state.syncHierarchyTable.loading || 0) - const isLoadingPmsi = useAppSelector((state) => state.pmsi.syncLoading || 0) - - const [open, setOpen] = useState(false) - const isSelected = findSelectedInListAndSubItems(selectedItems ? selectedItems : [], cim10Item, cim10Hierarchy) - const isIndeterminated = checkIfIndeterminated(cim10Item, selectedItems) - const _onExpand = async (cim10Code: string) => { - if (isLoadingsyncHierarchyTable > 0 || isLoadingPmsi > 0) return - dispatch(incrementLoadingSyncHierarchyTable()) - setOpen(!open) - const newHierarchy = await expandItem( - cim10Code, - selectedItems || [], - cim10Hierarchy, - defaultCondition.type, - dispatch - ) - await handleClick(selectedItems, newHierarchy) - dispatch(decrementLoadingSyncHierarchyTable()) - } - - const handleClickOnHierarchy = async (cim10Item: Hierarchy) => { - if (isLoadingsyncHierarchyTable > 0 || isLoadingPmsi > 0) return - dispatch(incrementLoadingSyncHierarchyTable()) - const newSelectedItems = getHierarchySelection(cim10Item, selectedItems || [], cim10Hierarchy) - await handleClick(newSelectedItems) - dispatch(decrementLoadingSyncHierarchyTable()) - } - - if (!subItems || (subItems && Array.isArray(subItems) && subItems.length === 0)) { - return ( - - -
handleClickOnHierarchy(cim10Item)} - className={cx(classes.indicator, { - [classes.selectedIndicator]: isSelected, - [classes.indeterminateIndicator]: isIndeterminated - })} - style={{ color: '#0063af', cursor: 'pointer' }} - /> - - - handleClickOnHierarchy(cim10Item)} className={classes.label} primary={label} /> - - - ) - } - - return ( - <> - - -
handleClickOnHierarchy(cim10Item)} - className={cx(classes.indicator, { - [classes.selectedIndicator]: isSelected, - [classes.indeterminateIndicator]: isIndeterminated - })} - style={{ color: '#0063af', cursor: 'pointer' }} - /> - - - _onExpand(id)} className={classes.label} primary={label} /> - - {id !== '*' && - (open ? setOpen(!open)} /> : _onExpand(id)} />)} - - - -
- {subItems && - subItems.map((cimHierarchySubItem: Hierarchy, index: number) => - cimHierarchySubItem.id === 'loading' ? ( - -
- - - ) : ( - -
- - - ) - )} - - - - ) -} - -type Cim10HierarchyProps = { - isOpen: boolean - selectedCriteria: Cim10DataType - goBack: () => void - onChangeSelectedHierarchy: ( - data: Hierarchy[] | null | undefined, - newHierarchy?: Hierarchy[] - ) => void - isEdition?: boolean - onConfirm: () => void -} - -const Cim10Hierarchy: React.FC = (props) => { - const { isOpen = false, selectedCriteria, onChangeSelectedHierarchy, onConfirm, goBack, isEdition } = props - - const { classes } = useStyles() - const initialState: HierarchyTree | null = useAppSelector((state) => state.syncHierarchyTable) - const isLoadingSyncHierarchyTable = initialState?.loading ?? 0 - const isLoadingPmsi = useAppSelector((state) => state.pmsi.syncLoading || 0) - const [currentState, setCurrentState] = useState({ ...selectedCriteria, ...initialState }) - const [loading, setLoading] = useState(isLoadingSyncHierarchyTable > 0 || isLoadingPmsi > 0) - - const cim10Hierarchy = useAppSelector((state) => state.pmsi.condition.list || {}) - - useEffect(() => { - const newList = { ...selectedCriteria, ...initialState } ?? {} - if (!newList.code) { - newList.code = selectedCriteria.code - } - newList.code?.map( - (item: Hierarchy) => findEquivalentRowInItemAndSubItems(item, cim10Hierarchy).equivalentRow - ) - setCurrentState(newList) - }, [initialState, cim10Hierarchy]) - - const _handleClick = ( - newSelectedItems: Hierarchy[] | null | undefined, - newHierarchy?: Hierarchy[] - ) => { - onChangeSelectedHierarchy(newSelectedItems, newHierarchy) - } - useEffect(() => { - if (isLoadingSyncHierarchyTable > 0 || isLoadingPmsi > 0) { - setLoading(true) - } else if (isLoadingSyncHierarchyTable === 0 && isLoadingPmsi === 0) { - setLoading(false) - } - }, [isLoadingSyncHierarchyTable, isLoadingPmsi]) - - return isOpen ? ( - <> - - - {!isEdition ? ( - <> - - - - - Ajouter un critère de diagnostic - - ) : ( - Modifier un critère de diagnostic - )} - -
{loading && }
- - {cim10Hierarchy && - cim10Hierarchy.map((cim10Item, index) => ( - - ))} - - - - {!isEdition && ( - - )} - - -
- - ) : ( - <> - ) -} - -export default Cim10Hierarchy diff --git a/src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/Cim10Form/components/Hierarchy/styles.ts b/src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/Cim10Form/components/Hierarchy/styles.ts deleted file mode 100644 index 478271216..000000000 --- a/src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/Cim10Form/components/Hierarchy/styles.ts +++ /dev/null @@ -1,101 +0,0 @@ -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' }, - cimHierarchyActionContainer: { - display: 'flex', - alignItems: 'center', - justifyContent: 'center', - flexWrap: 'wrap', - borderTop: '1px solid grey', - position: 'absolute', - width: '100%', - bottom: 0, - left: 0, - '& > button': { - margin: '12px 8px' - } - }, - cimItem: { - padding: '2px 16px' - }, - label: { - '& > span': { - textOverflow: 'ellipsis', - overflow: 'hidden', - whiteSpace: 'nowrap', - cursor: 'pointer' - } - }, - indicator: { - width: 20, - height: 20, - border: '2px solid currentColor', - borderRadius: 10 - }, - loader: { - height: '4px', - marginTop: '2px' - }, - selectedIndicator: { - width: 20, - height: 20, - backgroundColor: 'currentColor', - boxShadow: 'inset 0 0 0 2px white', - border: '2px solid currentColor', - borderRadius: 10 - }, - indeterminateIndicator: { - color: '#555 !important', - width: 20, - height: 20, - backgroundColor: 'currentColor', - boxShadow: 'inset 0 0 0 4px white', - border: '2px solid currentColor', - borderRadius: 10 - }, - drawerContentContainer: { - height: 'calc(100vh - 207px)', - overflow: 'auto', - margin: 12 - }, - subItemsContainer: { - position: 'relative', - marginLeft: 25 - }, - subItemsContainerIndicator: { - content: '""', - position: 'absolute', - width: 2, - height: 'calc(100% + -10px)', - bottom: 15, - background: '#D0D7D8' - }, - subItemsIndicator: { - content: '""', - position: 'absolute', - width: 17, - height: 2, - marginTop: 14.5, - background: '#D0D7D8' - } -})) - -export default useStyles diff --git a/src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/Cim10Form/index.tsx b/src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/Cim10Form/index.tsx index fadc12a81..cace52070 100644 --- a/src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/Cim10Form/index.tsx +++ b/src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/Cim10Form/index.tsx @@ -1,17 +1,38 @@ -import React, { useEffect, useState } from 'react' -import { Tab, Tabs } from '@mui/material' +import React, { useMemo, useState } from 'react' +import { + Alert, + Autocomplete, + Button, + Divider, + FormControlLabel, + FormLabel, + Grid, + IconButton, + Radio, + RadioGroup, + Switch, + TextField, + Typography +} from '@mui/material' -import Cim10Form from './components/Form/Cim10Form' -import Cim10Hierarchy from './components/Hierarchy/Cim10Hierarchy' +import KeyboardBackspaceIcon from '@mui/icons-material/KeyboardBackspace' -import useStyles from './styles' -import { initSyncHierarchyTableEffect, syncOnChangeFormValue } from 'utils/pmsi' -import { useAppDispatch, useAppSelector } from 'state' -import { fetchCondition } from 'state/pmsi' -import { EXPLORATION } from '../../../../../../../../constants' +import useStyles from '../formStyles' import { CriteriaDrawerComponentProps } from 'types' import { Cim10DataType, Comparators, CriteriaType } from 'types/requestCriterias' -import { Hierarchy } from 'types/hierarchy' +import { FhirItem, Hierarchy } from 'types/hierarchy' +import { BlockWrapper } from 'components/ui/Layout' +import OccurenceInput from 'components/ui/Inputs/Occurences' +import AdvancedInputs from '../AdvancedInputs' +import { SourceType } from 'types/scope' +import { getConfig } from 'config' +import ValueSetField from 'components/SearchValueSet/ValueSetField' +import { getValueSetsFromSystems } from 'utils/valueSets' + +enum Error { + ADVANCED_INPUTS_ERROR, + NO_ERROR +} export const defaultCondition: Omit = { type: CriteriaType.CONDITION, @@ -29,83 +50,191 @@ export const defaultCondition: Omit = { encounterStatus: [] } -const Index = (props: CriteriaDrawerComponentProps) => { +const Cim10Form = (props: CriteriaDrawerComponentProps) => { const { criteriaData, selectedCriteria, onChangeSelectedCriteria, goBack } = props - const [selectedTab, setSelectedTab] = useState<'form' | 'hierarchy'>(selectedCriteria ? 'form' : 'hierarchy') - const [defaultCriteria, setDefaultCriteria] = useState( + const [currentCriteria, setCurrentCriteria] = useState( (selectedCriteria as Cim10DataType) || defaultCondition ) - + const [error, setError] = useState(Error.NO_ERROR) + const [multiFields, setMultiFields] = useState(localStorage.getItem('multiple_fields')) const isEdition = selectedCriteria !== null - const dispatch = useAppDispatch() - const cim10Hierarchy = useAppSelector((state) => state.pmsi.condition.list || {}) - const { classes } = useStyles() - const _onChangeSelectedHierarchy = ( - newSelectedItems: Hierarchy[] | null | undefined, - newHierarchy?: Hierarchy[] - ) => { - _onChangeFormValue('code', newSelectedItems, newHierarchy) - } - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const _onChangeFormValue = async (key: string, value: any, newHierarchy: Hierarchy[] = cim10Hierarchy) => - await syncOnChangeFormValue( - key, - value, - newHierarchy, - setDefaultCriteria, - selectedTab, - defaultCondition.type, - dispatch - ) - const _initSyncHierarchyTableEffect = async () => { - await initSyncHierarchyTableEffect( - cim10Hierarchy, - selectedCriteria, - defaultCriteria && defaultCriteria.code ? defaultCriteria.code : [], - fetchCondition, - defaultCondition.type, - dispatch - ) - } - useEffect(() => { - _initSyncHierarchyTableEffect() - }, []) + const defaultValuesType = currentCriteria.diagnosticType + ? currentCriteria.diagnosticType.map((diagnosticType) => { + const criteriaType = criteriaData.data.diagnosticTypes + ? criteriaData.data.diagnosticTypes.find((g: Hierarchy) => g.id === diagnosticType.id) + : null + return { + id: diagnosticType.id, + label: diagnosticType.label ? diagnosticType.label : criteriaType?.label ?? '?' + } + }) + : [] + const cim10References = useMemo(() => { + return getValueSetsFromSystems([getConfig().features.condition.valueSets.conditionHierarchy.url]) + }, []) return ( - <> - setSelectedTab(tab)} - > - - - + + + {!isEdition ? ( + <> + + + + + Ajouter un critère de diagnostic + + ) : ( + Modifier un critère de diagnostic + )} + + + + {!multiFields && ( + { + localStorage.setItem('multiple_fields', 'ok') + setMultiFields('ok') + }} + > + Tous les éléments des champs multiples sont liés par une contrainte OU + + )} + + + Diagnostic + setCurrentCriteria({ ...currentCriteria, title: e.target.value })} + /> + + setCurrentCriteria({ ...currentCriteria, isInclusive: !currentCriteria.isInclusive })} + style={{ margin: 'auto 1em' }} + component="legend" + > + Exclure les patients qui suivent les règles suivantes + + setCurrentCriteria({ ...currentCriteria, isInclusive: !e.target.checked })} + color="secondary" + /> + + + + + setCurrentCriteria({ + ...currentCriteria, + occurrence: newOccurence, + occurrenceComparator: newComparator + }) + } + withHierarchyInfo + /> + + + + setCurrentCriteria({ ...currentCriteria, source: value })} + > + } label="AREM" /> + } label="ORBIS" /> + + + + + + Les données AREM sont disponibles uniquement pour la période du 07/12/2009 au 31/12/2022. + + + Seuls les diagnostics rattachés à une visite Orbis (avec un Dossier Administratif - NDA) sont actuellement + disponibles. + + + Les données PMSI d'ORBIS sont codées au quotidien par les médecins. Les données PMSI AREM sont validées, + remontées aux tutelles et disponibles dans le SNDS. + + + + + setCurrentCriteria({ ...currentCriteria, code: value })} + placeholder="Sélectionner les codes Cim10" + /> + + option.label} + isOptionEqualToValue={(option, value) => option.id === value.id} + value={defaultValuesType} + onChange={(e, value) => setCurrentCriteria({ ...currentCriteria, diagnosticType: value })} + renderInput={(params) => } + /> + option.label} + isOptionEqualToValue={(option, value) => option.id === value.id} + value={currentCriteria.encounterStatus} + onChange={(e, value) => setCurrentCriteria({ ...currentCriteria, encounterStatus: value })} + renderInput={(params) => } + /> + setCurrentCriteria((prevValues) => ({ ...prevValues, [key]: value }))} + onError={(isError) => setError(isError ? Error.ADVANCED_INPUTS_ERROR : Error.NO_ERROR)} + /> + - { - - } - { - setSelectedTab('form')} - goBack={goBack} - /> - } - + + {!isEdition && ( + + )} + + + + ) } -export default Index +export default Cim10Form diff --git a/src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/Cim10Form/styles.ts b/src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/Cim10Form/styles.ts deleted file mode 100644 index 6772fa0b4..000000000 --- a/src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/Cim10Form/styles.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { makeStyles } from 'tss-react/mui' - -const useStyles = makeStyles()(() => ({ - tabs: { - marginTop: 72, - position: 'absolute' - } -})) - -export default useStyles diff --git a/src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/DocumentsForm/DocumentsForm.tsx b/src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/DocumentsForm/DocumentsForm.tsx index c43918b47..b4d5381dd 100644 --- a/src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/DocumentsForm/DocumentsForm.tsx +++ b/src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/DocumentsForm/DocumentsForm.tsx @@ -22,7 +22,7 @@ import { import KeyboardBackspaceIcon from '@mui/icons-material/KeyboardBackspace' -import AdvancedInputs from '../AdvancedInputs/AdvancedInputs' +import AdvancedInputs from '../AdvancedInputs' import useStyles from './styles' 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 b33c1dce8..ffa974c64 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 @@ -31,7 +31,7 @@ import { mappingCriteria } from '../DemographicForm' import { SourceType } from 'types/scope' import { Hierarchy } from 'types/hierarchy' import { CriteriaLabel } from 'components/ui/CriteriaLabel' -import ExecutiveUnitsInput from 'components/ui/Inputs/ExecutiveUnit' +import ExecutiveUnitsInput from 'components/ui/Inputs/ExecutiveUnits' enum Error { EMPTY_FORM, diff --git a/src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/GHM/components/Hierarchy/GhmHierarchy.tsx b/src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/GHM/components/Hierarchy/GhmHierarchy.tsx deleted file mode 100644 index 02e4a7dc1..000000000 --- a/src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/GHM/components/Hierarchy/GhmHierarchy.tsx +++ /dev/null @@ -1,231 +0,0 @@ -import React, { Fragment, useEffect, useState } from 'react' - -import { - Button, - Collapse, - Divider, - Grid, - IconButton, - LinearProgress, - List, - ListItem, - ListItemIcon, - ListItemText, - Skeleton, - Tooltip, - Typography -} from '@mui/material' - -import ExpandLess from '@mui/icons-material/ExpandLess' -import ExpandMore from '@mui/icons-material/ExpandMore' -import KeyboardBackspaceIcon from '@mui/icons-material/KeyboardBackspace' - -import { useAppDispatch, useAppSelector } from 'state' - -import { - checkIfIndeterminated, - expandItem, - findEquivalentRowInItemAndSubItems, - getHierarchySelection -} from 'utils/pmsi' - -import useStyles from './styles' -import { decrementLoadingSyncHierarchyTable, incrementLoadingSyncHierarchyTable } from 'state/syncHierarchyTable' -import { findSelectedInListAndSubItems } from 'utils/cohortCreation' -import { defaultClaim } from '../../index' -import { HierarchyTree } from 'types' -import { GhmDataType } from 'types/requestCriterias' -import { Hierarchy } from 'types/hierarchy' - -type GhmListItemProps = { - ghmItem: Hierarchy - selectedItems?: Hierarchy[] | null - handleClick: (ghmItem: Hierarchy[] | null | undefined, newHierarchy?: Hierarchy[]) => void -} - -const GhmListItem: React.FC = (props) => { - const { ghmItem, selectedItems, handleClick } = props - const { id, label, subItems } = ghmItem - - const { classes, cx } = useStyles() - const dispatch = useAppDispatch() - - const ghmHierarchy = useAppSelector((state) => state.pmsi.claim.list || {}) - const isLoadingsyncHierarchyTable = useAppSelector((state) => state.syncHierarchyTable.loading ?? 0) - const isLoadingPmsi = useAppSelector((state) => state.pmsi.syncLoading ?? 0) - - const [open, setOpen] = useState(false) - const isSelected = findSelectedInListAndSubItems(selectedItems ?? [], ghmItem, ghmHierarchy) - const isIndeterminated = checkIfIndeterminated(ghmItem, selectedItems) - const _onExpand = async (ghmCode: string) => { - if (isLoadingsyncHierarchyTable > 0 || isLoadingPmsi > 0) return - dispatch(incrementLoadingSyncHierarchyTable()) - setOpen(!open) - const newHierarchy = await expandItem(ghmCode, selectedItems || [], ghmHierarchy, defaultClaim.type, dispatch) - handleClick(selectedItems, newHierarchy) - dispatch(decrementLoadingSyncHierarchyTable()) - } - - const handleClickOnHierarchy = async (ghmItem: Hierarchy) => { - if (isLoadingsyncHierarchyTable > 0 || isLoadingPmsi > 0) return - dispatch(incrementLoadingSyncHierarchyTable()) - const newSelectedItems = getHierarchySelection(ghmItem, selectedItems || [], ghmHierarchy) - handleClick(newSelectedItems) - dispatch(decrementLoadingSyncHierarchyTable()) - } - - if (!subItems || (subItems && Array.isArray(subItems) && subItems.length === 0)) { - return ( - - - - )} - - - - ) : ( - <> - ) -} - -export default GhmHierarchy diff --git a/src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/GHM/components/Hierarchy/styles.ts b/src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/GHM/components/Hierarchy/styles.ts deleted file mode 100644 index d473b65da..000000000 --- a/src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/GHM/components/Hierarchy/styles.ts +++ /dev/null @@ -1,102 +0,0 @@ -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' }, - ghmHierarchyActionContainer: { - display: 'flex', - alignItems: 'center', - justifyContent: 'center', - flexWrap: 'wrap', - borderTop: '1px solid grey', - position: 'absolute', - width: '100%', - bottom: 0, - left: 0, - '& > button': { - margin: '12px 8px' - } - }, - ghmItem: { - padding: '2px 16px' - }, - label: { - '& > span': { - textOverflow: 'ellipsis', - overflow: 'hidden', - whiteSpace: 'nowrap', - cursor: 'pointer' - } - }, - indicator: { - width: 20, - height: 20, - border: '2px solid currentColor', - borderRadius: 10, - backgroundColor: '#fff' - }, - loader: { - height: '4px', - marginTop: '2px' - }, - selectedIndicator: { - width: 20, - height: 20, - backgroundColor: 'currentColor', - boxShadow: 'inset 0 0 0 2px white', - border: '2px solid currentColor', - borderRadius: 10 - }, - indeterminateIndicator: { - color: '#555 !important', - width: 20, - height: 20, - backgroundColor: 'currentColor', - boxShadow: 'inset 0 0 0 4px white', - border: '2px solid currentColor', - borderRadius: 10 - }, - drawerContentContainer: { - height: 'calc(100vh - 207px - 4px)', - overflow: 'auto', - margin: 12 - }, - subItemsContainer: { - position: 'relative', - marginLeft: 25 - }, - subItemsContainerIndicator: { - content: '""', - position: 'absolute', - width: 2, - height: 'calc(100% + -10px)', - bottom: 15, - background: '#D0D7D8' - }, - subItemsIndicator: { - content: '""', - position: 'absolute', - width: 17, - height: 2, - marginTop: 14.5, - background: '#D0D7D8' - } -})) - -export default useStyles diff --git a/src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/GHM/index.tsx b/src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/GHM/index.tsx deleted file mode 100644 index 88af99817..000000000 --- a/src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/GHM/index.tsx +++ /dev/null @@ -1,102 +0,0 @@ -import React, { useEffect, useState } from 'react' -import { Tab, Tabs } from '@mui/material' - -import GHMForm from './components/Form/GhmForm' -import GHMHierarchy from './components/Hierarchy/GhmHierarchy' - -import useStyles from './styles' -import { useAppDispatch, useAppSelector } from 'state' -import { initSyncHierarchyTableEffect, syncOnChangeFormValue } from 'utils/pmsi' -import { fetchClaim } from 'state/pmsi' -import { EXPLORATION } from '../../../../../../../../constants' -import { CriteriaDrawerComponentProps } from 'types' -import { Comparators, GhmDataType, CriteriaType } from 'types/requestCriterias' -import { Hierarchy } from 'types/hierarchy' - -export const defaultClaim: Omit = { - type: CriteriaType.CLAIM, - title: 'Critères GHM', - code: [], - label: undefined, - occurrence: 1, - occurrenceComparator: Comparators.GREATER_OR_EQUAL, - startOccurrence: [null, null], - isInclusive: true, - encounterStartDate: [null, null], - encounterEndDate: [null, null], - encounterStatus: [] -} - -const Index = (props: CriteriaDrawerComponentProps) => { - const { criteriaData, selectedCriteria, onChangeSelectedCriteria, goBack } = props - const [selectedTab, setSelectedTab] = useState<'form' | 'hierarchy'>(selectedCriteria ? 'form' : 'hierarchy') - const [defaultCriteria, setDefaultCriteria] = useState((selectedCriteria as GhmDataType) || defaultClaim) - - const isEdition = selectedCriteria !== null - const dispatch = useAppDispatch() - const ghmState = useAppSelector((state) => state.pmsi.claim || {}) - const ghmHierarchy = ghmState.list - - const { classes } = useStyles() - - const _onChangeSelectedHierarchy = ( - newSelectedItems: Hierarchy[] | null | undefined, - newHierarchy?: Hierarchy[] - ) => { - _onChangeFormValue('code', newSelectedItems, newHierarchy) - } - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const _onChangeFormValue = async (key: string, value: any, newHierarchy: Hierarchy[] = ghmHierarchy) => - await syncOnChangeFormValue(key, value, newHierarchy, setDefaultCriteria, selectedTab, defaultClaim.type, dispatch) - - const _initSyncHierarchyTableEffect = async () => { - await initSyncHierarchyTableEffect( - ghmHierarchy, - selectedCriteria, - defaultCriteria && defaultCriteria.code ? defaultCriteria.code : [], - fetchClaim, - defaultClaim.type, - dispatch - ) - } - useEffect(() => { - _initSyncHierarchyTableEffect() - }, []) - - return ( - <> - setSelectedTab(tab)} - > - - - - - { - - } - { - setSelectedTab('form')} - goBack={goBack} - /> - } - - ) -} -export default Index diff --git a/src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/GHM/styles.ts b/src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/GHM/styles.ts deleted file mode 100644 index 6772fa0b4..000000000 --- a/src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/GHM/styles.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { makeStyles } from 'tss-react/mui' - -const useStyles = makeStyles()(() => ({ - tabs: { - marginTop: 72, - position: 'absolute' - } -})) - -export default useStyles diff --git a/src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/GHM/components/Form/GhmForm.tsx b/src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/GHMForm/index.tsx similarity index 54% rename from src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/GHM/components/Form/GhmForm.tsx rename to src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/GHMForm/index.tsx index 247d8e2f4..306afa96b 100644 --- a/src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/GHM/components/Form/GhmForm.tsx +++ b/src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/GHMForm/index.tsx @@ -1,5 +1,4 @@ -import React, { useState } from 'react' - +import React, { useMemo, useState } from 'react' import { Alert, Autocomplete, @@ -14,70 +13,51 @@ import { Typography } from '@mui/material' +import useStyles from '../formStyles' +import { CriteriaDrawerComponentProps } from 'types' +import { Comparators, GhmDataType, CriteriaType } from 'types/requestCriterias' import KeyboardBackspaceIcon from '@mui/icons-material/KeyboardBackspace' - -import useStyles from './styles' -import { useAppDispatch, useAppSelector } from 'state' -import { fetchClaim } from 'state/pmsi' -import { CriteriaItemDataCache, HierarchyTree } from 'types' -import AdvancedInputs from '../../../AdvancedInputs/AdvancedInputs' -import AsyncAutocomplete from 'components/ui/Inputs/AsyncAutocomplete' -import services from 'services/aphp' -import { Comparators, GhmDataType, SelectedCriteriaType } from 'types/requestCriterias' import { BlockWrapper } from 'components/ui/Layout' import OccurenceInput from 'components/ui/Inputs/Occurences' +import AdvancedInputs from '../AdvancedInputs' import { SourceType } from 'types/scope' -import { Hierarchy } from 'types/hierarchy' - -type GHMFormProps = { - isOpen: boolean - isEdition: boolean - criteriaData: CriteriaItemDataCache - selectedCriteria: GhmDataType - // eslint-disable-next-line @typescript-eslint/no-explicit-any - onChangeValue: (key: string, value: any) => void - goBack: () => void - onChangeSelectedCriteria: (data: SelectedCriteriaType) => void -} +import ValueSetField from 'components/SearchValueSet/ValueSetField' +import { getConfig } from 'config' +import { getValueSetsFromSystems } from 'utils/valueSets' enum Error { ADVANCED_INPUTS_ERROR, NO_ERROR } -const GhmForm: React.FC = (props) => { - const { isOpen, isEdition, criteriaData, selectedCriteria, onChangeValue, onChangeSelectedCriteria, goBack } = props +export const defaultClaim: Omit = { + type: CriteriaType.CLAIM, + title: 'Critères GHM', + code: [], + label: undefined, + occurrence: 1, + occurrenceComparator: Comparators.GREATER_OR_EQUAL, + startOccurrence: [null, null], + isInclusive: true, + encounterStartDate: [null, null], + encounterEndDate: [null, null], + encounterStatus: [] +} + +const GhmForm = (props: CriteriaDrawerComponentProps) => { + const { criteriaData, selectedCriteria, onChangeSelectedCriteria, goBack } = props + const [currentCriteria, setCurrentCriteria] = useState((selectedCriteria as GhmDataType) || defaultClaim) - const { classes } = useStyles() - const dispatch = useAppDispatch() - const initialState: HierarchyTree | null = useAppSelector((state) => state.syncHierarchyTable) - const currentState = { ...selectedCriteria, ...initialState } + const isEdition = selectedCriteria !== null const [multiFields, setMultiFields] = useState(localStorage.getItem('multiple_fields')) - const [occurrence, setOccurrence] = useState(currentState.occurrence || 1) - const [occurrenceComparator, setOccurrenceComparator] = useState( - currentState.occurrenceComparator || Comparators.GREATER_OR_EQUAL - ) const [error, setError] = useState(Error.NO_ERROR) + const { classes } = useStyles() - const getGhmOptions = async (searchValue: string, signal: AbortSignal) => - await services.cohortCreation.fetchGhmData(searchValue, false, signal) - const _onSubmit = () => { - onChangeSelectedCriteria({ ...currentState, occurrence: occurrence, occurrenceComparator: occurrenceComparator }) - dispatch(fetchClaim()) - } - const defaultValuesCode = currentState.code - ? currentState.code.map((code) => { - const criteriaCode = criteriaData.data.ghmData - ? criteriaData.data.ghmData.find((g: Hierarchy) => g.id === code.id) - : null - return { - id: code.id, - label: code.label ? code.label : criteriaCode?.label ?? '?' - } - }) - : [] - - return isOpen ? ( + const ghmReferences = useMemo(() => { + return getValueSetsFromSystems([getConfig().features.claim.valueSets.claimHierarchy.url]) + }, []) + + return ( {!isEdition ? ( @@ -127,13 +107,13 @@ const GhmForm: React.FC = (props) => { id="criteria-name-required" placeholder="Nom du critère" variant="outlined" - value={currentState.title} - onChange={(e) => onChangeValue('title', e.target.value)} + value={currentCriteria.title} + onChange={(e) => setCurrentCriteria({ ...currentCriteria, title: e.target.value })} /> onChangeValue('isInclusive', !currentState.isInclusive)} + onClick={() => setCurrentCriteria({ ...currentCriteria, isInclusive: !currentCriteria.isInclusive })} style={{ margin: 'auto 1em' }} component="legend" > @@ -141,35 +121,35 @@ const GhmForm: React.FC = (props) => { onChangeValue('isInclusive', !event.target.checked)} + checked={!currentCriteria.isInclusive} + onChange={(e) => setCurrentCriteria({ ...currentCriteria, isInclusive: !e.target.checked })} color="secondary" /> { - setOccurrence(newOccurence) - setOccurrenceComparator(newComparator) - }} + value={currentCriteria.occurrence || 1} + comparator={currentCriteria.occurrenceComparator || Comparators.GREATER_OR_EQUAL} + onchange={(newOccurence, newComparator) => + setCurrentCriteria({ + ...currentCriteria, + occurrence: newOccurence, + occurrenceComparator: newComparator + }) + } withHierarchyInfo /> - { - onChangeValue('code', value) - }} - /> + + setCurrentCriteria({ ...currentCriteria, code: value })} + placeholder="Sélectionner les codes GHM" + /> + = (props) => { noOptionsText="Veuillez entrer un statut de visite associée" getOptionLabel={(option) => option.label} isOptionEqualToValue={(option, value) => option.id === value.id} - value={currentState.encounterStatus} - onChange={(e, value) => onChangeValue('encounterStatus', value)} + value={currentCriteria.encounterStatus} + onChange={(e, value) => setCurrentCriteria({ ...currentCriteria, encounterStatus: value })} renderInput={(params) => } /> setCurrentCriteria((prevValues) => ({ ...prevValues, [key]: value }))} onError={(isError) => setError(isError ? Error.ADVANCED_INPUTS_ERROR : Error.NO_ERROR)} /> @@ -198,7 +178,7 @@ const GhmForm: React.FC = (props) => { )} - )} - - - - - ) : ( - <> - ) -} - -export default MedicationForm diff --git a/src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/MedicationForm/components/Form/styles.ts b/src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/MedicationForm/components/Form/styles.ts deleted file mode 100644 index d26f1b494..000000000 --- a/src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/MedicationForm/components/Form/styles.ts +++ /dev/null @@ -1,53 +0,0 @@ -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/MedicationForm/components/Hierarchy/MedicationHierarchy.tsx b/src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/MedicationForm/components/Hierarchy/MedicationHierarchy.tsx deleted file mode 100644 index cc2d793aa..000000000 --- a/src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/MedicationForm/components/Hierarchy/MedicationHierarchy.tsx +++ /dev/null @@ -1,323 +0,0 @@ -import React, { Fragment, useEffect, useState } from 'react' -import InfiniteScroll from 'react-infinite-scroll-component' - -import { - Button, - Collapse, - Divider, - Grid, - IconButton, - LinearProgress, - List, - ListItem, - ListItemIcon, - ListItemText, - Skeleton, - Tooltip, - Typography, - Select, - MenuItem -} from '@mui/material' - -import ExpandLess from '@mui/icons-material/ExpandLess' -import ExpandMore from '@mui/icons-material/ExpandMore' -import KeyboardBackspaceIcon from '@mui/icons-material/KeyboardBackspace' - -import { useAppDispatch, useAppSelector } from 'state' - -import { - checkIfIndeterminated, - expandItem, - findEquivalentRowInItemAndSubItems, - getHierarchySelection -} from 'utils/pmsi' - -import useStyles from './styles' -import { findSelectedInListAndSubItems } from 'utils/cohortCreation' -import { decrementLoadingSyncHierarchyTable, incrementLoadingSyncHierarchyTable } from 'state/syncHierarchyTable' -import { defaultMedication } from '../../index' -import { HierarchyTree } from 'types' -import { MedicationDataType } from 'types/requestCriterias' -import { Hierarchy } from 'types/hierarchy' - -type MedicationListItemProps = { - medicationItem: Hierarchy - selectedItems?: Hierarchy[] | null - handleClick: (medicationItem: Hierarchy[] | null | undefined, newHierarchy?: Hierarchy[]) => void - setLoading: (isLoading: boolean) => void - valueSetSystem?: 'ATC' | 'UCD' -} - -const MedicationListItem: React.FC = (props) => { - const { medicationItem, selectedItems, handleClick, setLoading, valueSetSystem } = props - const { id, label, subItems } = medicationItem - - const { classes, cx } = useStyles() - const dispatch = useAppDispatch() - - const medicationHierarchy = useAppSelector((state) => state.medication.list || {}) - const isLoadingsyncHierarchyTable = useAppSelector((state) => state.syncHierarchyTable.loading ?? 0) - const isLoadingMedication = useAppSelector((state) => state.medication.syncLoading ?? 0) - - const [open, setOpen] = useState(false) - const isSelected = findSelectedInListAndSubItems( - selectedItems ?? [], - medicationItem, - medicationHierarchy, - valueSetSystem - ) - const isIndeterminated = checkIfIndeterminated(medicationItem, selectedItems) - - const _onExpand = async (medicationCode: string) => { - if (isLoadingsyncHierarchyTable > 0 || isLoadingMedication > 0) return - dispatch(incrementLoadingSyncHierarchyTable()) - setOpen(!open) - const newHierarchy = await expandItem( - medicationCode, - selectedItems || [], - medicationHierarchy, - defaultMedication.type, - dispatch - ) - handleClick(selectedItems, newHierarchy) - dispatch(decrementLoadingSyncHierarchyTable()) - } - - const handleClickOnHierarchy = (medicationItem: Hierarchy) => { - if (isLoadingsyncHierarchyTable > 0 || isLoadingMedication > 0) return - dispatch(incrementLoadingSyncHierarchyTable()) - const newSelectedItems = getHierarchySelection(medicationItem, selectedItems || [], medicationHierarchy) - handleClick(newSelectedItems) - dispatch(decrementLoadingSyncHierarchyTable()) - } - - if (!subItems || (subItems && Array.isArray(subItems) && subItems.length === 0)) { - return ( - - - - )} - - - - ) : ( - <> - ) -} - -export default MedicationExploration diff --git a/src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/MedicationForm/components/Hierarchy/styles.ts b/src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/MedicationForm/components/Hierarchy/styles.ts deleted file mode 100644 index 02c36a7e6..000000000 --- a/src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/MedicationForm/components/Hierarchy/styles.ts +++ /dev/null @@ -1,116 +0,0 @@ -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' }, - medicationHierarchyActionContainer: { - display: 'flex', - alignItems: 'center', - justifyContent: 'center', - flexWrap: 'wrap', - borderTop: '1px solid grey', - backgroundColor: 'white', - position: 'absolute', - width: '100%', - bottom: 0, - left: 0, - '& > button': { - margin: '12px 8px' - } - }, - medicationItem: { - padding: '2px 16px' - }, - label: { - '& > span': { - textOverflow: 'ellipsis', - overflow: 'hidden', - whiteSpace: 'nowrap', - cursor: 'pointer' - } - }, - indicator: { - width: 20, - height: 20, - border: '2px solid currentColor', - borderRadius: 10, - backgroundColor: '#fff' - }, - loader: { - height: '4px', - marginTop: '2px' - }, - selectedIndicator: { - width: 20, - height: 20, - backgroundColor: 'currentColor', - boxShadow: 'inset 0 0 0 2px white', - border: '2px solid currentColor', - borderRadius: 10 - }, - indeterminateIndicator: { - color: '#555 !important', - width: 20, - height: 20, - backgroundColor: 'currentColor', - boxShadow: 'inset 0 0 0 4px white', - border: '2px solid currentColor', - borderRadius: 10 - }, - drawerContentContainer: { - maxHeight: 'calc(100vh - 250px)', - overflow: 'auto', - margin: 12 - }, - subItemsContainer: { - position: 'relative', - marginLeft: 25 - }, - subItemsContainerIndicator: { - content: '""', - position: 'absolute', - width: 2, - height: 'calc(100% + -10px)', - bottom: 15, - background: '#D0D7D8' - }, - subItemsIndicator: { - content: '""', - position: 'absolute', - width: 17, - height: 2, - marginTop: 14.5, - background: '#D0D7D8' - }, - referentielContainer: { - display: 'flex', - alignItems: 'center', - justifyContent: 'center' - }, - select: { - marginLeft: 8, - borderRadius: 25, - backgroundColor: '#FFF', - '& .MuiSelect-select': { - borderRadius: 25 - } - } -})) - -export default useStyles diff --git a/src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/MedicationForm/index.tsx b/src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/MedicationForm/index.tsx index dee1c6f4e..d9838d4a5 100644 --- a/src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/MedicationForm/index.tsx +++ b/src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/MedicationForm/index.tsx @@ -1,18 +1,37 @@ -import React, { useState, useEffect } from 'react' -import { Tabs, Tab } from '@mui/material' - -import MedicationForm from './components/Form/MedicationForm' -import MedicationExploration from './components/Hierarchy/MedicationHierarchy' +import React, { useEffect, useMemo, useState } from 'react' import { CriteriaDrawerComponentProps } from 'types' - -import useStyles from './styles' -import { useAppDispatch, useAppSelector } from 'state' -import { initSyncHierarchyTableEffect, syncOnChangeFormValue } from 'utils/pmsi' -import { fetchMedication } from 'state/medication' -import { EXPLORATION } from '../../../../../../../../constants' import { Comparators, MedicationDataType, CriteriaType } from 'types/requestCriterias' -import { Hierarchy } from 'types/hierarchy' +import { HierarchyWithLabelAndSystem } from 'types/hierarchy' +import { + Grid, + IconButton, + Divider, + Typography, + Alert, + TextField, + FormLabel, + RadioGroup, + FormControlLabel, + Radio, + Autocomplete, + Button, + Switch +} from '@mui/material' +import { getConfig } from 'config' +import ValueSetField from 'components/SearchValueSet/ValueSetField' +import OccurenceInput from 'components/ui/Inputs/Occurences' +import { BlockWrapper } from 'components/ui/Layout' +import useStyles from '../formStyles' +import { SourceType } from 'types/scope' +import AdvancedInputs from '../AdvancedInputs' +import KeyboardBackspaceIcon from '@mui/icons-material/KeyboardBackspace' +import { getValueSetsFromSystems } from 'utils/valueSets' + +enum Error { + ADVANCED_INPUTS_ERROR, + NO_ERROR +} export const defaultMedication: Omit = { type: CriteriaType.MEDICATION_REQUEST, @@ -29,77 +48,216 @@ export const defaultMedication: Omit = { encounterStatus: [] } -const Index = (props: CriteriaDrawerComponentProps) => { +const MedicationForm = (props: CriteriaDrawerComponentProps) => { const { criteriaData, selectedCriteria, onChangeSelectedCriteria, goBack } = props - const [selectedTab, setSelectedTab] = useState<'form' | 'exploration'>(selectedCriteria ? 'form' : 'exploration') - const [defaultCriteria, setDefaultCriteria] = useState( - (selectedCriteria as MedicationDataType) || defaultMedication - ) - const isEdition = selectedCriteria !== null - const dispatch = useAppDispatch() - const medicationHierarchy = useAppSelector((state) => state.medication.list || {}) - const { classes } = useStyles() + const [currentCriteria, setCurrentCriteria] = useState((selectedCriteria as MedicationDataType) || defaultMedication) + const [multiFields, setMultiFields] = useState(localStorage.getItem('multiple_fields')) + const [error, setError] = useState(Error.NO_ERROR) - const _onChangeSelectedHierarchy = ( - newSelectedItems: Hierarchy[] | null | undefined, - newHierarchy?: Hierarchy[] - ) => { - _onChangeFormValue('code', newSelectedItems, newHierarchy) - } - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const _onChangeFormValue = (key: string, value: any, hierarchy: Hierarchy[] = medicationHierarchy) => - syncOnChangeFormValue(key, value, hierarchy, setDefaultCriteria, selectedTab, defaultMedication.type, dispatch) + useEffect(() => { + if (currentCriteria.type === CriteriaType.MEDICATION_ADMINISTRATION) { + setCurrentCriteria({ ...currentCriteria, endOccurrence: [null, null] }) + } + }, [currentCriteria.type]) - const _initSyncHierarchyTableEffect = async () => { - await initSyncHierarchyTableEffect( - medicationHierarchy, - selectedCriteria, - defaultCriteria && defaultCriteria.code ? defaultCriteria.code : [], - fetchMedication, - defaultMedication.type, - dispatch - ) + if (criteriaData?.data?.prescriptionTypes === 'loading' || criteriaData?.data?.administrations === 'loading') { + return <> } - useEffect(() => { - _initSyncHierarchyTableEffect() + + const selectedCriteriaPrescriptionType = + currentCriteria.type === CriteriaType.MEDICATION_REQUEST && currentCriteria.prescriptionType + ? currentCriteria.prescriptionType.map((prescriptionType) => { + const criteriaPrescriptionType = criteriaData.data.prescriptionTypes + ? criteriaData.data.prescriptionTypes.find((p: HierarchyWithLabelAndSystem) => p.id === prescriptionType.id) + : null + return { + id: prescriptionType.id, + label: prescriptionType.label ? prescriptionType.label : criteriaPrescriptionType?.label ?? '?' + } + }) + : [] + + const selectedCriteriaAdministration = currentCriteria.administration + ? currentCriteria.administration.map((administration) => { + const criteriaAdministration = criteriaData.data.administrations + ? criteriaData.data.administrations.find((p: HierarchyWithLabelAndSystem) => p.id === administration.id) + : null + return { + id: administration.id, + label: administration.label ? administration.label : criteriaAdministration?.label ?? '?' + } + }) + : [] + + const medicationReferences = useMemo(() => { + return getValueSetsFromSystems([ + getConfig().features.medication.valueSets.medicationAtc.url, + getConfig().features.medication.valueSets.medicationUcd.url + ]) }, []) return ( - <> - setSelectedTab(tab)} - > - - - + + + {!isEdition ? ( + <> + + + + + Ajouter un critère de médicament + + ) : ( + Modifier un critère de médicament + )} + + + + {!multiFields && ( + { + localStorage.setItem('multiple_fields', 'ok') + setMultiFields('ok') + }} + > + Tous les éléments des champs multiples sont liés par une contrainte OU + + )} + + + Médicaments + setCurrentCriteria({ ...currentCriteria, title: e.target.value })} + /> + + setCurrentCriteria({ ...currentCriteria, isInclusive: !currentCriteria.isInclusive })} + style={{ margin: 'auto 1em' }} + component="legend" + > + Exclure les patients qui suivent les règles suivantes + + setCurrentCriteria({ ...currentCriteria, isInclusive: !e.target.checked })} + color="secondary" + /> + + + + setCurrentCriteria({ + ...currentCriteria, + occurrence: newOccurence, + occurrenceComparator: newComparator + }) + } + withHierarchyInfo + /> + + + setCurrentCriteria({ ...currentCriteria, type: value as CriteriaType.MEDICATION_ADMINISTRATION }) + } + > + } label="Prescription" /> + } + label="Administration" + /> + + + + setCurrentCriteria({ ...currentCriteria, code: value })} + placeholder="Sélectionner les codes" + /> + + + {currentCriteria.type === CriteriaType.MEDICATION_REQUEST && ( + option.label} + isOptionEqualToValue={(option, value) => option.id === value.id} + value={selectedCriteriaPrescriptionType} + onChange={(e, value) => setCurrentCriteria({ ...currentCriteria, prescriptionType: value })} + renderInput={(params) => } + /> + )} + {currentCriteria.type === CriteriaType.MEDICATION_ADMINISTRATION && ( + option.label} + isOptionEqualToValue={(option, value) => option.id === value.id} + value={selectedCriteriaAdministration} + onChange={(e, value) => setCurrentCriteria({ ...currentCriteria, administration: value })} + renderInput={(params) => } + /> + )} + option.label} + isOptionEqualToValue={(option, value) => option.id === value.id} + value={currentCriteria.encounterStatus} + onChange={(e, value) => setCurrentCriteria({ ...currentCriteria, encounterStatus: value })} + renderInput={(params) => } + /> + setCurrentCriteria((prevValues) => ({ ...prevValues, [key]: value }))} + onError={(isError) => setError(isError ? Error.ADVANCED_INPUTS_ERROR : Error.NO_ERROR)} + /> + - { - - } - { - setSelectedTab('form')} - goBack={goBack} - /> - } - + + {!isEdition && ( + + )} + + + + ) } -export default Index +export default MedicationForm diff --git a/src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/MedicationForm/styles.ts b/src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/MedicationForm/styles.ts deleted file mode 100644 index 6772fa0b4..000000000 --- a/src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/MedicationForm/styles.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { makeStyles } from 'tss-react/mui' - -const useStyles = makeStyles()(() => ({ - tabs: { - marginTop: 72, - position: 'absolute' - } -})) - -export default useStyles diff --git a/src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/PregnantForm/index.tsx b/src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/PregnantForm/index.tsx index 3dc7b88de..feaf690e9 100644 --- a/src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/PregnantForm/index.tsx +++ b/src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/PregnantForm/index.tsx @@ -23,7 +23,7 @@ import CriteriaLayout from 'components/ui/CriteriaLayout' import { SourceType } from 'types/scope' import { Hierarchy } from 'types/hierarchy' import { CriteriaLabel } from 'components/ui/CriteriaLabel' -import ExecutiveUnitsInput from 'components/ui/Inputs/ExecutiveUnit' +import ExecutiveUnitsInput from 'components/ui/Inputs/ExecutiveUnits' enum Error { EMPTY_FORM, diff --git a/src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/GHM/components/Form/styles.ts b/src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/formStyles.ts similarity index 94% rename from src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/GHM/components/Form/styles.ts rename to src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/formStyles.ts index 22ddd251b..0ba12810d 100644 --- a/src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/GHM/components/Form/styles.ts +++ b/src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/formStyles.ts @@ -12,9 +12,7 @@ const useStyles = makeStyles()(() => ({ height: 72, padding: 20, backgroundColor: '#317EAA', - color: 'white', - //not default - marginBottom: 46 + color: 'white' }, backButton: { color: 'white' }, divider: { background: 'white' }, diff --git a/src/components/CreationCohort/DiagramView/components/LogicalOperator/LogicalOperator.tsx b/src/components/CreationCohort/DiagramView/components/LogicalOperator/index.tsx similarity index 100% rename from src/components/CreationCohort/DiagramView/components/LogicalOperator/LogicalOperator.tsx rename to src/components/CreationCohort/DiagramView/components/LogicalOperator/index.tsx diff --git a/src/components/CreationCohort/DiagramView/components/PopulationCard/PopulationCard.tsx b/src/components/CreationCohort/DiagramView/components/PopulationCard/PopulationCard.tsx index fac586964..756abd22f 100644 --- a/src/components/CreationCohort/DiagramView/components/PopulationCard/PopulationCard.tsx +++ b/src/components/CreationCohort/DiagramView/components/PopulationCard/PopulationCard.tsx @@ -47,7 +47,7 @@ const PopulationCard = ({ label, onEditDisabled, population, loading, onEdit }: {population .slice(0, isExtended ? population.length : 4) - .map((pop, index: number) => + .map((pop) => pop.id !== Rights.EXPIRED ? ( [] - selectedPopulation: Hierarchy[] - sourceType: SourceType - onConfirm: (selectedPopulation: Hierarchy[]) => void - onClose: () => void -} - -const PopulationRightPanel = ({ - open, - title, - population, - selectedPopulation, - sourceType, - mandatory = false, - onConfirm, - onClose -}: PopulationRightPanelProps) => { - const [selectedCodes, setSelectedCodes] = useState[]>([]) - - return ( - - - - - - {title ?? 'Structure hospitalière'} - - - - - - - - - - - - - ) -} - -export default PopulationRightPanel diff --git a/src/components/CreationCohort/DiagramView/DiagramView.tsx b/src/components/CreationCohort/DiagramView/index.tsx similarity index 88% rename from src/components/CreationCohort/DiagramView/DiagramView.tsx rename to src/components/CreationCohort/DiagramView/index.tsx index c60c46061..503767753 100644 --- a/src/components/CreationCohort/DiagramView/DiagramView.tsx +++ b/src/components/CreationCohort/DiagramView/index.tsx @@ -1,7 +1,7 @@ import React, { useEffect, useState } from 'react' import { Alert, Button, Grid } from '@mui/material' -import LogicalOperator from './components/LogicalOperator/LogicalOperator' +import LogicalOperator from './components/LogicalOperator' import TemporalConstraintCard from './components/TemporalConstraintCard/TemporalConstraintCard' import CohortCreationBreadcrumbs from './components/Breadcrumbs/Breadcrumbs' import { useAppDispatch, useAppSelector } from 'state' @@ -9,10 +9,11 @@ import { Rights, SourceType } from 'types/scope' import { buildCohortCreation } from 'state/cohortCreation' import { ScopeElement } from 'types' import { Hierarchy } from 'types/hierarchy' -import PopulationRightPanel from './components/PopulationCard/components/PopulationRightPanel' +import Panel from '../../ui/Panel' import PopulationCard from './components/PopulationCard/PopulationCard' import ModalRightError from './components/PopulationCard/components/ModalRightError' import { checkNominativeCriteria, cleanNominativeCriterias } from 'utils/cohortCreation' +import ScopeTree from 'components/ScopeTree' const DiagramView = () => { const dispatch = useAppDispatch() @@ -45,9 +46,8 @@ const DiagramView = () => { }, [selectedPopulation]) useEffect(() => { - if (!selectedPopulation?.length && rights.length === 1) { + if (!selectedPopulation?.length && rights.length === 1) dispatch(buildCohortCreation({ selectedPopulation: rights })) - } }, [selectedPopulation, rights]) return ( @@ -92,15 +92,20 @@ const DiagramView = () => { {selectedPopulation && selectedPopulation.length > 0 ? : <>}
- handleChangePopulation(selectedCodes)} onClose={() => setOpenDrawer(false)} - /> + > + + setOpenDrawer(true)} /> ) diff --git a/src/components/CreationCohort/Requeteur.tsx b/src/components/CreationCohort/Requeteur.tsx index c1257e34c..b8c3614df 100644 --- a/src/components/CreationCohort/Requeteur.tsx +++ b/src/components/CreationCohort/Requeteur.tsx @@ -5,7 +5,7 @@ import { useNavigate, useParams } from 'react-router-dom' import Grid from '@mui/material/Grid' import ControlPanel from './ControlPanel/ControlPanel' -import DiagramView from './DiagramView/DiagramView' +import DiagramView from './DiagramView' import ModalCreateNewRequest from './Modals/ModalCreateNewRequest/ModalCreateNewRequest' import { useAppDispatch, useAppSelector } from 'state' @@ -82,7 +82,6 @@ const Requeteur = () => { } try { const criteriaCache = await getDataFromFetch(criteriaList(), selectedCriteria, criteriaData.cache) - const allowMaternityForms = selectedPopulation?.every((population) => population?.access === 'Nominatif') const questionnairesEnabled = config.features.questionnaires.enabled dispatch( diff --git a/src/components/Dashboard/BiologyList/index.tsx b/src/components/Dashboard/BiologyList/index.tsx index 5e73af3ed..0197e9e18 100644 --- a/src/components/Dashboard/BiologyList/index.tsx +++ b/src/components/Dashboard/BiologyList/index.tsx @@ -28,15 +28,13 @@ import useSearchCriterias, { initBioSearchCriterias } from 'reducers/searchCrite import { cancelPendingRequest } from 'utils/abortController' import { selectFiltersAsArray } from 'utils/filters' import DataTableObservation from 'components/DataTable/DataTableObservation' -import AnabioFilter from 'components/Filters/AnabioFilter' -import LoincFilter from 'components/Filters/LoincFilter' import { useSearchParams } from 'react-router-dom' import { SourceType } from 'types/scope' -import { - fetchLoincCodes as fetchLoincCodesApi, - fetchAnabioCodes as fetchAnabioCodesApi -} from 'services/aphp/serviceBiology' import { checkIfPageAvailable, handlePageError } from 'utils/paginationUtils' +import { getCodeList } from 'services/aphp/serviceValueSets' +import { getConfig } from 'config' +import CodeFilter from 'components/Filters/CodeFilter' +import { getValueSetsFromSystems } from 'utils/valueSets' type BiologyListProps = { groupId?: string @@ -78,24 +76,23 @@ const BiologyList = ({ groupId, deidentified }: BiologyListProps) => { orderBy, searchInput, filters, - filters: { validatedStatus, nda, ipp, loinc, anabio, startDate, endDate, executiveUnits, encounterStatus } + filters: { code, validatedStatus, nda, ipp, startDate, endDate, executiveUnits, encounterStatus } }, { changeOrderBy, changeSearchInput, addFilters, removeFilter, addSearchCriterias } ] = useSearchCriterias(initBioSearchCriterias) const filtersAsArray = useMemo( () => selectFiltersAsArray({ + code, validatedStatus, nda, ipp, - loinc, - anabio, startDate, endDate, executiveUnits, encounterStatus }), - [validatedStatus, nda, ipp, loinc, anabio, startDate, endDate, executiveUnits, encounterStatus] + [validatedStatus, code, nda, ipp, startDate, endDate, executiveUnits, encounterStatus] ) const [loadingStatus, setLoadingStatus] = useState(LoadingStatus.FETCHING) @@ -122,7 +119,7 @@ const BiologyList = ({ groupId, deidentified }: BiologyListProps) => { searchCriterias: { orderBy, searchInput, - filters: { validatedStatus, nda, ipp, loinc, anabio, startDate, endDate, executiveUnits, encounterStatus } + filters: { validatedStatus, nda, ipp, code, startDate, endDate, executiveUnits, encounterStatus } } }, groupId, @@ -170,7 +167,7 @@ const BiologyList = ({ groupId, deidentified }: BiologyListProps) => { useEffect(() => { const fetch = async () => { try { - const encounterStatus = await services.cohortCreation.fetchEncounterStatus() + const encounterStatus = (await getCodeList(getConfig().core.valueSets.encounterStatus.url)).results setEncounterStatusList(encounterStatus) } catch (e) { /* empty */ @@ -193,8 +190,7 @@ const BiologyList = ({ groupId, deidentified }: BiologyListProps) => { validatedStatus, nda, ipp, - loinc, - anabio, + code, startDate, endDate, executiveUnits, @@ -214,6 +210,13 @@ const BiologyList = ({ groupId, deidentified }: BiologyListProps) => { } }, [loadingStatus]) + const references = useMemo(() => { + return getValueSetsFromSystems([ + getConfig().features.observation.valueSets.biologyHierarchyAnabio.url, + getConfig().features.observation.valueSets.biologyHierarchyLoinc.url + ]) + }, []) + return ( @@ -314,12 +317,14 @@ const BiologyList = ({ groupId, deidentified }: BiologyListProps) => { color="secondary" open={toggleFilterByModal} onClose={() => setToggleFilterByModal(false)} - onSubmit={(newFilters) => addFilters({ ...filters, ...newFilters })} + onSubmit={(newFilters) => { + console.log('test filter', newFilters) + addFilters({ ...filters, ...newFilters }) + }} > {!deidentified && } {!deidentified && } - - + { searchInput, nda, ipp, - anabio, - loinc, + code, startDate, endDate, validatedStatus, @@ -390,8 +394,7 @@ const BiologyList = ({ groupId, deidentified }: BiologyListProps) => { filters: { nda, ipp, - anabio, - loinc, + code, startDate, endDate, validatedStatus, @@ -444,19 +447,11 @@ const BiologyList = ({ groupId, deidentified }: BiologyListProps) => { )} - - - - diff --git a/src/components/Dashboard/Documents/Documents.tsx b/src/components/Dashboard/Documents/index.tsx similarity index 98% rename from src/components/Dashboard/Documents/Documents.tsx rename to src/components/Dashboard/Documents/index.tsx index 4a6c2366a..739efd282 100644 --- a/src/components/Dashboard/Documents/Documents.tsx +++ b/src/components/Dashboard/Documents/index.tsx @@ -41,11 +41,13 @@ import { useAppDispatch, useAppSelector } from 'state' import Modal from 'components/ui/Modal' import EncounterStatusFilter from 'components/Filters/EncounterStatusFilter' import { SourceType } from 'types/scope' -import { Hierarchy } from 'types/hierarchy' import { useSearchParams } from 'react-router-dom' import { checkIfPageAvailable, handlePageError } from 'utils/paginationUtils' import { CanceledError } from 'axios' import { DocumentReference } from 'fhir/r4' +import { HierarchyWithLabelAndSystem } from 'types/hierarchy' +import { getCodeList } from 'services/aphp/serviceValueSets' +import { getConfig } from 'config' type DocumentsProps = { groupId?: string @@ -83,7 +85,7 @@ const Documents: React.FC = ({ groupId, deidentified }) => { const [page, setPage] = useState(getPageParam ? parseInt(getPageParam, 10) : 1) const [searchInputError, setSearchInputError] = useState(null) const [loadingStatus, setLoadingStatus] = useState(LoadingStatus.FETCHING) - const [encounterStatusList, setEncounterStatusList] = useState[]>([]) + const [encounterStatusList, setEncounterStatusList] = useState([]) const [toggleFilterInfoModal, setToggleFilterInfoModal] = useState(false) const [isReadonlyFilterInfoModal, setIsReadonlyFilterInfoModal] = useState(true) @@ -203,8 +205,8 @@ const Documents: React.FC = ({ groupId, deidentified }) => { useEffect(() => { const fetch = async () => { - const encounterStatus = await services.cohortCreation.fetchEncounterStatus() - setEncounterStatusList(encounterStatus) + const encounterStatus = await getCodeList(getConfig().core.valueSets.encounterStatus.url) + setEncounterStatusList(encounterStatus.results) } fetch() getSavedFilters() diff --git a/src/components/Dashboard/ExportModal/exportUtils.ts b/src/components/Dashboard/ExportModal/exportUtils.ts index 44bb55a55..2f066b7ab 100644 --- a/src/components/Dashboard/ExportModal/exportUtils.ts +++ b/src/components/Dashboard/ExportModal/exportUtils.ts @@ -1,4 +1,3 @@ -import { getConfig } from 'config' import { mapRequestParamsToSearchCriteria } from 'mappers/filters' import moment from 'moment' import { @@ -92,17 +91,13 @@ const fetchConditionCount = async (cohortId: string, conditionFilters?: SearchCr try { let conditionResp if (conditionFilters && conditionFilters !== null) { - const { diagnosticTypes, code, source, nda, startDate, endDate, executiveUnits, encounterStatus } = + const { code, diagnosticTypes, source, nda, startDate, endDate, executiveUnits, encounterStatus } = conditionFilters.filters - const _code = code - .map((e) => encodeURIComponent(`${getConfig().features.condition.valueSets.conditionHierarchy.url}|`) + e.id) - .join(',') - conditionResp = await fetchCondition({ size: 0, _list: [cohortId], - code: _code, + code: code.map((code) => encodeURI(`${code.system}|${code.id}`)).join(','), source: source, type: diagnosticTypes?.map((type) => type.id) ?? [], 'min-recorded-date': startDate ?? '', @@ -130,14 +125,10 @@ const fetchProcedureCount = async (cohortId: string, procedureFilters?: SearchCr if (procedureFilters && procedureFilters !== null) { const { code, source, nda, startDate, endDate, executiveUnits, encounterStatus } = procedureFilters.filters - const _code = code - .map((e) => encodeURIComponent(`${getConfig().features.procedure.valueSets.procedureHierarchy.url}|`) + e.id) - .join(',') - procedureResp = await fetchProcedure({ size: 0, _list: [cohortId], - code: _code, + code: code.map((code) => encodeURI(`${code.system}|${code.id}`)).join(','), source: source, minDate: startDate ?? '', maxDate: endDate ?? '', @@ -164,14 +155,10 @@ const fetchClaimCount = async (cohortId: string, claimFilters?: SearchCriterias< if (claimFilters && claimFilters !== null) { const { code, nda, startDate, endDate, executiveUnits, encounterStatus } = claimFilters.filters - const _code = code - .map((e) => encodeURIComponent(`${getConfig().features.claim.valueSets.claimHierarchy.url}|`) + e.id) - .join(',') - claimResp = await fetchClaim({ size: 0, _list: [cohortId], - diagnosis: _code, + diagnosis: code.map((code) => encodeURI(`${code.system}|${code.id}`)).join(','), minCreated: startDate ?? '', maxCreated: endDate ?? '', _text: claimFilters.searchInput, @@ -199,8 +186,16 @@ const fetchMedicationCount = async ( try { let medicationResp if (medicationFilters && medicationFilters !== null) { - const { nda, startDate, endDate, executiveUnits, encounterStatus, prescriptionTypes, administrationRoutes } = - medicationFilters.filters + const { + nda, + startDate, + endDate, + executiveUnits, + code, + encounterStatus, + prescriptionTypes, + administrationRoutes + } = medicationFilters.filters medicationResp = resourceType === ResourceType.MEDICATION_REQUEST @@ -209,6 +204,7 @@ const fetchMedicationCount = async ( _list: [cohortId], encounter: nda, _text: medicationFilters.searchInput, + code: code.map((code) => encodeURI(`${code.system}|${code.id}`)).join(','), type: prescriptionTypes?.map(({ id }) => id), minDate: startDate, maxDate: endDate, @@ -220,6 +216,7 @@ const fetchMedicationCount = async ( _list: [cohortId], encounter: nda, _text: medicationFilters.searchInput, + code: code.map((code) => encodeURI(`${code.system}|${code.id}`)).join(','), route: administrationRoutes?.map(({ id }) => id), minDate: startDate, maxDate: endDate, @@ -328,7 +325,7 @@ const fetchObservationCount = async (cohortId: string, observationFilters?: Sear try { let observationResp if (observationFilters && observationFilters !== null) { - const { nda, startDate, endDate, executiveUnits, encounterStatus, loinc, anabio, validatedStatus } = + const { nda, code, startDate, endDate, executiveUnits, encounterStatus, validatedStatus } = observationFilters.filters observationResp = await fetchObservation({ @@ -336,17 +333,7 @@ const fetchObservationCount = async (cohortId: string, observationFilters?: Sear _list: [cohortId], _text: observationFilters.searchInput, encounter: nda, - loinc: loinc - .map( - (e) => encodeURIComponent(`${getConfig().features.observation.valueSets.biologyHierarchyLoinc.url}|`) + e.id - ) - .join(','), - anabio: anabio - .map( - (e) => - encodeURIComponent(`${getConfig().features.observation.valueSets.biologyHierarchyAnabio.url}|`) + e.id - ) - .join(','), + code: code.map((code) => encodeURI(`${code.system}|${code.id}`)).join(','), minDate: startDate ?? '', maxDate: endDate ?? '', rowStatus: validatedStatus, diff --git a/src/components/Dashboard/ImagingList/index.tsx b/src/components/Dashboard/ImagingList/index.tsx index 642fb597f..25aa15228 100644 --- a/src/components/Dashboard/ImagingList/index.tsx +++ b/src/components/Dashboard/ImagingList/index.tsx @@ -33,10 +33,11 @@ import { useAppDispatch, useAppSelector } from 'state' import { BlockWrapper } from 'components/ui/Layout' import EncounterStatusFilter from 'components/Filters/EncounterStatusFilter' import { SourceType } from 'types/scope' -import { Hierarchy } from 'types/hierarchy' -import { AppConfig } from 'config' +import { AppConfig, getConfig } from 'config' import { useSearchParams } from 'react-router-dom' import { checkIfPageAvailable, handlePageError } from 'utils/paginationUtils' +import { FhirItem, HierarchyElementWithSystem } from 'types/hierarchy' +import { getCodeList } from 'services/aphp/serviceValueSets' type ImagingListProps = { groupId?: string @@ -59,8 +60,8 @@ const ImagingList = ({ groupId, deidentified }: ImagingListProps) => { const [toggleSavedFiltersModal, setToggleSavedFiltersModal] = useState(false) const [toggleFilterInfoModal, setToggleFilterInfoModal] = useState(false) const [isReadonlyFilterInfoModal, setIsReadonlyFilterInfoModal] = useState(true) - const [allModalities, setAllModalities] = useState[]>([]) - const [encounterStatusList, setEncounterStatusList] = useState[]>([]) + const [allModalities, setAllModalities] = useState([]) + const [encounterStatusList, setEncounterStatusList] = useState([]) const [page, setPage] = useState(getPageParam ? parseInt(getPageParam, 10) : 1) const [ @@ -164,12 +165,11 @@ const ImagingList = ({ groupId, deidentified }: ImagingListProps) => { useEffect(() => { const fetch = async () => { const [modalities, encounterStatus] = await Promise.all([ - services.cohortCreation.fetchModalities(), - services.cohortCreation.fetchEncounterStatus() + getCodeList(getConfig().features.imaging.valueSets.imagingModalities.url, true), + getCodeList(getConfig().core.valueSets.encounterStatus.url) ]) - - setAllModalities(modalities) - setEncounterStatusList(encounterStatus) + setAllModalities(modalities.results) + setEncounterStatusList(encounterStatus.results) } fetch() }, []) diff --git a/src/components/Dashboard/MedicationList/index.tsx b/src/components/Dashboard/MedicationList/index.tsx index 611002641..ef8266324 100644 --- a/src/components/Dashboard/MedicationList/index.tsx +++ b/src/components/Dashboard/MedicationList/index.tsx @@ -14,7 +14,7 @@ import ExecutiveUnitsFilter from 'components/Filters/ExecutiveUnitsFilter' import IppFilter from 'components/Filters/IppFilter' import List from 'components/ui/List' import Modal from 'components/ui/Modal' -import { medicationTabs } from 'components/Patient/PatientMedication/PatientMedication' +import { medicationTabs } from 'components/Patient/PatientMedication' import NdaFilter from 'components/Filters/NdaFilter' import PrescriptionTypesFilter from 'components/Filters/PrescriptionTypesFilter' import SearchInput from 'components/ui/Searchbar/SearchInput' @@ -36,6 +36,10 @@ import { selectFiltersAsArray } from 'utils/filters' import { mapToLabel } from 'mappers/pmsi' import { useSearchParams } from 'react-router-dom' import { checkIfPageAvailable, handlePageError } from 'utils/paginationUtils' +import { getCodeList } from 'services/aphp/serviceValueSets' +import { getConfig } from 'config' +import { getValueSetsFromSystems } from 'utils/valueSets' +import CodeFilter from 'components/Filters/CodeFilter' type MedicationListProps = { groupId?: string @@ -83,6 +87,7 @@ const MedicationList = ({ groupId, deidentified }: MedicationListProps) => { searchInput, filters, filters: { + code, nda, ipp, startDate, @@ -98,6 +103,7 @@ const MedicationList = ({ groupId, deidentified }: MedicationListProps) => { const filtersAsArray = useMemo( () => selectFiltersAsArray({ + code, nda, ipp, startDate, @@ -107,7 +113,7 @@ const MedicationList = ({ groupId, deidentified }: MedicationListProps) => { administrationRoutes, prescriptionTypes }), - [nda, ipp, startDate, endDate, executiveUnits, encounterStatus, administrationRoutes, prescriptionTypes] + [code, nda, ipp, startDate, endDate, executiveUnits, encounterStatus, administrationRoutes, prescriptionTypes] ) const [allAdministrationRoutes, setAllAdministrationRoutes] = useState[]>([]) @@ -140,6 +146,7 @@ const MedicationList = ({ groupId, deidentified }: MedicationListProps) => { orderBy, searchInput, filters: { + code, nda, ipp, startDate, @@ -197,13 +204,13 @@ const MedicationList = ({ groupId, deidentified }: MedicationListProps) => { useEffect(() => { const fetch = async () => { const [administrations, prescriptions, encounterStatus] = await Promise.all([ - services.cohortCreation.fetchAdministrations(), - services.cohortCreation.fetchPrescriptionTypes(), - services.cohortCreation.fetchEncounterStatus() + getCodeList(getConfig().features.medication.valueSets.medicationAdministrations.url), + getCodeList(getConfig().features.medication.valueSets.medicationPrescriptionTypes.url), + getCodeList(getConfig().core.valueSets.encounterStatus.url) ]) - setAllAdministrationRoutes(administrations) - setAllPrescriptionTypes(prescriptions) - setEncounterStatusList(encounterStatus) + setAllAdministrationRoutes(administrations.results) + setAllPrescriptionTypes(prescriptions.results) + setEncounterStatusList(encounterStatus.results) } fetch() }, []) @@ -214,6 +221,7 @@ const MedicationList = ({ groupId, deidentified }: MedicationListProps) => { setPage(1) } }, [ + code, searchInput, orderBy, nda, @@ -253,6 +261,13 @@ const MedicationList = ({ groupId, deidentified }: MedicationListProps) => { setTriggerClean(!triggerClean) }, [selectedTab]) + const references = useMemo(() => { + return getValueSetsFromSystems([ + getConfig().features.medication.valueSets.medicationAtc.url, + getConfig().features.medication.valueSets.medicationUcd.url + ]) + }, []) + return ( @@ -372,6 +387,7 @@ const MedicationList = ({ groupId, deidentified }: MedicationListProps) => { allAdministrationTypes={allAdministrationRoutes} /> )} + { readonly={isReadonlyFilterInfoModal} onClose={() => setToggleFilterInfoModal(false)} onSubmit={({ + code, filterName, searchInput, nda, @@ -439,6 +456,7 @@ const MedicationList = ({ groupId, deidentified }: MedicationListProps) => { searchInput, orderBy: { orderBy: Order.PERIOD_START, orderDirection: Direction.DESC }, filters: { + code, nda, ipp, prescriptionTypes, @@ -455,81 +473,79 @@ const MedicationList = ({ groupId, deidentified }: MedicationListProps) => { validationText="Sauvegarder" > - + + {!deidentified && ( - + )} {!deidentified && ( - - - + )} - - {!deidentified && ( - - )} - {!deidentified && ( - - - - )} - {selectedTab.id === ResourceType.MEDICATION_REQUEST && ( - - )} - {selectedTab.id === ResourceType.MEDICATION_ADMINISTRATION && ( - - )} - - - - + )} + + + + diff --git a/src/components/Dashboard/PMSIList/index.tsx b/src/components/Dashboard/PMSIList/index.tsx index b03004e16..67b4f359d 100644 --- a/src/components/Dashboard/PMSIList/index.tsx +++ b/src/components/Dashboard/PMSIList/index.tsx @@ -17,7 +17,7 @@ import IppFilter from 'components/Filters/IppFilter' import List from 'components/ui/List' import Modal from 'components/ui/Modal' import NdaFilter from 'components/Filters/NdaFilter' -import { PMSITabs } from 'components/Patient/PatientPMSI/PatientPMSI' +import { PMSITabs } from 'components/Patient/PatientPMSI' import SourceFilter from 'components/Filters/SourceFilter' import Searchbar from 'components/ui/Searchbar' import SearchInput from 'components/ui/Searchbar/SearchInput' @@ -33,13 +33,15 @@ import { Direction, FilterKeys, Order, PMSIFilters } from 'types/searchCriterias import { CanceledError } from 'axios' import { useSavedFilters } from 'hooks/filters/useSavedFilters' import services from 'services/aphp' -import { fetchClaimCodes, fetchConditionCodes, fetchProcedureCodes } from 'services/aphp/servicePmsi' import useSearchCriterias, { initPmsiSearchCriterias } from 'reducers/searchCriteriasReducer' import { cancelPendingRequest } from 'utils/abortController' import { selectFiltersAsArray } from 'utils/filters' import { mapToLabel, mapToSourceType } from 'mappers/pmsi' import { useSearchParams } from 'react-router-dom' import { checkIfPageAvailable, handlePageError } from 'utils/paginationUtils' +import { getCodeList } from 'services/aphp/serviceValueSets' +import { getConfig } from 'config' +import { getValueSetsFromSystems } from 'utils/valueSets' type PMSIListProps = { groupId?: string @@ -181,11 +183,11 @@ const PMSIList = ({ groupId, deidentified }: PMSIListProps) => { const fetch = async () => { try { const [diagnosticTypes, encounterStatus] = await Promise.all([ - services.cohortCreation.fetchDiagnosticTypes(), - services.cohortCreation.fetchEncounterStatus() + getCodeList(getConfig().features.condition.valueSets.conditionStatus.url), + getCodeList(getConfig().core.valueSets.encounterStatus.url) ]) - setAllDiagnosticTypesList(diagnosticTypes) - setEncounterStatusList(encounterStatus) + setAllDiagnosticTypesList(diagnosticTypes.results) + setEncounterStatusList(encounterStatus.results) } catch (e) { /* empty */ } @@ -240,14 +242,14 @@ const PMSIList = ({ groupId, deidentified }: PMSIListProps) => { setTriggerClean(!triggerClean) }, [selectedTab]) - const fetchCodes = useCallback(() => { + const references = useMemo(() => { switch (selectedTab.id) { case ResourceType.CONDITION: - return fetchConditionCodes + return getValueSetsFromSystems([getConfig().features.condition.valueSets.conditionHierarchy.url]) case ResourceType.PROCEDURE: - return fetchProcedureCodes + return getValueSetsFromSystems([getConfig().features.procedure.valueSets.procedureHierarchy.url]) default: - return fetchClaimCodes + return getValueSetsFromSystems([getConfig().features.claim.valueSets.claimHierarchy.url]) } }, [selectedTab.id]) @@ -372,7 +374,7 @@ const PMSIList = ({ groupId, deidentified }: PMSIListProps) => { > {!deidentified && } {!deidentified && } - + {selectedTab.id === ResourceType.CONDITION && ( { )} {selectedTab.id === ResourceType.CONDITION && ( diff --git a/src/components/Dashboard/PatientList/PatientList.tsx b/src/components/Dashboard/PatientList/index.tsx similarity index 99% rename from src/components/Dashboard/PatientList/PatientList.tsx rename to src/components/Dashboard/PatientList/index.tsx index 5a38c174c..57217cfb7 100644 --- a/src/components/Dashboard/PatientList/PatientList.tsx +++ b/src/components/Dashboard/PatientList/index.tsx @@ -1,6 +1,6 @@ import React, { useState, useEffect, useRef, useMemo } from 'react' -import { CircularProgress, Grid, Tooltip } from '@mui/material' +import { CircularProgress, Grid, Tooltip, Typography } from '@mui/material' import DataTablePatient from 'components/DataTable/DataTablePatient' @@ -46,10 +46,10 @@ import VitalStatusesFilter from 'components/Filters/VitalStatusesFilter' import TextInput from 'components/Filters/TextInput' import { useSavedFilters } from 'hooks/filters/useSavedFilters' import { ResourceType } from 'types/requestCriterias' -import List from 'components/ui/List' import { useAppDispatch, useAppSelector } from 'state' import { useSearchParams } from 'react-router-dom' import { checkIfPageAvailable, handlePageError } from 'utils/paginationUtils' +import List from 'components/ui/List' type PatientListProps = { total: number @@ -68,10 +68,10 @@ const PatientList = ({ groupId, total, deidentified }: PatientListProps) => { const [toggleFilterInfoModal, setToggleFilterInfoModal] = useState(false) const [isReadonlyFilterInfoModal, setIsReadonlyFilterInfoModal] = useState(true) const { + allSavedFiltersAsListItems, allSavedFilters, savedFiltersErrors, selectedSavedFilter, - allSavedFiltersAsListItems, methods: { getSavedFilters, postSavedFilter, @@ -141,7 +141,6 @@ const PatientList = ({ groupId, total, deidentified }: PatientListProps) => { if (agePyramidData) setAgePyramid(agePyramidData) } setPatientsResult((ps) => ({ ...ps, nb: totalPatients, label: 'patient(s)' })) - checkIfPageAvailable(totalPatients, page, setPage, dispatch) } setLoadingStatus(LoadingStatus.SUCCESS) @@ -166,7 +165,6 @@ const PatientList = ({ groupId, total, deidentified }: PatientListProps) => { useEffect(() => { setSearchParams({ page: page.toString() }) - handlePageError(page, setPage, dispatch, setLoadingStatus) }, [page]) diff --git a/src/components/Filters/AdministrationTypesFilter/index.tsx b/src/components/Filters/AdministrationTypesFilter/index.tsx index b94062012..77e3ea0a2 100644 --- a/src/components/Filters/AdministrationTypesFilter/index.tsx +++ b/src/components/Filters/AdministrationTypesFilter/index.tsx @@ -2,14 +2,14 @@ import { Autocomplete, TextField, Typography } from '@mui/material' import { InputWrapper } from 'components/ui/Inputs' import { FormContext } from 'components/ui/Modal' import React, { useContext, useEffect, useState } from 'react' -import { Hierarchy } from 'types/hierarchy' +import { FhirItem } from 'types/hierarchy' import { LabelObject } from 'types/searchCriterias' import { capitalizeFirstLetter } from 'utils/capitalize' type AdministrationTypesFilterProps = { value: LabelObject[] name: string - allAdministrationTypes: Hierarchy[] + allAdministrationTypes: FhirItem[] disabled?: boolean } diff --git a/src/components/Filters/AnabioFilter/index.tsx b/src/components/Filters/AnabioFilter/index.tsx deleted file mode 100644 index 54c0e882c..000000000 --- a/src/components/Filters/AnabioFilter/index.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import { Typography } from '@mui/material' -import { InputWrapper } from 'components/ui/Inputs' -import AsyncAutocomplete from 'components/ui/Inputs/AsyncAutocomplete' -import { FormContext } from 'components/ui/Modal' -import React, { useContext, useEffect, useState } from 'react' -import { LabelObject } from 'types/searchCriterias' - -type AnabioFilterProps = { - value: LabelObject[] - name: string - disabled?: boolean - onFetch: (text: string, noStar: boolean, signal: AbortSignal) => Promise -} - -const AnabioFilter = ({ name, value, disabled = false, onFetch }: AnabioFilterProps) => { - const context = useContext(FormContext) - const [anabio, setAnabio] = useState(value) - - useEffect(() => { - if (context?.updateFormData) context.updateFormData(name, anabio) - }, [anabio]) - - return ( - - Code ANABIO : - onFetch(text, false, signal)} - onChange={(newValue) => { - setAnabio(newValue) - }} - /> - - ) -} - -export default AnabioFilter diff --git a/src/components/Filters/CodeFilter/index.tsx b/src/components/Filters/CodeFilter/index.tsx index 28fa4479e..223016d19 100644 --- a/src/components/Filters/CodeFilter/index.tsx +++ b/src/components/Filters/CodeFilter/index.tsx @@ -1,38 +1,35 @@ import { Typography } from '@mui/material' +import ValueSetField from 'components/SearchValueSet/ValueSetField' import { InputWrapper } from 'components/ui/Inputs' -import AsyncAutocomplete from 'components/ui/Inputs/AsyncAutocomplete' import { FormContext } from 'components/ui/Modal' import React, { useContext, useEffect, useState } from 'react' import { LabelObject } from 'types/searchCriterias' +import { Reference } from 'types/valueSet' type CodeFilterProps = { value: LabelObject[] + references: Reference[] name: string disabled?: boolean - onFetch: (text: string, noStar: boolean, signal: AbortSignal) => Promise } -const CodeFilter = ({ name, value, disabled = false, onFetch }: CodeFilterProps) => { +const CodeFilter = ({ name, value, references, disabled = false }: CodeFilterProps) => { const context = useContext(FormContext) const [code, setCode] = useState(value) useEffect(() => { if (context?.updateFormData) context.updateFormData(name, code) - }, [code]) + }, [code, name]) return ( Code : - onFetch(text, false, signal)} - onChange={(newValue) => { - setCode(newValue) - }} + value={code} + references={references} + placeholder="Sélectionner des codes" + onSelect={setCode} /> ) diff --git a/src/components/Filters/DiagnosticTypesFilter/index.tsx b/src/components/Filters/DiagnosticTypesFilter/index.tsx index fa01bfb43..c89f1df8a 100644 --- a/src/components/Filters/DiagnosticTypesFilter/index.tsx +++ b/src/components/Filters/DiagnosticTypesFilter/index.tsx @@ -2,14 +2,14 @@ import { Autocomplete, TextField, Typography } from '@mui/material' import { InputWrapper } from 'components/ui/Inputs' import { FormContext } from 'components/ui/Modal' import React, { useContext, useEffect, useState } from 'react' -import { Hierarchy } from 'types/hierarchy' +import { FhirItem, HierarchyElementWithSystem } from 'types/hierarchy' import { LabelObject } from 'types/searchCriterias' import { capitalizeFirstLetter } from 'utils/capitalize' type DiagnosticTypesFilterProps = { value: LabelObject[] name: string - allDiagnosticTypesList: Hierarchy[] + allDiagnosticTypesList: FhirItem[] disabled?: boolean } diff --git a/src/components/Filters/ExecutiveUnitsFilter/index.tsx b/src/components/Filters/ExecutiveUnitsFilter/index.tsx index e08e4ae67..c3dbbdf5d 100644 --- a/src/components/Filters/ExecutiveUnitsFilter/index.tsx +++ b/src/components/Filters/ExecutiveUnitsFilter/index.tsx @@ -3,8 +3,7 @@ import React, { useContext, useEffect, useState } from 'react' import { ScopeElement } from 'types' import { SourceType } from 'types/scope' import { Hierarchy } from 'types/hierarchy' -import ExecutiveUnitsInput from 'components/ui/Inputs/ExecutiveUnit' -import { Typography } from '@mui/material' +import ExecutiveUnitsInput from 'components/ui/Inputs/ExecutiveUnits' type ExecutiveUnitsFilterProps = { value: Hierarchy[] @@ -19,7 +18,7 @@ const ExecutiveUnitsFilter = ({ name, value, sourceType, disabled = false }: Exe useEffect(() => { context?.updateFormData(name, population) - }, [population, context, name]) + }, [population, name]) return ( setPopulation(selectedPopulation)} - label={ - - Unité exécutrice : - - } /> ) } diff --git a/src/components/Filters/LoincFilter/index.tsx b/src/components/Filters/LoincFilter/index.tsx deleted file mode 100644 index 0da1971d5..000000000 --- a/src/components/Filters/LoincFilter/index.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import { Typography } from '@mui/material' -import AsyncAutocomplete from 'components/ui/Inputs/AsyncAutocomplete' -import { InputWrapper } from 'components/ui/Inputs' -import { FormContext } from 'components/ui/Modal' -import React, { useContext, useEffect, useState } from 'react' -import { LabelObject } from 'types/searchCriterias' - -type LoincFilterProps = { - value: LabelObject[] - name: string - disabled?: boolean - onFetch: (text: string, noStar: boolean, signal: AbortSignal) => Promise -} - -const LoincFilter = ({ name, value, disabled = false, onFetch }: LoincFilterProps) => { - const context = useContext(FormContext) - const [loinc, setLoinc] = useState(value) - - useEffect(() => { - if (context?.updateFormData) context.updateFormData(name, loinc) - }, [loinc]) - - return ( - - Code LOINC : - onFetch(text, false, signal)} - onChange={(newValue) => { - setLoinc(newValue) - }} - /> - - ) -} - -export default LoincFilter diff --git a/src/components/Filters/PrescriptionTypesFilter/index.tsx b/src/components/Filters/PrescriptionTypesFilter/index.tsx index 4aaea8bfe..bc7fbe457 100644 --- a/src/components/Filters/PrescriptionTypesFilter/index.tsx +++ b/src/components/Filters/PrescriptionTypesFilter/index.tsx @@ -2,14 +2,14 @@ import { Autocomplete, TextField, Typography } from '@mui/material' import { InputWrapper } from 'components/ui/Inputs' import { FormContext } from 'components/ui/Modal' import React, { useContext, useEffect, useState } from 'react' -import { Hierarchy } from 'types/hierarchy' +import { FhirItem } from 'types/hierarchy' import { LabelObject } from 'types/searchCriterias' import { capitalizeFirstLetter } from 'utils/capitalize' type PrescriptionTypesFilterProps = { value: LabelObject[] name: string - allPrescriptionTypes: Hierarchy[] + allPrescriptionTypes: FhirItem[] disabled?: boolean } diff --git a/src/components/Hierarchy/CodesWithSystems.tsx b/src/components/Hierarchy/CodesWithSystems.tsx new file mode 100644 index 000000000..d608a394a --- /dev/null +++ b/src/components/Hierarchy/CodesWithSystems.tsx @@ -0,0 +1,56 @@ +import { Chip, Grid, Typography } from '@mui/material' +import React, { useMemo } from 'react' +import { Hierarchy } from 'types/hierarchy' +import { groupBySystem } from 'utils/hierarchy/hierarchy' +import { getLabelFromCode, getLabelFromSystem, isDisplayedWithSystem } from 'utils/valueSets' + +type CodesWithSystemsProps = { + codes: Hierarchy[] + disabled?: boolean + isExtended?: boolean + onDelete: (node: Hierarchy) => void +} + +const CodesWithSystems = ({ codes, disabled = false, isExtended = true, onDelete }: CodesWithSystemsProps) => { + const groupedBySystem = useMemo(() => groupBySystem(codes), [codes]) + + const ChipGroup = ({ codes }: { codes: Hierarchy[] }) => ( + <> + {codes.map((code) => ( + onDelete(code)} + /> + ))} + + ) + + return ( + <> + {codes.length > 0 && isExtended && ( + + {groupedBySystem.map((group) => ( + + {isDisplayedWithSystem(group.system) && ( + + {`${getLabelFromSystem(group.system)} (${group.codes.length})`} + + )} + + + ))} + + )} + {codes.length > 0 && !isExtended && ( +
+ +
+ )} + + ) +} + +export default CodesWithSystems diff --git a/src/components/Hierarchy/SelectedCodes.tsx b/src/components/Hierarchy/SelectedCodes.tsx new file mode 100644 index 000000000..76778f27d --- /dev/null +++ b/src/components/Hierarchy/SelectedCodes.tsx @@ -0,0 +1,51 @@ +import React, { useState } from 'react' + +import { Collapse, Grid, Typography } from '@mui/material' +import { KeyboardArrowDown, KeyboardArrowRight } from '@mui/icons-material' +import { Hierarchy } from 'types/hierarchy' +import CodesWithSystems from './CodesWithSystems' + +type SelectedCodesProps = { + values: Hierarchy[] + onDelete: (node: Hierarchy) => void +} + +const SelectedCodes = ({ values, onDelete }: SelectedCodesProps) => { + const [openSelectedCodesDrawer, setOpenSelectedCodesDrawer] = useState(false) + + return ( + + + + + + + + + + {values.length} sélectionné(s) + + + + {values.length > 0 && ( + <> + {openSelectedCodesDrawer ? ( + setOpenSelectedCodesDrawer((prev) => !prev)} /> + ) : ( + setOpenSelectedCodesDrawer((prev) => !prev)} /> + )} + + )} + + + + ) +} + +export default SelectedCodes diff --git a/src/components/Hierarchy/styles.ts b/src/components/Hierarchy/styles.ts new file mode 100644 index 000000000..ba03f22e3 --- /dev/null +++ b/src/components/Hierarchy/styles.ts @@ -0,0 +1,31 @@ +import { Grid, styled } from '@mui/material' + +type RowContainerProps = { + color: string +} + +type RowWrapperProps = { + size?: string +} + +type CellWrapperProps = { + cursor?: boolean + color?: string + fontWeight?: number +} + +export const RowContainerWrapper = styled(Grid)(({ color }) => ({ + backgroundColor: color, + borderBottom: '1px solid rgba(224, 224, 224, 1)' +})) + +export const RowWrapper = styled(Grid)(({ size = '50px' }) => ({ + height: size, + padding: 0 +})) + +export const CellWrapper = styled(Grid)(({ cursor = false, color = '#4F4F4f', fontWeight = 600 }) => ({ + color: color, + cursor: cursor ? 'pointer' : '', + fontWeight: fontWeight +})) diff --git a/src/components/Patient/PatientBiology/PatientBiology.tsx b/src/components/Patient/PatientBiology/PatientBiology.tsx index 0fbae1ca8..a5d3bfcee 100644 --- a/src/components/Patient/PatientBiology/PatientBiology.tsx +++ b/src/components/Patient/PatientBiology/PatientBiology.tsx @@ -23,26 +23,23 @@ import { BlockWrapper } from 'components/ui/Layout' import useSearchCriterias, { initBioSearchCriterias } from 'reducers/searchCriteriasReducer' import Chip from 'components/ui/Chip' import { AlertWrapper } from 'components/ui/Alert' -import AnabioFilter from 'components/Filters/AnabioFilter' import DatesRangeFilter from 'components/Filters/DatesRangeFilter' import ExecutiveUnitsFilter from 'components/Filters/ExecutiveUnitsFilter' -import LoincFilter from 'components/Filters/LoincFilter' import NdaFilter from 'components/Filters/NdaFilter' import { Save, SavedSearch } from '@mui/icons-material' import { ResourceType } from 'types/requestCriterias' import { useSavedFilters } from 'hooks/filters/useSavedFilters' import List from 'components/ui/List' import TextInput from 'components/Filters/TextInput' -import { - fetchLoincCodes as fetchLoincCodesApi, - fetchAnabioCodes as fetchAnabioCodesApi -} from 'services/aphp/serviceBiology' -import services from 'services/aphp' import EncounterStatusFilter from 'components/Filters/EncounterStatusFilter' import { SourceType } from 'types/scope' -import { Hierarchy } from 'types/hierarchy' import { useSearchParams } from 'react-router-dom' import { checkIfPageAvailable, handlePageError } from 'utils/paginationUtils' +import { FhirItem } from 'types/hierarchy' +import { getConfig } from 'config' +import { getCodeList } from 'services/aphp/serviceValueSets' +import { getValueSetsFromSystems } from 'utils/valueSets' +import CodeFilter from 'components/Filters/CodeFilter' type PatientBiologyProps = { groupId?: string @@ -62,7 +59,7 @@ const PatientBiology = ({ groupId }: PatientBiologyProps) => { const [toggleSavedFiltersModal, setToggleSavedFiltersModal] = useState(false) const [toggleFilterInfoModal, setToggleFilterInfoModal] = useState(false) const [isReadonlyFilterInfoModal, setIsReadonlyFilterInfoModal] = useState(true) - const [encounterStatusList, setEncounterStatusList] = useState[]>([]) + const [encounterStatusList, setEncounterStatusList] = useState([]) const { allSavedFilters, savedFiltersErrors, @@ -93,7 +90,7 @@ const PatientBiology = ({ groupId }: PatientBiologyProps) => { orderBy, searchInput, filters, - filters: { nda, loinc, anabio, startDate, endDate, executiveUnits, validatedStatus, encounterStatus } + filters: { nda, code, startDate, endDate, executiveUnits, validatedStatus, encounterStatus } }, { changeOrderBy, changeSearchInput, addFilters, removeFilter, addSearchCriterias } ] = useSearchCriterias(initBioSearchCriterias) @@ -101,14 +98,13 @@ const PatientBiology = ({ groupId }: PatientBiologyProps) => { return selectFiltersAsArray({ nda, validatedStatus, - loinc, - anabio, + code, startDate, endDate, executiveUnits, encounterStatus }) - }, [nda, loinc, anabio, startDate, endDate, executiveUnits, encounterStatus]) + }, [nda, code, startDate, endDate, executiveUnits, encounterStatus]) const controllerRef = useRef(null) const meState = useAppSelector((state) => state.me) @@ -125,7 +121,7 @@ const PatientBiology = ({ groupId }: PatientBiologyProps) => { searchCriterias: { orderBy, searchInput, - filters: { validatedStatus, nda, loinc, anabio, startDate, endDate, executiveUnits, encounterStatus } + filters: { validatedStatus, nda, code, startDate, endDate, executiveUnits, encounterStatus } } }, groupId, @@ -150,7 +146,7 @@ const PatientBiology = ({ groupId }: PatientBiologyProps) => { useEffect(() => { const fetchEncounterStatusList = async () => { - const encounterStatus = await services.cohortCreation.fetchEncounterStatus() + const encounterStatus = (await getCodeList(getConfig().core.valueSets.encounterStatus.url)).results setEncounterStatusList(encounterStatus) } fetchEncounterStatusList() @@ -163,7 +159,7 @@ const PatientBiology = ({ groupId }: PatientBiologyProps) => { setLoadingStatus(LoadingStatus.IDDLE) setPage(1) } - }, [nda, loinc, anabio, startDate, endDate, executiveUnits, validatedStatus, orderBy, searchInput, encounterStatus]) + }, [nda, code, startDate, endDate, executiveUnits, validatedStatus, orderBy, searchInput, encounterStatus]) useEffect(() => { const updatedSearchParams = new URLSearchParams(searchParams) @@ -179,6 +175,13 @@ const PatientBiology = ({ groupId }: PatientBiologyProps) => { } }, [loadingStatus]) + const references = useMemo(() => { + return getValueSetsFromSystems([ + getConfig().features.observation.valueSets.biologyHierarchyAnabio.url, + getConfig().features.observation.valueSets.biologyHierarchyLoinc.url + ]) + }, []) + return ( @@ -272,8 +275,7 @@ const PatientBiology = ({ groupId }: PatientBiologyProps) => { onSubmit={(newFilters) => addFilters({ ...filters, ...newFilters })} > {!searchResults.deidentified && } - - + { filterName, searchInput, nda, - anabio, - loinc, + code, startDate, endDate, validatedStatus, @@ -340,7 +341,7 @@ const PatientBiology = ({ groupId }: PatientBiologyProps) => { { searchInput, orderBy: { orderBy: Order.FAMILY, orderDirection: Direction.ASC }, - filters: { nda, anabio, loinc, startDate, endDate, validatedStatus, executiveUnits, encounterStatus } + filters: { nda, code, startDate, endDate, validatedStatus, executiveUnits, encounterStatus } }, searchResults.deidentified ?? true ) @@ -376,19 +377,11 @@ const PatientBiology = ({ groupId }: PatientBiologyProps) => { /> - - - - diff --git a/src/components/Patient/PatientDocs/PatientDocs.tsx b/src/components/Patient/PatientDocs/PatientDocs.tsx index 32d7a6f8d..289a1070e 100644 --- a/src/components/Patient/PatientDocs/PatientDocs.tsx +++ b/src/components/Patient/PatientDocs/PatientDocs.tsx @@ -47,11 +47,12 @@ import TextInput from 'components/Filters/TextInput' import List from 'components/ui/List' import DocStatusFilter from '../../Filters/DocStatusFilter' import EncounterStatusFilter from 'components/Filters/EncounterStatusFilter' -import services from 'services/aphp' import { SourceType } from 'types/scope' -import { Hierarchy } from 'types/hierarchy' import { useSearchParams } from 'react-router-dom' import { checkIfPageAvailable, handlePageError } from 'utils/paginationUtils' +import { FhirItem } from 'types/hierarchy' +import { getCodeList } from 'services/aphp/serviceValueSets' +import { getConfig } from 'config' const PatientDocs: React.FC = ({ groupId }) => { const dispatch = useAppDispatch() @@ -62,7 +63,7 @@ const PatientDocs: React.FC = ({ groupId }) => { const [toggleSavedFiltersModal, setToggleSavedFiltersModal] = useState(false) const [toggleFilterInfoModal, setToggleFilterInfoModal] = useState(false) const [isReadonlyFilterInfoModal, setIsReadonlyFilterInfoModal] = useState(true) - const [encounterStatusList, setEncounterStatusList] = useState[]>([]) + const [encounterStatusList, setEncounterStatusList] = useState([]) const patient = useAppSelector((state) => state.patient) const searchResults = { deidentified: patient?.deidentified || false, @@ -162,8 +163,8 @@ const PatientDocs: React.FC = ({ groupId }) => { useEffect(() => { const fetch = async () => { - const encounterStatus = await services.cohortCreation.fetchEncounterStatus() - setEncounterStatusList(encounterStatus) + const encounterStatus = await getCodeList(getConfig().core.valueSets.encounterStatus.url) + setEncounterStatusList(encounterStatus.results) } fetch() }, []) diff --git a/src/components/Patient/PatientForms/MaternityForms/Timeline.tsx b/src/components/Patient/PatientForms/MaternityForms/Timeline.tsx index 1edae7438..da6cbb732 100644 --- a/src/components/Patient/PatientForms/MaternityForms/Timeline.tsx +++ b/src/components/Patient/PatientForms/MaternityForms/Timeline.tsx @@ -42,7 +42,6 @@ const Timeline: React.FC = ({ loading, questionnaireResponses, ma ) : ( <> - {console.log('manelle tu es MON canard')} {/* */}
{years.reverse().map((year) => ( diff --git a/src/components/Patient/PatientForms/MaternityForms/index.tsx b/src/components/Patient/PatientForms/MaternityForms/index.tsx index 254d160b4..7a501beec 100644 --- a/src/components/Patient/PatientForms/MaternityForms/index.tsx +++ b/src/components/Patient/PatientForms/MaternityForms/index.tsx @@ -22,7 +22,9 @@ import Timeline from './Timeline' import services from 'services/aphp' import EncounterStatusFilter from 'components/Filters/EncounterStatusFilter' import { SourceType } from 'types/scope' -import { Hierarchy } from 'types/hierarchy' +import { FhirItem, HierarchyElementWithSystem } from 'types/hierarchy' +import { getCodeList } from 'services/aphp/serviceValueSets' +import { getConfig } from 'config' type PatientFormsProps = { groupId?: string @@ -36,7 +38,7 @@ const MaternityForm = ({ groupId }: PatientFormsProps) => { const [loadingStatus, setLoadingStatus] = useState(LoadingStatus.FETCHING) const [maternityFormNamesIds, setMaternityFormNamesIds] = useState([]) - const [encounterStatusList, setEncounterStatusList] = useState[]>([]) + const [encounterStatusList, setEncounterStatusList] = useState([]) const [ { @@ -85,10 +87,10 @@ const MaternityForm = ({ groupId }: PatientFormsProps) => { const fetch = async () => { const [maternityFormNamesIds, encounterStatus] = await Promise.all([ services.patients.fetchMaternityFormNamesIds(), - services.cohortCreation.fetchEncounterStatus() + getCodeList(getConfig().core.valueSets.encounterStatus.url) ]) setMaternityFormNamesIds(maternityFormNamesIds) - setEncounterStatusList(encounterStatus) + setEncounterStatusList(encounterStatus.results) } useEffect(() => { diff --git a/src/components/Patient/PatientImaging/PatientImaging.tsx b/src/components/Patient/PatientImaging/index.tsx similarity index 96% rename from src/components/Patient/PatientImaging/PatientImaging.tsx rename to src/components/Patient/PatientImaging/index.tsx index 4586a29e1..ad99a7073 100644 --- a/src/components/Patient/PatientImaging/PatientImaging.tsx +++ b/src/components/Patient/PatientImaging/index.tsx @@ -29,14 +29,14 @@ import { useSavedFilters } from 'hooks/filters/useSavedFilters' import { Save, SavedSearch } from '@mui/icons-material' import TextInput from 'components/Filters/TextInput' import List from 'components/ui/List' -import services from 'services/aphp' import { BlockWrapper } from 'components/ui/Layout' import EncounterStatusFilter from 'components/Filters/EncounterStatusFilter' import { SourceType } from 'types/scope' -import { Hierarchy } from 'types/hierarchy' -import { AppConfig } from 'config' +import { AppConfig, getConfig } from 'config' import { useSearchParams } from 'react-router-dom' import { checkIfPageAvailable, handlePageError } from 'utils/paginationUtils' +import { FhirItem } from 'types/hierarchy' +import { getCodeList } from 'services/aphp/serviceValueSets' const PatientImaging: React.FC = ({ groupId }) => { const dispatch = useAppDispatch() @@ -51,8 +51,8 @@ const PatientImaging: React.FC = ({ groupId }) => { const [toggleSavedFiltersModal, setToggleSavedFiltersModal] = useState(false) const [toggleFilterInfoModal, setToggleFilterInfoModal] = useState(false) const [isReadonlyFilterInfoModal, setIsReadonlyFilterInfoModal] = useState(true) - const [allModalities, setAllModalities] = useState[]>([]) - const [encounterStatusList, setEncounterStatusList] = useState[]>([]) + const [allModalities, setAllModalities] = useState([]) + const [encounterStatusList, setEncounterStatusList] = useState([]) const searchResults = { deidentified: patient?.deidentified || false, @@ -131,12 +131,12 @@ const PatientImaging: React.FC = ({ groupId }) => { useEffect(() => { const fetch = async () => { const [modalities, encounterStatus] = await Promise.all([ - services.cohortCreation.fetchModalities(), - services.cohortCreation.fetchEncounterStatus() + getCodeList(getConfig().features.imaging.valueSets.imagingModalities.url, true), + getCodeList(getConfig().core.valueSets.encounterStatus.url) ]) - setAllModalities(modalities) - setEncounterStatusList(encounterStatus) + setAllModalities(modalities.results) + setEncounterStatusList(encounterStatus.results) } fetch() }, []) diff --git a/src/components/Patient/PatientMedication/PatientMedication.tsx b/src/components/Patient/PatientMedication/index.tsx similarity index 91% rename from src/components/Patient/PatientMedication/PatientMedication.tsx rename to src/components/Patient/PatientMedication/index.tsx index bcd95438e..05e96d033 100644 --- a/src/components/Patient/PatientMedication/PatientMedication.tsx +++ b/src/components/Patient/PatientMedication/index.tsx @@ -36,12 +36,15 @@ import { MedicationAdministration, MedicationRequest } from 'fhir/r4' import TextInput from 'components/Filters/TextInput' import List from 'components/ui/List' import { mapToAttribute, mapToLabel } from 'mappers/pmsi' -import services from 'services/aphp' import EncounterStatusFilter from 'components/Filters/EncounterStatusFilter' import { SourceType } from 'types/scope' -import { Hierarchy } from 'types/hierarchy' import { useSearchParams } from 'react-router-dom' import { checkIfPageAvailable, handlePageError } from 'utils/paginationUtils' +import { FhirItem, HierarchyElementWithSystem } from 'types/hierarchy' +import { getConfig } from 'config' +import { getCodeList } from 'services/aphp/serviceValueSets' +import { getValueSetsFromSystems } from 'utils/valueSets' +import CodeFilter from 'components/Filters/CodeFilter' type PatientMedicationProps = { groupId?: string @@ -72,7 +75,7 @@ const PatientMedication = ({ groupId }: PatientMedicationProps) => { const [toggleFilterInfoModal, setToggleFilterInfoModal] = useState(false) const [isReadonlyFilterInfoModal, setIsReadonlyFilterInfoModal] = useState(true) const [triggerClean, setTriggerClean] = useState(false) - const [encounterStatusList, setEncounterStatusList] = useState[]>([]) + const [encounterStatusList, setEncounterStatusList] = useState([]) const dispatch = useAppDispatch() const patient = useAppSelector((state) => state.patient) @@ -106,12 +109,22 @@ const PatientMedication = ({ groupId }: PatientMedicationProps) => { orderBy, searchInput, filters, - filters: { nda, prescriptionTypes, startDate, endDate, administrationRoutes, executiveUnits, encounterStatus } + filters: { + code, + nda, + prescriptionTypes, + startDate, + endDate, + administrationRoutes, + executiveUnits, + encounterStatus + } }, { changeOrderBy, changeSearchInput, addFilters, removeFilter, removeSearchCriterias, addSearchCriterias } ] = useSearchCriterias(initMedSearchCriterias) const filtersAsArray = useMemo(() => { return selectFiltersAsArray({ + code, nda, prescriptionTypes, administrationRoutes, @@ -120,10 +133,10 @@ const PatientMedication = ({ groupId }: PatientMedicationProps) => { executiveUnits, encounterStatus }) - }, [nda, prescriptionTypes, administrationRoutes, startDate, endDate, executiveUnits, encounterStatus]) + }, [nda, code, prescriptionTypes, administrationRoutes, startDate, endDate, executiveUnits, encounterStatus]) - const [allAdministrationRoutes, setAllAdministrationRoutes] = useState[]>([]) - const [allPrescriptionTypes, setAllPrescriptionTypes] = useState[]>([]) + const [allAdministrationRoutes, setAllAdministrationRoutes] = useState([]) + const [allPrescriptionTypes, setAllPrescriptionTypes] = useState([]) const [searchResults, setSearchResults] = useState({ deidentified: false, list: [], @@ -150,6 +163,7 @@ const PatientMedication = ({ groupId }: PatientMedicationProps) => { orderBy, searchInput, filters: { + code, nda, administrationRoutes, prescriptionTypes, @@ -183,13 +197,13 @@ const PatientMedication = ({ groupId }: PatientMedicationProps) => { useEffect(() => { const fetch = async () => { const [administrations, prescriptions, encounterStatus] = await Promise.all([ - services.cohortCreation.fetchAdministrations(), - services.cohortCreation.fetchPrescriptionTypes(), - services.cohortCreation.fetchEncounterStatus() + getCodeList(getConfig().features.medication.valueSets.medicationAdministrations.url), + getCodeList(getConfig().features.medication.valueSets.medicationPrescriptionTypes.url), + getCodeList(getConfig().core.valueSets.encounterStatus.url) ]) - setAllAdministrationRoutes(administrations) - setAllPrescriptionTypes(prescriptions) - setEncounterStatusList(encounterStatus) + setAllAdministrationRoutes(administrations.results) + setAllPrescriptionTypes(prescriptions.results) + setEncounterStatusList(encounterStatus.results) } fetch() setOldTabs(selectedTab) @@ -202,6 +216,7 @@ const PatientMedication = ({ groupId }: PatientMedicationProps) => { setOldTabs(selectedTab) } }, [ + code, searchInput, nda, startDate, @@ -215,11 +230,9 @@ const PatientMedication = ({ groupId }: PatientMedicationProps) => { useEffect(() => { setOldTabs(selectedTab) - const updatedSearchParams = new URLSearchParams(searchParams) updatedSearchParams.set('page', page.toString()) setSearchParams(updatedSearchParams) - handlePageError(page, setPage, dispatch, setLoadingStatus) }, [page]) @@ -253,6 +266,13 @@ const PatientMedication = ({ groupId }: PatientMedicationProps) => { }) }, [patient, selectedTab.id]) + const references = useMemo(() => { + return getValueSetsFromSystems([ + getConfig().features.medication.valueSets.medicationAtc.url, + getConfig().features.medication.valueSets.medicationUcd.url + ]) + }, []) + return ( @@ -363,6 +383,7 @@ const PatientMedication = ({ groupId }: PatientMedicationProps) => { allAdministrationTypes={allAdministrationRoutes} /> )} + { readonly={isReadonlyFilterInfoModal} onClose={() => setToggleFilterInfoModal(false)} onSubmit={({ + code, filterName, searchInput, nda, @@ -429,6 +451,7 @@ const PatientMedication = ({ groupId }: PatientMedicationProps) => { searchInput, orderBy: { orderBy: Order.PERIOD_START, orderDirection: Direction.DESC }, filters: { + code, nda, prescriptionTypes, startDate, @@ -444,7 +467,6 @@ const PatientMedication = ({ groupId }: PatientMedicationProps) => { validationText="Sauvegarder" > - { minLimit={2} maxLimit={50} /> - {!searchResults.deidentified && ( - - )} - {!searchResults.deidentified && ( { allAdministrationTypes={allAdministrationRoutes} /> )} + { encounterStatusList={encounterStatusList} /> - diff --git a/src/components/Patient/PatientPMSI/PatientPMSI.tsx b/src/components/Patient/PatientPMSI/index.tsx similarity index 94% rename from src/components/Patient/PatientPMSI/PatientPMSI.tsx rename to src/components/Patient/PatientPMSI/index.tsx index 727872813..c012b9a60 100644 --- a/src/components/Patient/PatientPMSI/PatientPMSI.tsx +++ b/src/components/Patient/PatientPMSI/index.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react' +import React, { useEffect, useMemo, useRef, useState } from 'react' import { CircularProgress, Grid, Tooltip } from '@mui/material' import Chip from 'components/ui/Chip' @@ -23,7 +23,6 @@ import { PMSILabel } from 'types/patient' import { selectFiltersAsArray } from 'utils/filters' import { BlockWrapper } from 'components/ui/Layout' import useSearchCriterias, { initPmsiSearchCriterias } from 'reducers/searchCriteriasReducer' -import services from 'services/aphp' import CodeFilter from 'components/Filters/CodeFilter' import DatesRangeFilter from 'components/Filters/DatesRangeFilter' import DiagnosticTypesFilter from 'components/Filters/DiagnosticTypesFilter' @@ -36,12 +35,14 @@ import { Save, SavedSearch } from '@mui/icons-material' import TextInput from 'components/Filters/TextInput' import { mapToAttribute, mapToLabel, mapToSourceType } from 'mappers/pmsi' import List from 'components/ui/List' -import { fetchClaimCodes, fetchConditionCodes, fetchProcedureCodes } from 'services/aphp/servicePmsi' import EncounterStatusFilter from 'components/Filters/EncounterStatusFilter' import { AlertWrapper } from 'components/ui/Alert' -import { Hierarchy } from 'types/hierarchy' import { useSearchParams } from 'react-router-dom' import { checkIfPageAvailable, handlePageError } from 'utils/paginationUtils' +import { FhirItem } from 'types/hierarchy' +import { getConfig } from 'config' +import { getCodeList } from 'services/aphp/serviceValueSets' +import { getValueSetsFromSystems } from 'utils/valueSets' type PatientPMSIProps = { groupId?: string @@ -72,7 +73,7 @@ const PatientPMSI = ({ groupId }: PatientPMSIProps) => { const [toggleFilterInfoModal, setToggleFilterInfoModal] = useState(false) const [isReadonlyFilterInfoModal, setIsReadonlyFilterInfoModal] = useState(true) const [triggerClean, setTriggerClean] = useState(false) - const [encounterStatusList, setEncounterStatusList] = useState[]>([]) + const [encounterStatusList, setEncounterStatusList] = useState([]) const dispatch = useAppDispatch() const [selectedTab, setSelectedTab] = useState({ @@ -114,7 +115,7 @@ const PatientPMSI = ({ groupId }: PatientPMSIProps) => { [code, nda, diagnosticTypes, source, startDate, endDate, executiveUnits, encounterStatus] ) - const [allDiagnosticTypesList, setAllDiagnosticTypesList] = useState[]>([]) + const [allDiagnosticTypesList, setAllDiagnosticTypesList] = useState([]) const [loadingStatus, setLoadingStatus] = useState(LoadingStatus.FETCHING) const patient = useAppSelector((state) => state.patient) const [searchResults, setSearchResults] = useState({ @@ -168,11 +169,11 @@ const PatientPMSI = ({ groupId }: PatientPMSIProps) => { const fetch = async () => { try { const [diagnosticTypes, encounterStatus] = await Promise.all([ - services.cohortCreation.fetchDiagnosticTypes(), - services.cohortCreation.fetchEncounterStatus() + getCodeList(getConfig().features.condition.valueSets.conditionStatus.url), + getCodeList(getConfig().core.valueSets.encounterStatus.url) ]) - setAllDiagnosticTypesList(diagnosticTypes) - setEncounterStatusList(encounterStatus) + setAllDiagnosticTypesList(diagnosticTypes.results) + setEncounterStatusList(encounterStatus.results) } catch (e) { /* empty */ } @@ -230,14 +231,14 @@ const PatientPMSI = ({ groupId }: PatientPMSIProps) => { }) }, [patient, selectedTab.id]) - const fetchCodes = useCallback(() => { + const references = useMemo(() => { switch (selectedTab.id) { case ResourceType.CONDITION: - return fetchConditionCodes + return getValueSetsFromSystems([getConfig().features.condition.valueSets.conditionHierarchy.url]) case ResourceType.PROCEDURE: - return fetchProcedureCodes + return getValueSetsFromSystems([getConfig().features.procedure.valueSets.procedureHierarchy.url]) default: - return fetchClaimCodes + return getValueSetsFromSystems([getConfig().features.claim.valueSets.claimHierarchy.url]) } }, [selectedTab.id]) @@ -352,7 +353,7 @@ const PatientPMSI = ({ groupId }: PatientPMSIProps) => { onClean={triggerClean} > {!searchResults.deidentified && } - + {selectedTab.id === ResourceType.CONDITION && ( { )} {selectedTab.id === ResourceType.CONDITION && ( diff --git a/src/components/Patient/PatientPreview/PatientPreview.tsx b/src/components/Patient/PatientPreview/PatientPreview.tsx index 87be50c9b..67c4b861c 100644 --- a/src/components/Patient/PatientPreview/PatientPreview.tsx +++ b/src/components/Patient/PatientPreview/PatientPreview.tsx @@ -5,16 +5,28 @@ import { Grid, Paper } from '@mui/material' import PatientField from './PatientField/PatientField' import { getAge } from 'utils/age' -import { getLastDiagnosisLabels } from 'utils/pmsi' import { CohortPatient, IPatientDetails } from 'types' import useStyles from './styles' +import { Condition } from 'fhir/r4' + +export const getLastDiagnosisLabels = (mainDiagnosisList: Condition[]) => { + const mainDiagnosisLabels = mainDiagnosisList.map((diagnosis) => diagnosis.code?.coding?.[0].display) + const lastThreeDiagnosisLabels = mainDiagnosisLabels + .filter((diagnosis, index) => mainDiagnosisLabels.indexOf(diagnosis) === index) + .slice(0, 3) + .join(' - ') + + return lastThreeDiagnosisLabels +} type PatientPreviewProps = { patient?: IPatientDetails deidentifiedBoolean: boolean } const PatientPreview: React.FC = ({ patient, deidentifiedBoolean }) => { + + const { classes } = useStyles() if (!patient) return <> diff --git a/src/components/Patient/PatientTimeline/FilterTimelineDialog/FilterTimelineDialog.tsx b/src/components/Patient/PatientTimeline/FilterTimelineDialog/FilterTimelineDialog.tsx index 8e3201afa..eddc284d2 100644 --- a/src/components/Patient/PatientTimeline/FilterTimelineDialog/FilterTimelineDialog.tsx +++ b/src/components/Patient/PatientTimeline/FilterTimelineDialog/FilterTimelineDialog.tsx @@ -13,20 +13,18 @@ import { } from '@mui/material' import { capitalizeFirstLetter } from 'utils/capitalize' - import useStyles from './styles' -import { LabelObject } from 'types/searchCriterias' -import { Hierarchy } from 'types/hierarchy' +import { FhirItem } from 'types/hierarchy' type FilterTimelineDialogProps = { open: boolean onClose: () => void - diagnosticTypesList: Hierarchy[] - selectedDiagnosticTypes: LabelObject[] - onChangeSelectedDiagnosticTypes: (selectedDiagnosticTypes: LabelObject[]) => void - encounterStatusList: Hierarchy[] - encounterStatus: LabelObject[] - onChangeEncounterStatus: (encounterStatus: LabelObject[]) => void + diagnosticTypesList: FhirItem[] + selectedDiagnosticTypes: FhirItem[] + onChangeSelectedDiagnosticTypes: (selectedDiagnosticTypes: FhirItem[]) => void + encounterStatusList: FhirItem[] + encounterStatus: FhirItem[] + onChangeEncounterStatus: (encounterStatus: FhirItem[]) => void } const FilterTimelineDialog: React.FC = ({ open, @@ -40,10 +38,10 @@ const FilterTimelineDialog: React.FC = ({ }) => { const { classes } = useStyles() - const [_selectedDiagnosticTypes, setSelectedDiagnosticTypes] = useState(selectedDiagnosticTypes) - const [_encounterStatus, setEncounterStatus] = useState(encounterStatus) + const [_selectedDiagnosticTypes, setSelectedDiagnosticTypes] = useState(selectedDiagnosticTypes) + const [_encounterStatus, setEncounterStatus] = useState(encounterStatus) - const _onChangeSelectedDiagnosticTypes = (event: React.ChangeEvent<{}>, value: LabelObject[]) => { + const _onChangeSelectedDiagnosticTypes = (event: React.ChangeEvent<{}>, value: FhirItem[]) => { setSelectedDiagnosticTypes(value) } @@ -93,8 +91,8 @@ const FilterTimelineDialog: React.FC = ({ options={encounterStatusList} value={_encounterStatus} disableCloseOnSelect - getOptionLabel={(encounterStatus: LabelObject) => encounterStatus.label} - renderOption={(props, encounterStatus: LabelObject) =>
  • {encounterStatus.label}
  • } + getOptionLabel={(encounterStatus: FhirItem) => encounterStatus.label} + renderOption={(props, encounterStatus: FhirItem) =>
  • {encounterStatus.label}
  • } renderInput={(params) => ( )} diff --git a/src/components/Patient/PatientTimeline/PatientTimeline.tsx b/src/components/Patient/PatientTimeline/PatientTimeline.tsx index ad84d4c6f..b752abced 100644 --- a/src/components/Patient/PatientTimeline/PatientTimeline.tsx +++ b/src/components/Patient/PatientTimeline/PatientTimeline.tsx @@ -29,9 +29,10 @@ import services from 'services/aphp' import useStyles from './styles' import { Condition, DocumentReference, Encounter, Period, Procedure } from 'fhir/r4' import { FilterKeys, LabelObject } from 'types/searchCriterias' -import { Hierarchy } from 'types/hierarchy' import { getExtension } from 'utils/fhir' import { getConfig } from 'config' +import { FhirItem, HierarchyElementWithSystem } from 'types/hierarchy' +import { getCodeList } from 'services/aphp/serviceValueSets' const dateFormat = 'YYYY-MM-DD' @@ -166,10 +167,10 @@ const PatientTimeline: React.FC = ({ const [dialogDocuments, setDialogDocuments] = useState([]) const [openFilter, setOpenFilter] = useState(false) - const [selectedTypes, setSelectedTypes] = useState[]>([]) - const [encounterStatus, setEncounterStatus] = useState([]) - const [diagnosticTypesList, setDiagnosticTypesList] = useState[]>([]) - const [encounterStatusList, setEncounterStatusList] = useState[]>([]) + const [selectedTypes, setSelectedTypes] = useState([]) + const [encounterStatus, setEncounterStatus] = useState([]) + const [diagnosticTypesList, setDiagnosticTypesList] = useState([]) + const [encounterStatusList, setEncounterStatusList] = useState([]) const [loading, setLoading] = useState(false) const yearComponentSize: { [year: number]: number } = {} @@ -192,16 +193,16 @@ const PatientTimeline: React.FC = ({ useEffect(() => { const _fetch = async () => { const [diagnosticTypes, encounterStatus] = await Promise.all([ - services.cohortCreation.fetchDiagnosticTypes(), - services.cohortCreation.fetchEncounterStatus() + getCodeList(getConfig().features.condition.valueSets.conditionStatus.url), + getCodeList(getConfig().core.valueSets.encounterStatus.url) ]) if (!diagnosticTypes) return - setEncounterStatusList(encounterStatus) + setEncounterStatusList(encounterStatus.results) // Find main diagnosis - const foundItem = diagnosticTypes.find((diagnosticTypes) => diagnosticTypes.id === 'dp') + const foundItem = diagnosticTypes.results.find((diagnosticTypes) => diagnosticTypes.id === 'dp') foundItem && setSelectedTypes([foundItem]) - setDiagnosticTypesList(diagnosticTypes) + setDiagnosticTypesList(diagnosticTypes.results) } _fetch() diff --git a/src/components/Routes/AppNavigation/config.tsx b/src/components/Routes/AppNavigation/config.tsx index 24fd76cb2..7b715b6de 100644 --- a/src/components/Routes/AppNavigation/config.tsx +++ b/src/components/Routes/AppNavigation/config.tsx @@ -5,8 +5,8 @@ import Login from 'views/Login/Login' import HealthCheck from 'views/HealthCheck/HealthCheck' import Welcome from 'views/Welcome/Welcome' import SearchPatient from 'views/SearchPatient/SearchPatient' -import Patient from 'views/Patient/Patient' -import Dashboard from 'views/Dashboard/Dashboard' +import Patient from 'views/Patient' +import Dashboard from 'views/Dashboard' import CohortCreation from 'views/CohortCreation/CohortCreation' import PageNotFound from 'views/PageNotFound/PageNotFound' import CareSiteView from 'views/Scope/CareSiteView' diff --git a/src/components/ScopeTree/ScopeTreeTable.tsx b/src/components/ScopeTree/ScopeTreeTable.tsx index 9b15e45ca..d992e92a3 100644 --- a/src/components/ScopeTree/ScopeTreeTable.tsx +++ b/src/components/ScopeTree/ScopeTreeTable.tsx @@ -4,7 +4,6 @@ import { Checkbox, CircularProgress, Grid, - ListItem, Table, TableBody, TableCell, @@ -14,29 +13,28 @@ import { Typography } from '@mui/material' import { LoadingStatus, ScopeElement, SelectedStatus } from 'types' -import { Hierarchy } from 'types/hierarchy' +import { Hierarchy, HierarchyInfo, SearchMode } from 'types/hierarchy' import { IndeterminateCheckBoxOutlined, KeyboardArrowDown, KeyboardArrowRight } from '@mui/icons-material' import servicesPerimeters from 'services/aphp/servicePerimeters' import displayDigit from 'utils/displayDigit' -import useStyles from './styles' +import { CellWrapper, RowContainerWrapper, RowWrapper } from '../Hierarchy/styles' import { SourceType } from 'types/scope' -import { sort } from 'utils/arrays' import { v4 as uuidv4 } from 'uuid' import { isSourceTypeInScopeLevel } from 'utils/perimeters' import { every } from 'lodash' +import { sortArray } from 'utils/arrays' type HierarchyItemProps = { item: Hierarchy path: string[] - searchMode: boolean + mode: SearchMode sourceType: SourceType loading: { search: LoadingStatus; expand: LoadingStatus } onSelect: (node: Hierarchy, toAdd: boolean) => void onExpand: (node: Hierarchy) => void } -const ScopeTreeRow = ({ item, path, sourceType, searchMode, loading, onSelect, onExpand }: HierarchyItemProps) => { - const { classes } = useStyles() +const ScopeTreeRow = ({ item, path, sourceType, mode, loading, onSelect, onExpand }: HierarchyItemProps) => { const [open, setOpen] = useState(false) const [internalLoading, setInternalLoading] = useState(false) const { id, name, subItems, rights, status, source_value, type, cohort_size, full_path } = item @@ -56,80 +54,89 @@ const ScopeTreeRow = ({ item, path, sourceType, searchMode, loading, onSelect, o }, [loading.expand]) return ( - - - - 1 ? { marginLeft: path.length * 24 - 24 + 'px' } : { margin: '0' }} - > - {canExpand && ( - <> - {internalLoading && } - {!internalLoading && ( - <> - {open && setOpen(false)} />} - {!open && } - - )} - - )} - - - - } - onChange={(event, checked) => onSelect(item, checked)} - inputProps={{ 'aria-labelledby': name }} - /> - - {searchMode && full_path && ( - - - {(full_path.split('/').length > 1 ? full_path.split('/').slice(1) : full_path.split('/').slice(0)).map( - (full_path: string) => ( - - {full_path} - - ) + <> + + 1 ? path.length * 20 - 20 + 'px' : '0'} + > + + <> + {internalLoading && } + {!internalLoading && ( + <> + {open && setOpen(false)} color="secondary" />} + {!open && } + )} - - - )} - {!searchMode && ( - - + + + {mode === SearchMode.RESEARCH && full_path && ( + + + {(full_path.split('/').length > 1 ? full_path.split('/').slice(1) : full_path.split('/').slice(0)).map( + (full_path: string, index, arr) => { + const last = index === arr.length - 1 + return ( + + {full_path} + + ) + } + )} + + + )} + {mode === SearchMode.EXPLORATION && ( + (open ? setOpen(false) : handleOpen())} - >{`${source_value} - ${name}`} - - )} - - (open ? setOpen(false) : handleOpen())}>{displayDigit(+cohort_size)} - - {sourceType === SourceType.ALL && ( - - {rights && {servicesPerimeters.getAccessFromRights(rights)}} - - )} - {sourceType !== SourceType.ALL && ( - - {type} - - )} - + fontSize={12.5} + fontWeight={700} + > + {`${source_value} - ${name}`} + + )} + + {displayDigit(+cohort_size)} + + {sourceType === SourceType.ALL && ( + + {rights && {servicesPerimeters.getAccessFromRights(rights)}} + + )} + {sourceType !== SourceType.ALL && ( + + {type} + + )} + + } + onChange={(event, checked) => onSelect(item, checked)} + inputProps={{ 'aria-labelledby': name }} + /> + + + {!internalLoading && open && - sort(subItems || [], 'source_value').map((subItem: Hierarchy) => { + sortArray(subItems || [], 'source_value').map((subItem: Hierarchy) => { if (isSourceTypeInScopeLevel(sourceType, subItem.type)) { return ( + ) } type HierarchyProps = { - hierarchy: Hierarchy[] - searchMode: boolean + hierarchy: HierarchyInfo + mode: SearchMode selectAllStatus: SelectedStatus sourceType: SourceType loading: { search: LoadingStatus; expand: LoadingStatus } @@ -155,7 +162,7 @@ type HierarchyProps = { const ScopeTreeTable = ({ hierarchy, - searchMode, + mode, selectAllStatus, sourceType, loading, @@ -163,15 +170,22 @@ const ScopeTreeTable = ({ onSelectAll, onExpand }: HierarchyProps) => { - const { classes } = useStyles() - return ( - - - + + + + Nom + + + Nombre de patients + + + {sourceType === SourceType.ALL ? 'Accès' : 'Type'} + + } onChange={(event, checked) => onSelectAll(checked)} /> - - - Nom - - - - Nombre de patients - - - - {sourceType === SourceType.ALL ? 'Accès' : 'Type'} - - + + - {loading.search === LoadingStatus.SUCCESS && !hierarchy.length && ( + {loading.search === LoadingStatus.SUCCESS && !hierarchy.tree.length && ( Aucun résultat à afficher )} - {loading.search !== LoadingStatus.FETCHING && - hierarchy.map((item) => { - if (!item) return

    Missing

    - return ( - - ) - })} + {loading.search !== LoadingStatus.FETCHING && ( +
    + {hierarchy.tree.map((item) => + item ? ( + + ) : ( +

    Missing

    + ) + )} +
    + )}
    {loading.search === LoadingStatus.FETCHING && ( diff --git a/src/components/ScopeTree/SelectedCodes.tsx b/src/components/ScopeTree/SelectedCodes.tsx deleted file mode 100644 index 74a22241c..000000000 --- a/src/components/ScopeTree/SelectedCodes.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import React, { useState } from 'react' - -import { Collapse, Grid, IconButton, Typography } from '@mui/material' -import Chip from 'components/ui/Chip' -import { KeyboardArrowDown, KeyboardArrowUp } from '@mui/icons-material' -import { ScopeElement } from 'types' -import { Hierarchy } from 'types/hierarchy' - -type SelectedCodesProps = { - values: Hierarchy[] - onDelete: (hierarchyElement: Hierarchy) => void -} - -const SelectedCodes = ({ values, onDelete }: SelectedCodesProps) => { - const [openSelectedCodesDrawer, setOpenSelectedCodesDrawer] = useState(false) - - return ( - - - - - {values?.length} sélectionné(s) - - - - {values.length > 0 && ( - setOpenSelectedCodesDrawer((prev) => !prev)}> - {openSelectedCodesDrawer ? : } - - )} - - - - - {values?.length > 0 && ( - - {values.map((code) => ( - onDelete(code)} - /> - ))} - - )} - - - - ) -} - -export default SelectedCodes diff --git a/src/components/ScopeTree/index.tsx b/src/components/ScopeTree/index.tsx index cd287c0ef..50241e336 100644 --- a/src/components/ScopeTree/index.tsx +++ b/src/components/ScopeTree/index.tsx @@ -1,17 +1,15 @@ -import React, { useCallback, useEffect } from 'react' -import { useAppDispatch, useAppSelector } from 'state' -import { ScopeElement } from 'types' +import React, { useEffect } from 'react' +import { LoadingStatus, ScopeElement } from 'types' import SearchInput from 'components/ui/Searchbar/SearchInput' -import { Grid, Pagination, Paper } from '@mui/material' -import { useHierarchy } from '../../hooks/hierarchy/useHierarchy' -import servicesPerimeters from '../../services/aphp/servicePerimeters' -import SelectedCodes from './SelectedCodes' -import { useSearchParameters } from 'hooks/useSearchParameters' +import { Grid, Paper } from '@mui/material' +import SelectedCodes from '../Hierarchy/SelectedCodes' import { SourceType } from 'types/scope' import { Hierarchy } from 'types/hierarchy' import ScopeTreeTable from './ScopeTreeTable' -import { saveFetchedPerimeters, saveFetchedRights } from 'state/scope' -import { cleanNodes } from 'utils/hierarchy' +import { useScopeTree } from 'hooks/scopeTree/useScopeTree' +import { Pagination } from 'components/ui/Pagination' +import { LIMIT_PER_PAGE } from 'hooks/search/useSearchParameters' +import { cleanNode } from 'utils/hierarchy/hierarchy' type ScopeTreeProps = { baseTree: Hierarchy[] @@ -21,107 +19,68 @@ type ScopeTreeProps = { } const ScopeTree = ({ baseTree, selectedNodes, sourceType, onSelect }: ScopeTreeProps) => { - const practitionerId = useAppSelector((state) => state.me)?.id || '' - const codes = useAppSelector((state) => - sourceType === SourceType.ALL ? state.scope.codes.rights : state.scope.codes.perimeters - ) - const dispatch = useAppDispatch() - const { options, onChangeSearchInput, onChangePage, onChangeCount, onChangeSearchMode } = useSearchParameters() - const fetchChildren = useCallback( - async (ids: string) => { - const { results } = - sourceType === SourceType.ALL - ? await servicesPerimeters.getRights({ practitionerId, ids, limit: -1, sourceType }) - : await servicesPerimeters.getPerimeters({ practitionerId, ids, limit: -1, sourceType }) - return results - }, - [practitionerId, sourceType] - ) - - const fetchSearch = useCallback( - async (search: string, page: number) => { - const { results, count } = - sourceType === SourceType.ALL - ? await servicesPerimeters.getRights({ practitionerId, search, page, limit: options.limit, sourceType }) - : await servicesPerimeters.getPerimeters({ practitionerId, search, page, limit: options.limit, sourceType }) - onChangeCount(count) - return results - }, - [practitionerId, sourceType] - ) - - const handleSaveCodes = useCallback((codes: Hierarchy[]) => { - if (sourceType === SourceType.ALL) dispatch(saveFetchedRights(cleanNodes(codes))) - else dispatch(saveFetchedPerimeters(cleanNodes(codes))) - }, []) - - const { hierarchy, selectedCodes, loadingStatus, selectAllStatus, search, expand, select, selectAll, deleteCode } = - useHierarchy(baseTree, selectedNodes, codes, handleSaveCodes, fetchChildren) - - const handleSearch = (searchValue: string, page: number) => { - if (searchValue === '') onChangeCount(baseTree.length) - onChangeSearchInput(searchValue) - onChangePage(page) - onChangeSearchMode(searchValue !== '') - search(searchValue, page, fetchSearch) - } + const { + hierarchyData: { hierarchy, loadingStatus, selectAllStatus, selectedCodes }, + hierarchyActions: { expand, select, selectAll }, + parametersData: { searchInput, mode }, + parametersActions: { onChangePage, onChangeSearchInput } + } = useScopeTree(baseTree, selectedNodes, sourceType) useEffect(() => { - onSelect(cleanNodes(selectedCodes)) + onSelect(selectedCodes.map((e) => cleanNode(e))) }, [selectedCodes]) return ( - - - - handleSearch(newValue, 0)} - /> - - - + <> + + + + + + + - - - - - - {options.totalPages > 1 && ( - - - - handleSearch(options.search, page - 1)} - page={options.page + 1} - /> - - + + + + - )} + - +
    + {loadingStatus.search === LoadingStatus.SUCCESS && hierarchy.count / LIMIT_PER_PAGE > 1 && ( + + + + )} + + select(code, false)} /> + +
    + ) } diff --git a/src/components/ScopeTree/styles.ts b/src/components/ScopeTree/styles.ts deleted file mode 100644 index 49c6c80a4..000000000 --- a/src/components/ScopeTree/styles.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { makeStyles } from 'tss-react/mui' - -const useStyles = makeStyles()(() => ({ - expandCell: { - padding: '16px 4px 16px 16px' - }, - checkbox: { - padding: '8px 0' - }, - secondRow: { - background: '#f3f5f9' - }, - expandIcon: { - padding: '0 0 0 8px' - }, - tableHead: { - height: 42, - backgroundColor: '#D1E2F4', - textTransform: 'uppercase' - }, - tableHeadCell: { - fontSize: 11, - fontWeight: 'bold', - color: '#0063AF' - }, - emptyTableHeadCell: { - width: '42px', - fontSize: 11, - fontWeight: 'bold', - color: '#0063AF', - padding: 0 - } -})) - -export default useStyles diff --git a/src/components/SearchValueSet/References.tsx b/src/components/SearchValueSet/References.tsx new file mode 100644 index 000000000..eac92d5b2 --- /dev/null +++ b/src/components/SearchValueSet/References.tsx @@ -0,0 +1,57 @@ +import React from 'react' + +import { Checkbox, FormControlLabel, Grid, Radio } from '@mui/material' +import { Reference, References } from 'types/valueSet' +import { Check, Warning } from '@mui/icons-material' + +export enum Type { + SINGLE, + MULTIPLE +} + +type ReferencesProps = { + type: Type + values: Reference[] + disabled?: boolean + onSelect: (ref: Reference[]) => void +} + +const ReferencesParameters = ({ values, type, disabled = false, onSelect }: ReferencesProps) => { + const handleSelectReference = (id: References) => { + const newReferences = values.map((ref) => ({ + ...ref, + checked: type === Type.SINGLE ? id === ref.id : id === ref.id ? !ref.checked : ref.checked + })) + onSelect(newReferences) + } + + return ( + <> + {values.map((ref) => ( + handleSelectReference(ref.id)} /> + ) : ( + handleSelectReference(ref.id)} /> + ) + } + label={ + + {ref.label} + {ref.standard ? ( + + ) : ( + + )} + + } + /> + ))} + + ) +} + +export default ReferencesParameters diff --git a/src/components/SearchValueSet/ValueSetField.tsx b/src/components/SearchValueSet/ValueSetField.tsx new file mode 100644 index 000000000..487e96527 --- /dev/null +++ b/src/components/SearchValueSet/ValueSetField.tsx @@ -0,0 +1,74 @@ +import { FormLabel, Grid, IconButton } from '@mui/material' +import React, { useState } from 'react' +import { FhirItem, Hierarchy } from 'types/hierarchy' +import { Reference } from 'types/valueSet' +import SearchValueSet from '.' +import Panel from 'components/ui/Panel' +import { SearchOutlined } from '@mui/icons-material' +import CloseIcon from '@mui/icons-material/Close' +import MoreHorizIcon from '@mui/icons-material/MoreHoriz' +import CodesWithSystems from 'components/Hierarchy/CodesWithSystems' + +type ValueSetFieldProps = { + value: Hierarchy[] + references: Reference[] + placeholder: string + disabled?: boolean + onSelect: (selectedItems: Hierarchy[]) => void +} + +const ValueSetField = ({ value, references, placeholder, disabled = false, onSelect }: ValueSetFieldProps) => { + const [openCodeResearch, setOpenCodeResearch] = useState(false) + const [isExtended, setIsExtended] = useState(false) + + const handleDelete = (node: Hierarchy) => { + const newCodes = value.filter((item) => item.id !== node.id) + onSelect(newCodes) + } + + return ( + <> + + + + {!value.length && {placeholder}} + + + {isExtended && value.length > 0 && ( + setIsExtended(false)}> + + + )} + {!isExtended && value.length > 0 && ( + setIsExtended(true)}> + + + )} + setOpenCodeResearch(true)} + disabled={disabled} + > + + + + + setOpenCodeResearch(false)} + onConfirm={() => setOpenCodeResearch(false)} + open={openCodeResearch} + > + + + + ) +} + +export default ValueSetField diff --git a/src/components/SearchValueSet/ValueSetTable.tsx b/src/components/SearchValueSet/ValueSetTable.tsx new file mode 100644 index 000000000..53da259a1 --- /dev/null +++ b/src/components/SearchValueSet/ValueSetTable.tsx @@ -0,0 +1,226 @@ +import React, { useEffect, useState } from 'react' + +import { + Checkbox, + CircularProgress, + Grid, + Paper, + Table, + TableBody, + TableCell, + TableContainer, + TableRow, + Typography +} from '@mui/material' +import { LoadingStatus, SelectedStatus } from 'types' +import { FhirHierarchy, FhirItem, Hierarchy, HierarchyInfo, Mode, SearchMode } from 'types/hierarchy' +import { KeyboardArrowDown, KeyboardArrowRight, IndeterminateCheckBoxOutlined } from '@mui/icons-material' +import { CellWrapper, RowContainerWrapper, RowWrapper } from '../Hierarchy/styles' +import { sortArray } from 'utils/arrays' +import { v4 as uuidv4 } from 'uuid' +import { LIMIT_PER_PAGE } from 'hooks/search/useSearchParameters' +import { Pagination } from 'components/ui/Pagination' +import { getLabelFromCode, isDisplayedWithCode } from 'utils/valueSets' + +type ValueSetRowProps = { + item: Hierarchy + loading: { expand: LoadingStatus; list: LoadingStatus } + isSelectionDisabled: (node: Hierarchy) => boolean + path: string[] + mode: SearchMode + isHierarchy: boolean + onExpand: (node: Hierarchy) => void + onSelect: ( + nodes: Hierarchy[], + mode: Mode.SELECT | Mode.UNSELECT | Mode.SELECT_ALL | Mode.UNSELECT_ALL + ) => void +} + +const ValueSetRow = ({ + item, + loading, + isSelectionDisabled, + path, + mode, + isHierarchy, + onSelect, + onExpand +}: ValueSetRowProps) => { + const [open, setOpen] = useState(false) + const [internalLoading, setInternalLoading] = useState(false) + const { label, subItems, status, id } = item + //const canExpand = !(subItems?.length === 0 || subItems) + + const handleOpen = () => { + setOpen(true) + setInternalLoading(true) + onExpand(item) + } + + useEffect(() => { + if (loading.expand === LoadingStatus.SUCCESS) setInternalLoading(false) + }, [loading.expand]) + + return ( + <> + + 1 ? path.length * 20 - 20 + 'px' : '0'}> + + {mode === SearchMode.EXPLORATION && isHierarchy && ( + <> + {internalLoading && } + {!internalLoading && ( + <> + {open && setOpen(false)} color="info" />} + {!open && } + + )} + + )} + + (open ? setOpen(false) : handleOpen())}> + {getLabelFromCode(item)} + + + } + onChange={(event, checked) => onSelect([item], checked ? Mode.SELECT : Mode.UNSELECT)} + color="info" + inputProps={{ 'aria-labelledby': label }} + /> + + + + {!internalLoading && + open && + isHierarchy && + sortArray(subItems || [], isDisplayedWithCode(item.system) ? 'id' : 'label').map((subItem) => { + return ( + + ) + })} + + ) +} + +type ValueSetTableProps = { + hierarchy: HierarchyInfo + selectAllStatus: SelectedStatus + loading: { expand: LoadingStatus; list: LoadingStatus } + isHierarchy?: boolean + mode: SearchMode + isSelectionDisabled?: (node: Hierarchy) => boolean + onExpand: (node: Hierarchy) => void + onSelect: ( + nodes: Hierarchy[], + mode: Mode.SELECT | Mode.UNSELECT | Mode.SELECT_ALL | Mode.UNSELECT_ALL + ) => void + onChangePage: (page: number) => void +} + +const ValueSetTable = ({ + hierarchy, + selectAllStatus, + loading, + mode, + isHierarchy = true, + isSelectionDisabled = () => false, + onSelect, + onExpand, + onChangePage +}: ValueSetTableProps) => { + + const handleSelect = (checked: boolean) => { + if (mode === SearchMode.RESEARCH) { + const notDisabled = hierarchy.tree.filter((node) => !isSelectionDisabled(node)) + onSelect(notDisabled, checked ? Mode.SELECT : Mode.UNSELECT) + } else onSelect(hierarchy.tree, checked ? Mode.SELECT_ALL : Mode.UNSELECT_ALL) + } + + return ( + + + + + + {loading.list === LoadingStatus.SUCCESS && !isHierarchy && ( + + + + + {hierarchy.count ? `${hierarchy.count} résultat(s)` : ` Aucun résultat à afficher`} + + !isSelectionDisabled(node)).length + } + checked={selectAllStatus === SelectedStatus.SELECTED} + indeterminate={selectAllStatus === SelectedStatus.INDETERMINATE} + indeterminateIcon={} + onChange={(event, checked) => handleSelect(checked)} + /> + + + + )} + {loading.list === LoadingStatus.SUCCESS && ( +
    + {hierarchy.tree.map((item) => + item ? ( + + ) : ( +

    Missing

    + ) + )} +
    + )} +
    +
    + {loading.list === LoadingStatus.FETCHING && ( + + + + )} + {!isHierarchy && + loading.list === LoadingStatus.SUCCESS && + Math.ceil(hierarchy.count / LIMIT_PER_PAGE) > 1 && ( + + onChangePage(page)} + color="#0063AF" + /> + + )} +
    +
    +
    + ) +} + +export default ValueSetTable diff --git a/src/components/SearchValueSet/index.tsx b/src/components/SearchValueSet/index.tsx new file mode 100644 index 000000000..ed23a046c --- /dev/null +++ b/src/components/SearchValueSet/index.tsx @@ -0,0 +1,134 @@ +import React, { useEffect } from 'react' +import { Grid, Divider, Paper, Collapse, Typography, Input, IconButton } from '@mui/material' +import Tabs from 'components/ui/Tabs' +import { LoadingStatus, TabType } from 'types' +import ReferencesParameters, { Type } from './References' +import ValueSetTable from './ValueSetTable' +import { Reference } from 'types/valueSet' +import SelectedCodes from 'components/Hierarchy/SelectedCodes' +import { useSearchValueSet } from 'hooks/valueSet/useSearchValueSet' +import { Displayer } from 'components/ui/Displayer/styles' +import ClearIcon from '@mui/icons-material/Clear' +import { FhirItem, Hierarchy, Mode, SearchMode, SearchModeLabel } from 'types/hierarchy' +import { cleanNode } from 'utils/hierarchy/hierarchy' +import { HIERARCHY_ROOT } from 'services/aphp/serviceValueSets' + +type SearchValueSetProps = { + references: Reference[] + selectedNodes: Hierarchy[] + onSelect: (selectedItems: Hierarchy[]) => void +} + +const SearchValueSet = ({ references, selectedNodes, onSelect }: SearchValueSetProps) => { + const { + mode, + searchInput, + onChangeMode, + onDelete, + selectedCodes, + isSelectionDisabled, + loadingStatus, + parameters: { refs, onChangeReferences, onChangeSearchInput, onChangePage }, + hierarchy: { exploration, research, selectAllStatus, expand, select } + } = useSearchValueSet(references, selectedNodes) + + const tabs: TabType[] = [ + { id: SearchMode.EXPLORATION, label: SearchModeLabel.EXPLORATION }, + { id: SearchMode.RESEARCH, label: SearchModeLabel.RESEARCH } + ] + + useEffect(() => { + onSelect(selectedCodes.map((e) => cleanNode(e))) + }, [selectedCodes]) + + return ( + <> + + + + onChangeMode(elem.id)} + /> + + + + + + + + Référentiels : + + + + + ref.checked)}> + + onChangeSearchInput(event.target.value)} + endAdornment={ + onChangeSearchInput('')}> + + + } + /> + + + + + + + + + + ref.checked)?.isHierarchy} + loading={{ list: loadingStatus.init, expand: loadingStatus.expand }} + hierarchy={exploration} + selectAllStatus={selectAllStatus} + isSelectionDisabled={isSelectionDisabled} + onSelect={select} + onExpand={expand} + onChangePage={onChangePage} + /> + + + + + + +
    + + + +
    + + ) +} + +export default SearchValueSet diff --git a/src/components/ui/Chip/Chips.tsx b/src/components/ui/Chip/Chips.tsx deleted file mode 100644 index e10b400db..000000000 --- a/src/components/ui/Chip/Chips.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import React from 'react' - -import Grid from '@mui/material/Grid' -import Chip from '@mui/material/Chip' - -import { useStyles } from './styles' - -export type ChipsProps = { - value: { - id: T - label: TL - onDelete: (value: T) => void - }[] -} -const Chips = ({ value }: ChipsProps) => { - const { classes } = useStyles() - - return ( - - {value?.length > 0 && - value.map(({ label, id, onDelete }) => ( - - ))} - - ) -} - -export default Chips diff --git a/src/components/ui/Chip/index.tsx b/src/components/ui/Chip/index.tsx index 9de6750bb..26a8ab49d 100644 --- a/src/components/ui/Chip/index.tsx +++ b/src/components/ui/Chip/index.tsx @@ -7,7 +7,7 @@ type ChipProps = { onDelete: () => void } const Chip = ({ label, style, onDelete }: ChipProps) => { - return onDelete()} /> + return } export default Chip diff --git a/src/components/ui/Displayer/styles.ts b/src/components/ui/Displayer/styles.ts new file mode 100644 index 000000000..1186b57fb --- /dev/null +++ b/src/components/ui/Displayer/styles.ts @@ -0,0 +1,9 @@ +import { Grid, styled } from '@mui/material' + +type DisplayerProps = { + isDisplayed: boolean +} + +export const Displayer = styled(Grid)(({ isDisplayed }) => ({ + display: isDisplayed ? 'block' : 'none' +})) diff --git a/src/components/ui/Inputs/AsyncAutocomplete/index.tsx b/src/components/ui/Inputs/AsyncAutocomplete/index.tsx deleted file mode 100644 index 64af8b42b..000000000 --- a/src/components/ui/Inputs/AsyncAutocomplete/index.tsx +++ /dev/null @@ -1,106 +0,0 @@ -import React, { useEffect, useState, Fragment, useRef, SyntheticEvent, ReactNode } from 'react' - -import { Autocomplete, CircularProgress, TextField } from '@mui/material' -import { cancelPendingRequest } from 'utils/abortController' -import { LabelObject } from 'types/searchCriterias' -import { useDebounce } from 'utils/debounce' - -type AsyncAutocompleteProps = { - variant?: 'standard' | 'filled' | 'outlined' - label?: ReactNode - className?: string - values?: LabelObject[] - noOptionsText?: string - helperText?: string - disabled?: boolean - onFetch: (options: string, signal: AbortSignal) => Promise - onChange: (elem: LabelObject[]) => void -} - -const AsyncAutocomplete = ({ - variant, - label, - className, - values = [], - noOptionsText, - helperText, - disabled = false, - onChange, - onFetch -}: AsyncAutocompleteProps) => { - const [open, setOpen] = useState(false) - const [searchValue, setSearchValue] = useState('') - const debouncedSearchValue = useDebounce(500, searchValue) - const [options, setOptions] = useState([]) - const [loading, setLoading] = useState(false) - const controllerRef = useRef(null) - - useEffect(() => { - const handleRequest = async () => { - if (!onFetch) return - setLoading(true) - controllerRef.current = cancelPendingRequest(controllerRef.current) - const response = (await onFetch(debouncedSearchValue, controllerRef.current?.signal)) || [] - setOptions(response) - setLoading(false) - } - handleRequest() - }, [debouncedSearchValue]) - - useEffect(() => { - if (!open) { - setOptions([]) - } - }, [open]) - - return ( - { - setOpen(true) - }} - onClose={() => { - setOpen(false) - }} - open={open} - className={className} - multiple - noOptionsText={noOptionsText} - loadingText={'Chargement en cours...'} - loading={loading} - value={values} - autoComplete - filterSelectedOptions - onChange={(event: SyntheticEvent, newValue: LabelObject[]) => { - onChange(newValue) - }} - options={options} - filterOptions={(x) => x} - isOptionEqualToValue={(option, value) => option.id === value.id} - getOptionLabel={(option) => option.label} - renderInput={(params) => ( - { - setSearchValue(e.target.value) - }} - InputProps={{ - ...params.InputProps, - endAdornment: ( - - {loading ? : null} - {params.InputProps.endAdornment} - - ) - }} - /> - )} - /> - ) -} - -export default AsyncAutocomplete diff --git a/src/components/ui/Inputs/ExecutiveUnit/index.tsx b/src/components/ui/Inputs/ExecutiveUnit/index.tsx deleted file mode 100644 index 5adb53530..000000000 --- a/src/components/ui/Inputs/ExecutiveUnit/index.tsx +++ /dev/null @@ -1,109 +0,0 @@ -import { Chip, CircularProgress, Grid, IconButton, Tooltip } from '@mui/material' -import React, { ReactNode, useEffect, useState } from 'react' -import { LoadingStatus, ScopeElement } from 'types' -import InfoIcon from '@mui/icons-material/Info' -import { InputWrapper } from 'components/ui/Inputs' -import EditIcon from '@mui/icons-material/Edit' -import { ExecutiveUnitsWrapper } from './styles' -import { SourceType } from 'types/scope' -import PopulationRightPanel from 'components/CreationCohort/DiagramView/components/PopulationCard/components/PopulationRightPanel' -import { Hierarchy } from 'types/hierarchy' -import servicesPerimeters from 'services/aphp/servicePerimeters' -import { getScopeLevelBySourceType } from 'utils/perimeters' -import { CriteriaLabel } from 'components/ui/CriteriaLabel' - -type ExecutiveUnitsFilterProps = { - value: Hierarchy[] - sourceType: SourceType - disabled?: boolean - onChange?: (value: Hierarchy[]) => void - label?: ReactNode -} - -const ExecutiveUnitsInput = ({ value, sourceType, disabled = false, onChange, label }: ExecutiveUnitsFilterProps) => { - const [population, setPopulation] = useState[]>([]) - const [selectedPopulation, setSelectedPopulation] = useState[]>(value) - const [loading, setLoading] = useState(disabled ? LoadingStatus.SUCCESS : LoadingStatus.FETCHING) - const [open, setOpen] = useState(false) - - const handleDelete = (id: string) => { - const newSelectedPopulation = selectedPopulation.filter((item) => item.id !== id) - setSelectedPopulation(newSelectedPopulation) - } - - useEffect(() => { - const handleFetchPopulation = async () => { - const response = await servicesPerimeters.getPerimeters({ sourceType: SourceType.APHP }) - setPopulation(response.results) - setLoading(LoadingStatus.SUCCESS) - } - if (!disabled) handleFetchPopulation() - }, []) - - useEffect(() => { - if (onChange) onChange(selectedPopulation) - }, [selectedPopulation]) - return ( - - - {label || } - - {'- Le niveau hiérarchique de rattachement est : ' + getScopeLevelBySourceType(sourceType) + '.'} -
    - {"- L'unité exécutrice" + - ' est la structure élémentaire de prise en charge des malades par une équipe soignante ou médico-technique identifiées par leurs fonctions et leur organisation.'} - - } - > - -
    -
    - - - - - {!selectedPopulation.length && 'Sélectionner une unité exécutrice'} - {selectedPopulation.map((unit) => ( - { - handleDelete(unit.id) - }} - /> - ))} - - setOpen(true)} disabled={disabled}> - {loading === LoadingStatus.SUCCESS && } - {loading === LoadingStatus.FETCHING && } - - - { - setSelectedPopulation(value) - setOpen(false) - }} - onClose={() => setOpen(false)} - /> - -
    - ) -} - -export default ExecutiveUnitsInput diff --git a/src/components/ui/Inputs/ExecutiveUnit/styles.tsx b/src/components/ui/Inputs/ExecutiveUnit/styles.tsx deleted file mode 100644 index 37d87b3b2..000000000 --- a/src/components/ui/Inputs/ExecutiveUnit/styles.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import { styled } from '@mui/material' - -export const ExecutiveUnitsWrapper = styled('div')(() => ({ - display: 'flex', - alignItems: 'center', - justifyContent: 'space-between', - border: '1px solid rgba(0,0,0,0.25)', - padding: '5px 14px', - width: '100%', - borderRadius: '4px', - color: 'rgba(0, 0, 0, 0.6)' -})) diff --git a/src/components/ui/Inputs/ExecutiveUnits/index.tsx b/src/components/ui/Inputs/ExecutiveUnits/index.tsx new file mode 100644 index 000000000..cf99a17ea --- /dev/null +++ b/src/components/ui/Inputs/ExecutiveUnits/index.tsx @@ -0,0 +1,137 @@ +import { CircularProgress, FormLabel, Grid, IconButton, Tooltip, Typography } from '@mui/material' +import React, { useEffect, useState } from 'react' +import { LoadingStatus, ScopeElement } from 'types' +import InfoIcon from '@mui/icons-material/Info' +import { InputWrapper } from 'components/ui/Inputs' +import { SourceType } from 'types/scope' +import { Hierarchy } from 'types/hierarchy' +import servicesPerimeters from 'services/aphp/servicePerimeters' +import { getScopeLevelBySourceType } from 'utils/perimeters' +import { CriteriaLabel } from 'components/ui/CriteriaLabel' +import ScopeTree from 'components/ScopeTree' +import Panel from 'components/ui/Panel' +import CloseIcon from '@mui/icons-material/Close' +import MoreHorizIcon from '@mui/icons-material/MoreHoriz' +import CodesWithSystems from 'components/Hierarchy/CodesWithSystems' +import { SearchOutlined } from '@mui/icons-material' + +type ExecutiveUnitsProps = { + value: Hierarchy[] + sourceType: SourceType + disabled?: boolean + onChange: (value: Hierarchy[]) => void + isCriterion?: boolean +} + +const ExecutiveUnits = ({ + value, + sourceType, + disabled = false, + onChange, + isCriterion = false +}: ExecutiveUnitsProps) => { + const [population, setPopulation] = useState[]>([]) + const [selectedPopulation, setSelectedPopulation] = useState[]>([]) + const [confirmedPopulation, setConfirmedPopulation] = useState[]>(value) + const [loading, setLoading] = useState(disabled ? LoadingStatus.SUCCESS : LoadingStatus.FETCHING) + const [open, setOpen] = useState(false) + const [isExtended, setIsExtended] = useState(false) + + const handleDelete = (node: Hierarchy) => { + const newSelectedPopulation = selectedPopulation.filter((item) => item.id !== node.id) + setSelectedPopulation(newSelectedPopulation) + setConfirmedPopulation(newSelectedPopulation) + } + + useEffect(() => { + const handleFetchPopulation = async () => { + const response = await servicesPerimeters.getPerimeters({ sourceType: SourceType.APHP }) + setPopulation(response.results) + setLoading(LoadingStatus.SUCCESS) + } + if (!disabled) handleFetchPopulation() + }, []) + + useEffect(() => { + onChange(confirmedPopulation) + }, [confirmedPopulation]) + + return ( + <> + + + {isCriterion ? ( + + ) : ( + + Unité exécutrice : + + )} + + {'- Le niveau hiérarchique de rattachement est : ' + getScopeLevelBySourceType(sourceType) + '.'} +
    + {"- L'unité exécutrice" + + ' est la structure élémentaire de prise en charge des malades par une équipe soignante ou médico-technique identifiées par leurs fonctions et leur organisation.'} + + } + > + +
    +
    + + + {!confirmedPopulation.length && Sélectionner une unité exécutrice} + + + + {isExtended && ( + setIsExtended(false)}> + + + )} + {!isExtended && ( + setIsExtended(true)}> + + + )} + setOpen(true)} disabled={disabled}> + {loading === LoadingStatus.FETCHING && } + {loading === LoadingStatus.SUCCESS && } + + + + { + setConfirmedPopulation(selectedPopulation) + setOpen(false) + }} + onClose={() => setOpen(false)} + > + + +
    + + ) +} + +export default ExecutiveUnits diff --git a/src/components/ui/Layout/index.tsx b/src/components/ui/Layout/index.tsx index e915a7cf4..61dce21db 100644 --- a/src/components/ui/Layout/index.tsx +++ b/src/components/ui/Layout/index.tsx @@ -8,11 +8,4 @@ type CustomProps = { export const BlockWrapper = styled(Grid)(({ padding = 0, margin = 0 }) => ({ padding: typeof padding === 'string' ? padding : `${padding}px`, margin: typeof margin === 'string' ? margin : `${margin}px` -})) - -export const InputWrapper = styled('div')(() => ({ - padding: 0, - '& > div': { - margin: '15px 0px' - } -})) +})) \ No newline at end of file diff --git a/src/components/ui/List/index.tsx b/src/components/ui/List/index.tsx index 2f8da5888..50d49cbc9 100644 --- a/src/components/ui/List/index.tsx +++ b/src/components/ui/List/index.tsx @@ -4,7 +4,7 @@ import Button from 'components/ui/Button' import { DeleteOutline, Edit, Visibility } from '@mui/icons-material' import { Checkbox, FormControlLabel, Grid, Typography } from '@mui/material' import { Item } from 'components/ui/List/ListItem' -import ListItems from 'components/ui/List/ListItems' +import ListItems from './ListItems' type id = string diff --git a/src/components/ui/Pagination/index.tsx b/src/components/ui/Pagination/index.tsx index a793784b2..2320c9310 100644 --- a/src/components/ui/Pagination/index.tsx +++ b/src/components/ui/Pagination/index.tsx @@ -1,7 +1,7 @@ import React, { useEffect, useState, useContext } from 'react' -import { Box, FormLabel, Grid, IconButton, PaginationItem } from '@mui/material' +import { Box, FormLabel, Grid, PaginationItem } from '@mui/material' import ArrowCircleRightIcon from '@mui/icons-material/ArrowCircleRight' -import { PaginationInput, StyledPagination } from './styles' +import { PaginationInput, StyledButton, StyledPagination } from './styles' import { useAppDispatch } from 'state' import { showDialog } from 'state/warningDialog' import { AppConfig } from 'config' @@ -11,9 +11,10 @@ type PaginationProps = { count: number onPageChange: (page: number) => void smallSize?: boolean + color?: string } -export const Pagination = ({ currentPage, count, onPageChange, smallSize }: PaginationProps) => { +export const Pagination = ({ currentPage, count, onPageChange, smallSize, color = '#5BC5F2' }: PaginationProps) => { const dispatch = useAppDispatch() const [goToPage, setGoToPage] = useState('') const config = useContext(AppConfig) @@ -58,6 +59,7 @@ export const Pagination = ({ currentPage, count, onPageChange, smallSize }: Pagi return ( Aller à la page setGoToPage(newValue)} onKeyDown={handleKeyDown} /> - + - + ) diff --git a/src/components/ui/Pagination/styles.ts b/src/components/ui/Pagination/styles.ts index 143a58286..80b6c8e89 100644 --- a/src/components/ui/Pagination/styles.ts +++ b/src/components/ui/Pagination/styles.ts @@ -1,25 +1,35 @@ -import { Pagination, styled } from '@mui/material' +import { IconButton, Pagination, styled } from '@mui/material' import { NumberInput } from '../NumberInput' -export const StyledPagination = styled(Pagination)(() => ({ +type ColorProps = { + elemColor?: string +} + +export const StyledPagination = styled(Pagination)(({ elemColor = '#5BC5F2' }) => ({ margin: '12px 10px', float: 'right', '& button': { backgroundColor: '#fff', - color: '#5BC5F2' + color: elemColor }, '& .MuiPaginationItem-page.Mui-selected': { - color: '#0063AF', - backgroundColor: '#FFF' + color: '#fff', + backgroundColor: elemColor, + height: 25 } })) -export const PaginationInput = styled(NumberInput)(() => ({ +export const PaginationInput = styled(NumberInput)(({ elemColor = '#5BC5F2' }) => ({ width: 50, height: 24, margin: '0 4px', backgroundColor: '#FFF', - border: '1px solid #5BC5F2', + border: `1px solid ${elemColor}`, borderRadius: 10, padding: 4 })) + +export const StyledButton = styled(IconButton)(({ elemColor = '#5BC5F2' }) => ({ + color: elemColor, + padding: 0 +})) diff --git a/src/components/ui/Panel/index.tsx b/src/components/ui/Panel/index.tsx new file mode 100644 index 000000000..c2299f1d9 --- /dev/null +++ b/src/components/ui/Panel/index.tsx @@ -0,0 +1,69 @@ +import React, { PropsWithChildren, ReactNode } from 'react' +import { Grid, Button, Drawer, Typography, Paper } from '@mui/material' + +type RightPanelProps = { + open: boolean + anchor?: 'right' | 'left' | 'top' | 'bottom' + title?: string + mandatory?: boolean + children: ReactNode + onConfirm: () => void + onClose: () => void +} + +const Panel = ({ + open, + title, + anchor = 'right', + mandatory = false, + children, + onConfirm, + onClose +}: PropsWithChildren) => { + return ( + + + + + + {title && ( + + {title} + + )} + + + + {children} + + + + + + + + + + ) +} + +export default Panel diff --git a/src/components/ui/Searchbar/SearchInput.tsx b/src/components/ui/Searchbar/SearchInput.tsx index d3168c756..8103e9595 100644 --- a/src/components/ui/Searchbar/SearchInput.tsx +++ b/src/components/ui/Searchbar/SearchInput.tsx @@ -17,6 +17,7 @@ type SearchInputProps = { displayHelpIcon?: boolean error?: SearchInputError | null width?: string + disabled?: boolean onchange: (value: string) => void } @@ -27,6 +28,7 @@ const SearchInput = ({ width = '100%', displayHelpIcon = false, error = null, + disabled = false, onchange }: SearchInputProps) => { const [searchInput, setSearchInput] = useState(value) @@ -45,6 +47,7 @@ const SearchInput = ({ <> { diff --git a/src/components/ui/Tabs/index.tsx b/src/components/ui/Tabs/index.tsx index 4dbdb14e5..8a0158279 100644 --- a/src/components/ui/Tabs/index.tsx +++ b/src/components/ui/Tabs/index.tsx @@ -1,36 +1,38 @@ import React from 'react' -import { Box } from '@mui/material' import { TabType } from 'types' import { TabWrapper, TabsWrapper } from './styles' type TabsProps = { values: TabType[] active: TabType + color?: string + disabled?: boolean onchange: (newValue: TabType) => void } -const Tabs = ({ values, active, onchange }: TabsProps) => { +const Tabs = ({ values, active, color = '#ED6D91', disabled = false, onchange }: TabsProps) => { const tabSize = 100 / values.length return ( <> {values && values?.length > 0 && ( - - value.id === active.id)} - onChange={(_: React.SyntheticEvent, newValue: number) => onchange(values[newValue])} - > - {values.map((value) => ( - - ))} - - + value.id === active.id)} + onChange={(_: React.SyntheticEvent, newValue: number) => onchange(values[newValue])} + > + {values.map((value) => ( + + ))} + )} ) diff --git a/src/components/ui/Tabs/styles.ts b/src/components/ui/Tabs/styles.ts index c5353210f..9dc498bea 100644 --- a/src/components/ui/Tabs/styles.ts +++ b/src/components/ui/Tabs/styles.ts @@ -1,31 +1,36 @@ import { Tab, Tabs } from '@mui/material' import { styled } from '@mui/material/styles' -type CustomProps = { +type TabsCustomProps = { + color: string +} + +type TabCustomProps = { width: string + color: string } -export const TabsWrapper = styled(Tabs)(() => ({ +export const TabsWrapper = styled(Tabs)(({ color }) => ({ minHeight: 32, borderRadius: 8, '.MuiTabs-indicator': { - backgroundColor: '#ED6D91', + backgroundColor: color, height: '4px' } })) -export const TabWrapper = styled(Tab)(({ width }) => ({ - color: '#FFF', - backgroundColor: '#153D8A', +export const TabWrapper = styled(Tab)(({ width, color }) => ({ + color: '#00000099', + backgroundColor: 'transparent', padding: '0px 6px', - fontWeight: 600, + fontWeight: 700, fontSize: 13, width: width, '&.Mui-selected': { - backgroundColor: '#0063AF', - color: '#FFF', + backgroundColor: 'transparent', + color: color, fontWeight: 900, - fontSize: 12 + fontSize: 13 }, '&.MuiButtonBase-root.MuiTab-root': { minHeight: 40 diff --git a/src/config.tsx b/src/config.tsx index e2d843001..c988c76ef 100644 --- a/src/config.tsx +++ b/src/config.tsx @@ -7,7 +7,7 @@ type ValueSetConfig = { url: string } -type AppConfig = { +export type AppConfig = { labels: { exploration: string } diff --git a/src/constants.js b/src/constants.js index c4c8d4c9f..69fbeb55e 100644 --- a/src/constants.js +++ b/src/constants.js @@ -5,5 +5,3 @@ export const CONFIG_URL = export const ACCESS_TOKEN = 'access_token' export const REFRESH_TOKEN = 'refresh_token' export const IMPERSONATED_USER = 'impersonated_user' - -export const EXPLORATION = 'Exploration' diff --git a/src/data/valueSets.ts b/src/data/valueSets.ts new file mode 100644 index 000000000..2d0d19000 --- /dev/null +++ b/src/data/valueSets.ts @@ -0,0 +1,99 @@ +import { AppConfig } from 'config' +import { HierarchyWithLabel } from 'types/hierarchy' +import { Reference, References, ReferencesLabel } from 'types/valueSet' + +export const getReferences = (config: Readonly) => [ + { + id: References.ATC, + title: 'Toute la hiérarchie', + label: ReferencesLabel.ATC, + standard: true, + url: config.features.medication.valueSets.medicationAtc.url, + checked: true, + isHierarchy: true, + joinDisplayWithCode: true, + joinDisplayWithSystem: true, + filterRoots: (code: HierarchyWithLabel) => /^[A-WZ]$/.test(code.id) + }, + { + id: References.UCD, + title: 'Toute la hiérarchie', + label: ReferencesLabel.UCD, + standard: true, + url: config.features.medication.valueSets.medicationUcd.url, + checked: true, + joinDisplayWithCode: true, + joinDisplayWithSystem: true, + isHierarchy: false, + filterRoots: () => true + }, + { + id: References.ANABIO, + title: 'Toute la hiérarchie', + label: ReferencesLabel.ANABIO, + standard: true, + url: config.features.observation.valueSets.biologyHierarchyAnabio.url, + checked: true, + isHierarchy: true, + joinDisplayWithCode: false, + joinDisplayWithSystem: true, + filterRoots: (biologyItem: HierarchyWithLabel) => + biologyItem.id !== '527941' && + biologyItem.id !== '547289' && + biologyItem.id !== '528247' && + biologyItem.id !== '981945' && + biologyItem.id !== '834019' && + biologyItem.id !== '528310' && + biologyItem.id !== '528049' && + biologyItem.id !== '527570' && + biologyItem.id !== '527614' + }, + { + id: References.LOINC, + title: 'Toute la hiérarchie', + label: ReferencesLabel.LOINC, + standard: true, + url: config.features.observation.valueSets.biologyHierarchyLoinc.url, + checked: true, + isHierarchy: false, + joinDisplayWithCode: true, + joinDisplayWithSystem: true, + filterRoots: () => true + }, + { + id: References.CCAM, + title: 'Toute la hiérarchie', + label: ReferencesLabel.CCAM, + standard: true, + url: config.features.procedure.valueSets.procedureHierarchy.url, + checked: true, + isHierarchy: true, + joinDisplayWithCode: true, + joinDisplayWithSystem: false, + filterRoots: () => true + }, + { + id: References.CIM10, + title: 'Toute la hiérarchie', + label: ReferencesLabel.CIM10, + standard: true, + url: config.features.condition.valueSets.conditionHierarchy.url, + checked: true, + isHierarchy: true, + joinDisplayWithSystem: false, + joinDisplayWithCode: true, + filterRoots: () => true + }, + { + id: References.GHM, + title: 'Toute la hiérarchie', + label: ReferencesLabel.GHM, + standard: true, + url: config.features.claim.valueSets.claimHierarchy.url, + checked: true, + isHierarchy: true, + joinDisplayWithCode: true, + joinDisplayWithSystem: false, + filterRoots: () => true + } +] diff --git a/src/hooks/hierarchy/useHierarchy.ts b/src/hooks/hierarchy/useHierarchy.ts index c5b27713e..3deefbac0 100644 --- a/src/hooks/hierarchy/useHierarchy.ts +++ b/src/hooks/hierarchy/useHierarchy.ts @@ -1,131 +1,168 @@ import { - buildHierarchy, + buildTree, + buildMultipleTrees, getHierarchyDisplay, - getItemSelectedStatus, + getListDisplay, getMissingCodes, - mapHierarchyToMap -} from './../../utils/hierarchy' + getMissingCodesWithSystems, + groupBySystem, + getHierarchyRootCodes, + mapHierarchyToMap, + getSelectedCodesFromTrees, + createHierarchyRoot +} from '../../utils/hierarchy/hierarchy' import { useEffect, useRef, useState } from 'react' -import { LoadingStatus, SelectedStatus } from 'types' -import { getSelectedCodes } from 'utils/hierarchy' -import { Hierarchy, Mode } from '../../types/hierarchy' -import { removeElement } from 'utils/arrays' - +import { Back_API_Response, LoadingStatus, SelectedStatus } from 'types' +import { Codes, Hierarchy, HierarchyInfo, HierarchyLoadingStatus, Mode, SearchMode } from '../../types/hierarchy' +import { replaceInMap } from 'utils/map' +import { HIERARCHY_ROOT } from 'services/aphp/serviceValueSets' +/** + * @param {Hierarchy[]} selectedNodes - Nodes selected in the hierarchy. + * @param {Codes>} fetchedCodes - All the codes that have already been fetched and saved. + * @param {(codes: Hierarchy[]) => void} onCache - A cache function to store the codes you fetch in the useHierarchy hook. + * @param {(ids: string, system: string) => Promise[]>} fetchHandler - A callback function that returns fetched hierarchies. + */ export const useHierarchy = ( - baseTree: Hierarchy[], - selectedNodes: Hierarchy[], - _codes: Hierarchy[], - onCache: (codes: Hierarchy[]) => void, - fetchHandler: (ids: string) => Promise[]> + selectedNodes: Hierarchy[], + fetchedCodes: Codes>, + onCache: (codes: Codes>) => void, + fetchHandler: (ids: string, system: string) => Promise[]> ) => { - const [hierarchyRepresentation, setHierarchyRepresentation] = useState[]>([]) - const [hierarchyDisplay, setHierarchyDisplay] = useState[]>([]) - const [selectedCodes, setSelectedCodes] = useState[]>(selectedNodes) - const [codes, setCodes] = useState>>( - mapHierarchyToMap(_codes.map((code) => ({ ...code, subItems: undefined }))) + const DEFAULT_HIERARCHY_INFO = { + tree: [], + count: 0, + page: 1, + system: '', + selectAllStatus: SelectedStatus.INDETERMINATE + } + const [trees, setTrees] = useState[]>>(new Map()) + const [hierarchies, setHierarchies] = useState>>(new Map()) + const [searchResults, setSearchResults] = useState>(DEFAULT_HIERARCHY_INFO) + const [selectedCodes, setSelectedCodes] = useState>>( + new Map(groupBySystem(selectedNodes).map((item) => [item.system, mapHierarchyToMap(item.codes)])) ) + const [codes, setCodes] = useState>>(fetchedCodes) + const latestCodes = useRef(codes) - const [loadingStatus, setLoadingStatus] = useState({ - search: LoadingStatus.FETCHING, + const [loadingStatus, setLoadingStatus] = useState({ + init: LoadingStatus.FETCHING, + search: LoadingStatus.SUCCESS, expand: LoadingStatus.SUCCESS }) - const [selectAllStatus, setSelectAllStatus] = useState(SelectedStatus.NOT_SELECTED) + useEffect(() => { latestCodes.current = codes }, [codes]) useEffect(() => { - return () => onCache(Array.from(latestCodes.current.values())) + return () => onCache(latestCodes.current) }, []) - useEffect(() => { - if (hierarchyDisplay.length) { - const node = { id: 'parent', subItems: hierarchyDisplay } as Hierarchy - const status = getItemSelectedStatus(node) - setSelectAllStatus(status) + const initTrees = async ( + initHandlers: { + system: string + fetchBaseTree: () => Promise>> + }[] + ) => { + const newTrees: Map[]> = new Map() + const newHierarchies: Map> = new Map() + let allCodes: Codes> = new Map() + for (const handler of initHandlers) { + const { results: baseTree, count } = await handler.fetchBaseTree() + const currentSelected = selectedCodes.get(handler.system) || new Map() + const toAdd = currentSelected.get(HIERARCHY_ROOT) ? new Map() : currentSelected + const toFind = [...baseTree, ...toAdd.values()] + const currentCodes = codes.get(handler.system) || new Map() + const newCodes = await getMissingCodes(baseTree, currentCodes, toFind, handler.system, Mode.INIT, fetchHandler) + const newTree = buildTree(baseTree, handler.system, toFind, newCodes, currentSelected, Mode.INIT) + const newHierarchy = getHierarchyDisplay(baseTree, newTree) + newTrees.set(handler.system, newTree) + newHierarchies.set(handler.system, { tree: newHierarchy, count, page: 1, system: handler.system }) + allCodes.set(handler.system, new Map([...newCodes, ...getHierarchyRootCodes(newTree)])) } - }, [hierarchyDisplay]) + setTrees(newTrees) + setHierarchies(newHierarchies) + setCodes(allCodes) + setLoadingStatus((prevLoadingStatus) => ({ ...prevLoadingStatus, init: LoadingStatus.SUCCESS })) + } - useEffect(() => { - const init = async () => { - const fetchedCodes = [...baseTree, ...selectedCodes] - const newCodes = await getMissingCodes(baseTree, codes, fetchedCodes, Mode.INIT, fetchHandler) - const newTree = buildHierarchy(baseTree, fetchedCodes, newCodes, selectedCodes, Mode.INIT) - const newDisplay = getHierarchyDisplay(baseTree, newTree) - setCodes(newCodes) - setHierarchyRepresentation(newTree) - setHierarchyDisplay(newDisplay) - setLoadingStatus({ ...loadingStatus, search: LoadingStatus.SUCCESS }) - } - init() - }, []) + const search = async (fetchSearch: () => Promise>>) => { + const { results: endCodes, count } = await fetchSearch() + const bySystem = groupBySystem(endCodes) + const newCodes = await getMissingCodesWithSystems(trees, bySystem, codes, fetchHandler) + const newTrees = buildMultipleTrees(trees, bySystem, newCodes, selectedCodes, Mode.SEARCH) + setCodes(newCodes) + setTrees(newTrees) + return { display: getListDisplay(endCodes, newTrees), count } + } - const search = async ( - searchValue: string, + const fetchMore = async ( + fetchSearch: () => Promise>>, page: number, - fetchSearch: (search: string, page: number) => Promise[]> + mode: SearchMode, + id?: string ) => { setLoadingStatus({ ...loadingStatus, search: LoadingStatus.FETCHING }) - const endCodes = searchValue ? await fetchSearch(searchValue, page) : [] - const newCodes = searchValue ? await getMissingCodes(baseTree, codes, endCodes, Mode.SEARCH, fetchHandler) : codes - const toDisplay = searchValue ? endCodes : baseTree - const newTree = buildHierarchy(hierarchyRepresentation, endCodes, newCodes, selectedCodes, Mode.SEARCH) - const newDisplay = getHierarchyDisplay(toDisplay, newTree) - setCodes(newCodes) - setHierarchyRepresentation(newTree) - setHierarchyDisplay(newDisplay) + const { display, count } = await search(fetchSearch) + if (mode == SearchMode.EXPLORATION && id) { + const currentHierarchy = hierarchies.get(id) || DEFAULT_HIERARCHY_INFO + setHierarchies(replaceInMap(id, { ...currentHierarchy, tree: display, page }, hierarchies)) + } else setSearchResults({ tree: display, count, page, system: '' }) setLoadingStatus({ ...loadingStatus, search: LoadingStatus.SUCCESS }) } - const select = (node: Hierarchy, toAdd: boolean) => { - const mode = toAdd ? Mode.SELECT : Mode.UNSELECT - const newTree = buildHierarchy(hierarchyRepresentation, [node], codes, selectedCodes, mode) - const newDisplay = getHierarchyDisplay(hierarchyDisplay, newTree) - const newSelectedCodes = getSelectedCodes(newTree) - setHierarchyRepresentation(newTree) - setHierarchyDisplay(newDisplay) - setSelectedCodes(newSelectedCodes) + const select = (nodes: Hierarchy[], mode: Mode.SELECT | Mode.UNSELECT | Mode.SELECT_ALL | Mode.UNSELECT_ALL) => { + console.log('test bug', nodes, mode) + const bySystem = groupBySystem(nodes) + const selectMode = mode === Mode.SELECT_ALL ? Mode.SELECT : mode === Mode.UNSELECT_ALL ? Mode.UNSELECT : mode + const newTrees = buildMultipleTrees(trees, bySystem, codes, selectedCodes, selectMode) + const displaySearch = getListDisplay(searchResults.tree, newTrees) + if (mode === Mode.SELECT_ALL || mode === Mode.UNSELECT_ALL) { + const system = nodes?.[0].system || '' + const root = new Map() + if (mode === Mode.SELECT_ALL) root.set(HIERARCHY_ROOT, createHierarchyRoot(system)) + setSelectedCodes(replaceInMap(system, root, selectedCodes)) + } else setSelectedCodes(getSelectedCodesFromTrees(newTrees, selectedCodes)) + setTrees(newTrees) + let newHierarchies = new Map(hierarchies) + hierarchies.forEach((hierarchy) => { + if (bySystem.find((elem) => elem.system === hierarchy.system)) { + const newTree = newTrees.get(hierarchy.system) || [] + newHierarchies = replaceInMap( + hierarchy.system, + { ...hierarchy, tree: getHierarchyDisplay(hierarchy.tree, newTree) }, + newHierarchies + ) + } + }) + setHierarchies(newHierarchies) + setSearchResults({ ...searchResults, tree: displaySearch }) } - const selectAll = (toAdd: boolean) => { - const mode = toAdd ? Mode.SELECT_ALL : Mode.UNSELECT_ALL - const newTree = buildHierarchy(hierarchyRepresentation, hierarchyDisplay, codes, selectedCodes, mode) - const newDisplay = getHierarchyDisplay(hierarchyDisplay, newTree) - const newSelectedCodes = getSelectedCodes(newTree) - setHierarchyRepresentation(newTree) - setHierarchyDisplay(newDisplay) - setSelectedCodes(newSelectedCodes) - } - - const deleteCode = (node: Hierarchy) => { - const newCodes = removeElement(node, selectedCodes) - const newTree = buildHierarchy(hierarchyRepresentation, [node], codes, selectedCodes, Mode.UNSELECT) - const newDisplay = getHierarchyDisplay(hierarchyDisplay, newTree) - setHierarchyRepresentation(newTree) - setHierarchyDisplay(newDisplay) - setSelectedCodes(newCodes) - } - - const expand = async (node: Hierarchy) => { + const expand = async (node: Hierarchy) => { setLoadingStatus({ ...loadingStatus, expand: LoadingStatus.FETCHING }) - const newCodes = await getMissingCodes(baseTree, codes, [node], Mode.EXPAND, fetchHandler) - const newTree = buildHierarchy(hierarchyRepresentation, [node], newCodes, selectedCodes, Mode.EXPAND) - const newDisplay = getHierarchyDisplay(hierarchyDisplay, newTree) - setCodes(newCodes) - setHierarchyRepresentation(newTree) - setHierarchyDisplay(newDisplay) + const hierarchyId = node.system + const currentTree = trees.get(hierarchyId) || [] + const currentHierarchy = hierarchies.get(hierarchyId) || DEFAULT_HIERARCHY_INFO + const currentCodes = codes.get(hierarchyId) || new Map() + const currentSelected = selectedCodes.get(hierarchyId) || new Map() + const newCodes = await getMissingCodes(currentTree, currentCodes, [node], hierarchyId, Mode.EXPAND, fetchHandler) + const newTree = buildTree(currentTree, node.system, [node], newCodes, currentSelected, Mode.EXPAND) + const display = getHierarchyDisplay(currentHierarchy.tree, newTree) + setCodes(replaceInMap(hierarchyId, newCodes, codes)) + setTrees(replaceInMap(hierarchyId, newTree, trees)) + setHierarchies(replaceInMap(hierarchyId, { ...currentHierarchy, tree: display }, hierarchies)) setLoadingStatus({ ...loadingStatus, search: LoadingStatus.SUCCESS }) } return { - hierarchy: hierarchyDisplay, + hierarchies, + searchResults, selectedCodes, loadingStatus, - selectAllStatus, - search, + initTrees, select, - selectAll, expand, - deleteCode + fetchMore } } diff --git a/src/hooks/scopeTree/useScopeTree.ts b/src/hooks/scopeTree/useScopeTree.ts new file mode 100644 index 000000000..46c7c7f48 --- /dev/null +++ b/src/hooks/scopeTree/useScopeTree.ts @@ -0,0 +1,117 @@ +import { useCallback, useEffect, useMemo, useRef, useState } from 'react' +import { Codes, Hierarchy, SearchMode } from 'types/hierarchy' +import { LIMIT_PER_PAGE } from '../search/useSearchParameters' +import { ScopeElement } from 'types' +import { SourceType, System } from 'types/scope' +import { useAppDispatch, useAppSelector } from 'state' +import servicesPerimeters from 'services/aphp/servicePerimeters' +import { saveScopeCodes, selectScopeCodes } from 'state/scope' +import { useHierarchy } from 'hooks/hierarchy/useHierarchy' +import { mapCodesToCache } from 'utils/hierarchy/hierarchy' + +export const useScopeTree = ( + baseTree: Hierarchy[], + selectedNodes: Hierarchy[], + sourceType: SourceType +) => { + const practitionerId = useAppSelector((state) => state.me)?.id || '' + const codes = useAppSelector((state) => selectScopeCodes(state, sourceType === SourceType.ALL)) + const controllerRef = useRef(null) + const dispatch = useAppDispatch() + const [searchInput, setSearchInput] = useState('') + const [mode, setMode] = useState(SearchMode.EXPLORATION) + + const fetchChildren = useCallback( + async (ids: string) => { + const { results } = + sourceType === SourceType.ALL + ? await servicesPerimeters.getRights({ practitionerId, ids, limit: -1, sourceType }) + : await servicesPerimeters.getPerimeters({ practitionerId, ids, limit: -1, sourceType }) + return results + }, + [practitionerId, sourceType] + ) + + const fetchSearch = async (search: string, page: number) => { + const options = { practitionerId, search, page, limit: LIMIT_PER_PAGE, sourceType } + const results = + sourceType === SourceType.ALL + ? await servicesPerimeters.getRights(options) + : await servicesPerimeters.getPerimeters(options) + return results + } + + const handleSaveCodes = (codes: Codes>) => { + const entity = mapCodesToCache(codes)?.[0] + dispatch(saveScopeCodes({ isRights: sourceType === SourceType.ALL, values: entity })) + } + + const { + hierarchies, + searchResults, + selectedCodes, + loadingStatus, + selectAllStatus, + initTrees, + fetchMore, + expand, + select, + selectAll + } = useHierarchy(selectedNodes, codes, handleSaveCodes, fetchChildren) + + useEffect(() => { + initTrees([ + { system: System.ScopeTree, fetchBaseTree: async () => ({ results: baseTree, count: baseTree.length }) } + ]) + }, [baseTree]) + + const current = useMemo(() => { + const found = mode === SearchMode.EXPLORATION ? hierarchies.get(System.ScopeTree) : searchResults + return ( + found || { + tree: [], + count: 0, + page: 1 + } + ) + }, [mode, hierarchies, searchResults]) + + const selected: Hierarchy[] = useMemo(() => { + return [...(selectedCodes.get(System.ScopeTree) || new Map()).values()] + }, [selectedCodes]) + + const handleChangePage = (page: number) => { + fetchMore(() => fetchSearch(searchInput, page - 1), page, SearchMode.RESEARCH) + } + + const handleChangeSearchInput = (newSearchInput: string) => { + setSearchInput(newSearchInput) + if (newSearchInput === '') setMode(SearchMode.EXPLORATION) + else { + setMode(SearchMode.RESEARCH) + fetchMore(() => fetchSearch(newSearchInput, 0), 1, SearchMode.RESEARCH) + } + } + + return { + hierarchyData: { + hierarchy: current, + loadingStatus, + selectAllStatus, + selectedCodes: selected + }, + hierarchyActions: { + expand, + select, + selectAll + }, + parametersData: { + searchInput, + mode + }, + parametersActions: { + onChangePage: handleChangePage, + onChangeSearchInput: handleChangeSearchInput + } + } +} diff --git a/src/hooks/useSearchParameters.ts b/src/hooks/search/useSearchParameters.ts similarity index 50% rename from src/hooks/useSearchParameters.ts rename to src/hooks/search/useSearchParameters.ts index 4fe59db27..b958b5988 100644 --- a/src/hooks/useSearchParameters.ts +++ b/src/hooks/search/useSearchParameters.ts @@ -1,23 +1,42 @@ import { useEffect, useMemo, useState } from 'react' +import { TabType } from 'types' +import { Reference } from 'types/valueSet' + +export const LIMIT_PER_PAGE = 20 + +export type SearchParameters = { + searchInput?: string, + searchMode?: boolean, + page?: number, + limit?: number, + count?: number, + totalPages?: number, + references?: Reference[], + tabs?: TabType[] +} export const useSearchParameters = () => { - const [search, setSearch] = useState('') + const [searchInput, setSearchInput] = useState('') const [searchMode, setSearchMode] = useState(false) const [page, setPage] = useState(0) - const [limit, setLimit] = useState(20) + const [limit, setLimit] = useState(LIMIT_PER_PAGE) const [count, setCount] = useState(0) const [totalPages, setTotalPages] = useState(0) + const [references, setReferences] = useState([]) + const [tabs, setTabs] = useState([]) const options = useMemo( () => ({ page, - search, + searchInput, searchMode, limit, count, - totalPages + totalPages, + references, + tabs }), - [search, page, limit, count, totalPages, searchMode] + [searchInput, page, limit, count, totalPages, searchMode, references, tabs] ) useEffect(() => { @@ -29,7 +48,7 @@ export const useSearchParameters = () => { } const onChangeSearchInput = (newValue: string) => { - setSearch(newValue) + setSearchInput(newValue) } const onChangePage = (newValue: number) => { @@ -44,12 +63,22 @@ export const useSearchParameters = () => { setSearchMode(newValue) } + const onChangeReferences = (references: Reference[]) => { + setReferences(references) + } + + const onChangeTabs = (tabs: TabType[]) => { + setTabs(tabs) + } + return { options, onChangeSearchInput, onChangeSearchMode, onChangePage, onChangeLimit, - onChangeCount + onChangeCount, + onChangeReferences, + onChangeTabs } } diff --git a/src/hooks/useDebounceAction.ts b/src/hooks/useDebounceAction.ts new file mode 100644 index 000000000..a69ae39fa --- /dev/null +++ b/src/hooks/useDebounceAction.ts @@ -0,0 +1,28 @@ +import { useState, useEffect, useCallback } from 'react' + +export const useDebounceAction = (callback: (...args: any[]) => void, delay: number) => { + const [debounceTimeout, setDebounceTimeout] = useState(null) + + const debounced = useCallback( + (...args: any[]) => { + if (debounceTimeout) { + clearTimeout(debounceTimeout) + } + const timeout = setTimeout(() => { + callback(...args) + }, delay) + + setDebounceTimeout(timeout) + }, + [callback, delay, debounceTimeout] + ) + useEffect(() => { + return () => { + if (debounceTimeout) { + clearTimeout(debounceTimeout) + } + } + }, [debounceTimeout]) + + return debounced +} diff --git a/src/hooks/useFetch.ts b/src/hooks/useFetch.ts deleted file mode 100644 index 3887bfec0..000000000 --- a/src/hooks/useFetch.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { useEffect, useState } from 'react' -import { Back_API_Response, LoadingStatus } from 'types' -import { isAxiosError } from 'axios' - -type FetchOptions = { - searchInput?: string - limit?: number - page?: number -} - -export const useFetch = (fetchCall: () => Promise>, options?: FetchOptions) => { - const [response, setResponse] = useState>({ - count: 0, - next: '', - previous: '', - results: [] - }) - const [error, setError] = useState('') - const [fetchStatus, setFetchStatus] = useState(LoadingStatus.IDDLE) - - useEffect(() => { - setFetchStatus(LoadingStatus.FETCHING) - }, [options]) - - useEffect(() => { - const handleFetchCall = async () => { - setFetchStatus(LoadingStatus.FETCHING) - const response = await fetchCall() - if (isAxiosError(response)) { - setError(response.message) - } - setFetchStatus(LoadingStatus.SUCCESS) - setResponse(response) - } - if (fetchStatus === LoadingStatus.FETCHING) { - handleFetchCall() - } - }, [fetchStatus]) - - return { - response, - error, - fetchStatus - } -} diff --git a/src/hooks/valueSet/useSearchValueSet.ts b/src/hooks/valueSet/useSearchValueSet.ts new file mode 100644 index 000000000..34a0fc5a4 --- /dev/null +++ b/src/hooks/valueSet/useSearchValueSet.ts @@ -0,0 +1,185 @@ +import { useCallback, useEffect, useMemo, useRef, useState } from 'react' +import { Reference } from 'types/valueSet' +import { LIMIT_PER_PAGE, SearchParameters, useSearchParameters } from '../search/useSearchParameters' +import { useHierarchy } from 'hooks/hierarchy/useHierarchy' +import { + HIERARCHY_ROOT, + getChildrenFromCodes, + getHierarchyRoots, + searchInValueSets +} from 'services/aphp/serviceValueSets' +import { useDebounceAction } from 'hooks/useDebounceAction' +import { Codes, FhirItem, Hierarchy, Mode, SearchMode } from 'types/hierarchy' +import { saveValueSets, selectValueSetCodes } from 'state/valueSets' +import { useAppDispatch, useAppSelector } from 'state' +import { getItemSelectedStatus, mapCodesToCache } from 'utils/hierarchy/hierarchy' + +export const useSearchValueSet = (references: Reference[], selectedNodes: Hierarchy[]) => { + const researchParameters = useSearchParameters() + const explorationParameters = useSearchParameters() + const [searchInput, setSearchInput] = useState('') + const [mode, setMode] = useState(SearchMode.EXPLORATION) + const [initialized, setInitialized] = useState({ exploration: false, research: false }) + const dispatch = useAppDispatch() + + const fetchChildren = useCallback( + async (ids: string, system: string) => (await getChildrenFromCodes(system, ids.split(','))).results, + [] + ) + + const handleSaveCodes = useCallback((codes: Codes>) => { + const entities = mapCodesToCache(codes) + dispatch(saveValueSets(entities)) + }, []) + + const urls = useMemo(() => { + return references.map((ref) => ref.url) + }, [references]) + + const codes = useAppSelector((state) => selectValueSetCodes(state, urls)) + + const { hierarchies, searchResults, selectedCodes, loadingStatus, initTrees, fetchMore, expand, select } = + useHierarchy(selectedNodes, codes, handleSaveCodes, fetchChildren) + + const controllerRef = useRef(null) + const currentHierarchy = useMemo(() => { + const ref = explorationParameters.options.references.find((ref) => ref.checked) + return ( + hierarchies.get(ref?.url || '') || { + tree: [], + count: 0, + page: 1, + system: '' + } + ) + }, [hierarchies, explorationParameters.options.references]) + + const selectAllStatus = useMemo(() => { + const node = { + id: 'parent', + subItems: mode === SearchMode.EXPLORATION ? currentHierarchy.tree : searchResults.tree + } as Hierarchy + return getItemSelectedStatus(node) + }, [currentHierarchy, searchResults, mode]) + + const isSelectionDisabled = useCallback( + (node: Hierarchy) => { + const isAll = selectedCodes.get(node.system)?.get(HIERARCHY_ROOT) ? true : false + if (mode === SearchMode.RESEARCH && isAll) return true + else { + const ref = explorationParameters.options.references.find((ref) => ref.checked) + if (ref && !ref.isHierarchy && isAll) return true + } + return false + }, + [explorationParameters.options.references, mode, selectedCodes] + ) + + const selected = useMemo(() => { + return [...selectedCodes.values()].flatMap((innerMap) => [...innerMap.values()]) + }, [selectedCodes]) + + const fetchSearch = async (searchInput: string, page: number, references: string[]) => { + if (references.length) { + return await searchInValueSets(references, searchInput, page * LIMIT_PER_PAGE, LIMIT_PER_PAGE) + } else return { results: [], count: 0 } + } + + const fetchBaseTree = async (ref: Reference) => { + const fetch = ref.isHierarchy + ? () => getHierarchyRoots(ref.url, ref.title, ref.filterRoots) + : () => searchInValueSets([ref.url], '', 0, LIMIT_PER_PAGE) + try { + return await fetch() + } catch (error) { + return { count: 0, results: [] } + } + } + + const initExploration = (references: Reference[]) => { + const hierachyReferences = references.map((ref, index) => ({ ...ref, checked: index === 0 })) + const initHandlers = references.map((ref) => ({ + system: ref.url, + fetchBaseTree: () => fetchBaseTree(ref) + })) + explorationParameters.onChangeReferences(hierachyReferences) + setInitialized({ ...initialized, exploration: true }) + initTrees(initHandlers) + } + + const initResearch = (references: Reference[]) => { + researchParameters.onChangeReferences(references) + const searchRefs = references.map((ref) => ref.url) + const searchCb = () => fetchSearch('', 0, searchRefs) + setInitialized({ ...initialized, research: true }) + fetchMore(searchCb, 1, SearchMode.RESEARCH) + } + + const getSearchParameter = (param: keyof SearchParameters) => { + return mode === SearchMode.EXPLORATION ? explorationParameters.options[param] : researchParameters.options[param] + } + + const handleDeleteSelectedCodes = (code: Hierarchy) => { + const isRoot = code.id === HIERARCHY_ROOT + const toSelect = isRoot ? hierarchies.get(code.system)?.tree || [] : [code] + const mode = isRoot ? Mode.UNSELECT_ALL : Mode.UNSELECT + select(toSelect, mode) + } + + const handleChangeReferences = (references: Reference[]) => { + if (mode === SearchMode.EXPLORATION) explorationParameters.onChangeReferences(references) + else { + researchParameters.onChangeReferences(references) + const refs = references.filter((ref) => ref.checked).map((ref) => ref.url) + fetchMore(() => fetchSearch(searchInput, 0, refs), 1, mode) + } + } + + const search = (newSearchInput: string) => { + const refs = researchParameters.options.references.filter((ref) => ref.checked).map((ref) => ref.url) + fetchMore(() => fetchSearch(newSearchInput, 0, refs), 1, SearchMode.RESEARCH) + } + + const handleChangePage = (page: number) => { + const refs = + mode === SearchMode.EXPLORATION + ? explorationParameters.options.references.filter((ref) => ref.checked).map((ref) => ref.url) + : researchParameters.options.references.filter((ref) => ref.checked).map((ref) => ref.url) + const input = mode === SearchMode.EXPLORATION ? '' : searchInput + fetchMore(() => fetchSearch(input, page - 1, refs), page, mode, refs?.[0]) + } + + const debouncedSearch = useDebounceAction(search, 500) + const handleChangeSearchInput = (newInput: string) => { + setSearchInput(newInput) + debouncedSearch(newInput) + } + + useEffect(() => { + if (mode === SearchMode.EXPLORATION && !initialized.exploration) initExploration(references) + if (mode === SearchMode.RESEARCH && !initialized.research) initResearch(references) + }, [mode]) + + return { + selectedCodes: selected, + isSelectionDisabled, + mode, + loadingStatus, + searchInput, + onChangeMode: setMode, + onDelete: handleDeleteSelectedCodes, + parameters: { + refs: getSearchParameter('references') as Reference[], + onChangeSearchInput: handleChangeSearchInput, + onChangePage: handleChangePage, + onChangeReferences: handleChangeReferences + }, + hierarchy: { + exploration: currentHierarchy, + research: searchResults, + selectAllStatus, + expand, + select + } + } +} diff --git a/src/mappers/filters.ts b/src/mappers/filters.ts index 6a43e936a..192dac6c3 100644 --- a/src/mappers/filters.ts +++ b/src/mappers/filters.ts @@ -30,12 +30,10 @@ import { convertStringToDuration, convertTimestampToDuration } from 'utils/age' -import { fetchClaimCodes, fetchConditionCodes, fetchProcedureCodes } from 'services/aphp/servicePmsi' -import { fetchAnabioCodes, fetchLoincCodes } from 'services/aphp/serviceBiology' -import services from 'services/aphp' import servicesPerimeters from 'services/aphp/servicePerimeters' import { Hierarchy } from 'types/hierarchy' import { getConfig } from 'config' +import { HIERARCHY_ROOT, getChildrenFromCodes, getCodeList, getHierarchyRoots } from 'services/aphp/serviceValueSets' export enum PatientsParamsKeys { GENDERS = 'gender', @@ -121,6 +119,7 @@ export enum AdministrationParamsKeys { NDA = 'context.identifier', ADMINISTRATION_ROUTES = 'dosage-route', DATE = 'effective-time', + CODE = 'code', EXECUTIVE_UNITS = 'context.encounter-care-site', ENCOUNTER_STATUS = 'context.status' } @@ -133,7 +132,8 @@ export enum ObservationParamsKeys { VALUE = 'value-quantity', EXECUTIVE_UNITS = 'encounter.encounter-care-site', ENCOUNTER_STATUS = 'encounter.status', - IPP = 'subject.identifier' + IPP = 'subject.identifier', + CODE = 'code' } export enum ImagingParamsKeys { @@ -192,6 +192,32 @@ const getGenericKeyFromResourceType = ( return '' } +const getValueSetCodes = async (parameters: URLSearchParams, key: string) => { + const codeIds = decodeURIComponent(parameters.get(key) ?? '') + const urlMap: Map = new Map() + console.log('test filter', codeIds) + if (codeIds) + codeIds.split(',').forEach((str) => { + const [url, id] = str.split('|') + const isFound = urlMap.get(url) + if (isFound) urlMap.set(url, [...isFound, id]) + else urlMap.set(url, [id]) + }) + try { + return ( + await Promise.all( + [...urlMap.entries()].map(([key, value]) => + value?.[0] === HIERARCHY_ROOT + ? getHierarchyRoots(key, 'Toute la hiérarchie') + : getChildrenFromCodes(key, value) + ) + ) + ).flatMap((res) => res.results) + } catch (error) { + return [] + } +} + const mapGenericFromRequestParams = async (parameters: URLSearchParams, type: ResourceType) => { const nda = decodeURIComponent(parameters.get(getGenericKeyFromResourceType(type, 'NDA')) ?? '') const dates = parameters.getAll(getGenericKeyFromResourceType(type, 'DATE')) @@ -208,7 +234,7 @@ const mapGenericFromRequestParams = async (parameters: URLSearchParams, type: Re ) let encounterStatus: LabelObject[] = [] if (encounterStatusParams) { - const allEncounterStatus = await services.cohortCreation.fetchEncounterStatus() + const allEncounterStatus = (await getCodeList(getConfig().core.valueSets.encounterStatus.url)).results encounterStatus = encounterStatusParams?.split(',')?.map((elem) => { return { id: elem.split('|')?.[1], @@ -266,22 +292,14 @@ const mapDocumentsFromRequestParams = async (parameters: URLSearchParams) => { } const mapConditionFromRequestParams = async (parameters: URLSearchParams) => { - const codeIds = - decodeURIComponent(parameters.get(ConditionParamsKeys.CODE) ?? '') - ?.split(',') - ?.map((e) => e.split('|')?.[1]) - ?.join(',') || '' - const fetchCodesResults = await fetchConditionCodes(codeIds, true) - const code = fetchCodesResults.map((e) => { - return { id: e.id, label: e.label } - }) + const code = await getValueSetCodes(parameters, ConditionParamsKeys.CODE) const diagnosticTypesParams = decodeURIComponent(parameters.get(ConditionParamsKeys.DIAGNOSTIC_TYPES) ?? '') let diagnosticTypes: LabelObject[] = [] if (diagnosticTypesParams) { - const allDiagnosticTypes = await services.cohortCreation.fetchDiagnosticTypes() + const allDiagnosticTypes = await getCodeList(getConfig().features.condition.valueSets.conditionStatus.url) diagnosticTypes = diagnosticTypesParams?.split(',')?.map((elem) => { const toParse = elem.split('|')?.[1] - return { id: toParse, label: allDiagnosticTypes.find((diag) => diag.id === toParse)?.label || '' } + return { id: toParse, label: (allDiagnosticTypes.results || []).find((diag) => diag.id === toParse)?.label || '' } }) } const source = parameters.get(ConditionParamsKeys.SOURCE) ?? '' @@ -293,15 +311,7 @@ const mapConditionFromRequestParams = async (parameters: URLSearchParams) => { } const mapProcedureFromRequestParams = async (parameters: URLSearchParams) => { - const codeIds = - decodeURIComponent(parameters.get(ProcedureParamsKeys.CODE) ?? '') - ?.split(',') - ?.map((e) => e.split('|')?.[1]) - ?.join(',') || '' - const fetchCodesResults = await fetchProcedureCodes(codeIds, true) - const code = fetchCodesResults.map((e) => { - return { id: e.id, label: e.label } - }) + const code = await getValueSetCodes(parameters, ProcedureParamsKeys.CODE) const source = parameters.get(ProcedureParamsKeys.SOURCE) ?? '' const { nda, startDate, endDate, executiveUnits, encounterStatus } = await mapGenericFromRequestParams( parameters, @@ -311,15 +321,7 @@ const mapProcedureFromRequestParams = async (parameters: URLSearchParams) => { } const mapClaimFromRequestParams = async (parameters: URLSearchParams) => { - const codeIds = - decodeURIComponent(parameters.get(ClaimParamsKeys.CODE) ?? '') - ?.split(',') - ?.map((e) => e.split('|')?.[1]) - ?.join(',') || '' - const fetchCodesResults = await fetchClaimCodes(codeIds, true) - const code = fetchCodesResults.map((e) => { - return { id: e.id, label: e.label } - }) + const code = await getValueSetCodes(parameters, ClaimParamsKeys.CODE) const { nda, startDate, endDate, executiveUnits, encounterStatus } = await mapGenericFromRequestParams( parameters, ResourceType.CLAIM @@ -331,7 +333,7 @@ const mapPrescriptionFromRequestParams = async (parameters: URLSearchParams) => const prescriptionTypesParam = decodeURIComponent(parameters.get(PrescriptionParamsKeys.PRESCRIPTION_TYPES) ?? '') let prescriptionTypes: LabelObject[] = [] if (prescriptionTypesParam) { - const types = await services.cohortCreation.fetchPrescriptionTypes() + const types = (await getCodeList(getConfig().features.medication.valueSets.medicationPrescriptionTypes.url)).results prescriptionTypes = prescriptionTypesParam?.split(',')?.map((elem) => { return { id: elem.split('|')?.[1], label: types.find((type) => type.id === elem.split('|')?.[1])?.label || '' } }) @@ -340,7 +342,8 @@ const mapPrescriptionFromRequestParams = async (parameters: URLSearchParams) => parameters, ResourceType.MEDICATION_REQUEST ) - return { prescriptionTypes, nda, startDate, endDate, executiveUnits, encounterStatus } + const code = await getValueSetCodes(parameters, PrescriptionParamsKeys.CODE) + return { code, prescriptionTypes, nda, startDate, endDate, executiveUnits, encounterStatus } } const mapAdministrationFromRequestParams = async (parameters: URLSearchParams) => { @@ -349,44 +352,27 @@ const mapAdministrationFromRequestParams = async (parameters: URLSearchParams) = ) let administrationRoutes: LabelObject[] = [] if (administrationRoutesParam) { - const routes = await services.cohortCreation.fetchAdministrations() + const routes = (await getCodeList(getConfig().features.medication.valueSets.medicationAdministrations.url)).results administrationRoutes = administrationRoutesParam?.split(',')?.map((elem) => { return { id: elem.split('|')?.[1], label: routes.find((route) => route.id === elem.split('|')?.[1])?.label || '' } }) } + const code = await getValueSetCodes(parameters, AdministrationParamsKeys.CODE) const { nda, startDate, endDate, executiveUnits, encounterStatus } = await mapGenericFromRequestParams( parameters, ResourceType.MEDICATION_ADMINISTRATION ) - return { administrationRoutes, nda, startDate, endDate, executiveUnits, encounterStatus } + return { code, administrationRoutes, nda, startDate, endDate, executiveUnits, encounterStatus } } const mapBiologyFromRequestParams = async (parameters: URLSearchParams) => { - const anabioLoinc = parameters.get(ObservationParamsKeys.ANABIO_LOINC)?.split(',') || [] - - const anabioIds = anabioLoinc - ?.filter((e) => e.includes(getConfig().features.observation.valueSets.biologyHierarchyAnabio.url)) - ?.map((e) => e.split('|')?.[1]) - ?.join(',') - const fetchAnabioResults = await fetchAnabioCodes(anabioIds, true) - const anabio = fetchAnabioResults.map((e) => { - return { id: e.id, label: e.label } - }) - const loincIds = anabioLoinc - ?.filter((e) => e.includes(getConfig().features.observation.valueSets.biologyHierarchyLoinc.url)) - ?.map((e) => e.split('|')?.[1]) - ?.join(',') - const fetchLoincResults = await fetchLoincCodes(loincIds, true) - const loinc = fetchLoincResults.map((e) => { - return { id: e.id, label: e.label } - }) - + const code = await getValueSetCodes(parameters, ObservationParamsKeys.CODE) const validatedStatus = true const { nda, startDate, endDate, executiveUnits, encounterStatus } = await mapGenericFromRequestParams( parameters, ResourceType.OBSERVATION ) - return { loinc, anabio, validatedStatus, nda, startDate, endDate, executiveUnits, encounterStatus } + return { code, validatedStatus, nda, startDate, endDate, executiveUnits, encounterStatus } } const mapImagingFromRequestParams = async (parameters: URLSearchParams) => { @@ -394,7 +380,8 @@ const mapImagingFromRequestParams = async (parameters: URLSearchParams) => { const ipp = decodeURIComponent(parameters.get(ImagingParamsKeys.IPP) ?? '') let modality: LabelObject[] = [] if (modalityParams) { - const allModalities = await services.cohortCreation.fetchModalities() + const allModalities = (await getCodeList(getConfig().features.imaging.valueSets.imagingModalities.url, true)) + .results modality = modalityParams?.split(',')?.map((elem) => { return { id: elem.split('|')?.[1], @@ -514,11 +501,9 @@ const mapConditionToRequestParams = (filters: PMSIFilters) => { const urlString = diagnosticTypes.map((elem) => diagnosticTypesUrl + elem.id).join(',') requestParams.push(`${ConditionParamsKeys.DIAGNOSTIC_TYPES}=${encodeURIComponent(urlString)}`) } - if (code && code.length > 0) + if (code.length) requestParams.push( - `${ConditionParamsKeys.CODE}=${encodeURIComponent( - code.map((e) => `${getConfig().features.condition.valueSets.conditionHierarchy.url}|${e.id}`).join(',') - )}` + `${ConditionParamsKeys.CODE}=${encodeURIComponent(code.map((e) => `${e.system}|${e.id}`).join(','))}` ) if (source) requestParams.push(`${ProcedureParamsKeys.SOURCE}=${source}`) requestParams.push( @@ -530,11 +515,9 @@ const mapConditionToRequestParams = (filters: PMSIFilters) => { const mapClaimToRequestParams = (filters: PMSIFilters) => { const { code, nda, endDate, startDate, executiveUnits, encounterStatus } = filters const requestParams: string[] = [] - if (code && code.length > 0) + if (code.length) requestParams.push( - `${ClaimParamsKeys.CODE}=${encodeURIComponent( - code.map((e) => `${getConfig().features.claim.valueSets.claimHierarchy.url}|${e.id}`).join(',') - )}` + `${ClaimParamsKeys.CODE}=${encodeURIComponent(code.map((e) => `${e.system}|${e.id}`).join(','))}` ) requestParams.push( ...mapGenericToRequestParams({ nda, startDate, endDate, executiveUnits, encounterStatus }, ResourceType.CLAIM) @@ -545,11 +528,9 @@ const mapClaimToRequestParams = (filters: PMSIFilters) => { const mapProcedureToRequestParams = (filters: PMSIFilters) => { const { source, code, nda, endDate, startDate, executiveUnits, encounterStatus } = filters const requestParams: string[] = [] - if (code && code.length > 0) + if (code.length) requestParams.push( - `${ProcedureParamsKeys.CODE}=${encodeURIComponent( - code.map((e) => `${getConfig().features.procedure.valueSets.procedureHierarchy.url}|${e.id}`).join(',') - )}` + `${ProcedureParamsKeys.CODE}=${encodeURIComponent(code.map((e) => `${e.system}|${e.id}`).join(','))}` ) if (source) requestParams.push(`${ProcedureParamsKeys.SOURCE}=${source}`) requestParams.push( @@ -559,13 +540,17 @@ const mapProcedureToRequestParams = (filters: PMSIFilters) => { } const mapPrescriptionToRequestParams = (filters: MedicationFilters) => { - const { prescriptionTypes, nda, endDate, startDate, executiveUnits, encounterStatus } = filters + const { code, prescriptionTypes, nda, endDate, startDate, executiveUnits, encounterStatus } = filters const requestParams: string[] = [] if (prescriptionTypes && prescriptionTypes.length > 0) { const prescriptionTypesUrl = `${getConfig().features.medication.valueSets.medicationPrescriptionTypes.url}|` const urlString = prescriptionTypes.map((elem) => prescriptionTypesUrl + elem.id).join(',') requestParams.push(`${PrescriptionParamsKeys.PRESCRIPTION_TYPES}=${encodeURIComponent(urlString)}`) } + if (code.length > 0) + requestParams.push( + `${PrescriptionParamsKeys.CODE}=${encodeURIComponent(code.map((e) => `${e.system}|${e.id}`).join(','))}` + ) requestParams.push( ...mapGenericToRequestParams( { nda, startDate, endDate, executiveUnits, encounterStatus }, @@ -576,13 +561,17 @@ const mapPrescriptionToRequestParams = (filters: MedicationFilters) => { } const mapAdministrationToRequestParams = (filters: MedicationFilters) => { - const { administrationRoutes, nda, endDate, startDate, executiveUnits, encounterStatus } = filters + const { code, administrationRoutes, nda, endDate, startDate, executiveUnits, encounterStatus } = filters const requestParams: string[] = [] if (administrationRoutes && administrationRoutes.length > 0) { const administrationRoutesUrl = `${getConfig().features.medication.valueSets.medicationAdministrations.url}|` const urlString = administrationRoutes.map((elem) => administrationRoutesUrl + elem.id).join(',') requestParams.push(`${AdministrationParamsKeys.ADMINISTRATION_ROUTES}=${encodeURIComponent(urlString)}`) } + if (code.length > 0) + requestParams.push( + `${PrescriptionParamsKeys.CODE}=${encodeURIComponent(code.map((e) => `${e.system}|${e.id}`).join(','))}` + ) requestParams.push( ...mapGenericToRequestParams( { nda, startDate, endDate, executiveUnits, encounterStatus }, @@ -593,34 +582,12 @@ const mapAdministrationToRequestParams = (filters: MedicationFilters) => { } const mapBiologyToRequestParams = (filters: BiologyFilters) => { - const { anabio, loinc, validatedStatus, nda, endDate, startDate, executiveUnits, encounterStatus } = filters + const { code, validatedStatus, nda, endDate, startDate, executiveUnits, encounterStatus } = filters const requestParams: string[] = ['value-quantity=ge0,le0', 'subject.active=true'] - if ((anabio && anabio.length > 0) || (loinc && loinc.length > 0)) { - const key = `${ObservationParamsKeys.ANABIO_LOINC}=` - let _anabio = '' - let _loinc = '' - - if (anabio && anabio.length > 0) { - _anabio = anabio - .map((e) => `${getConfig().features.observation.valueSets.biologyHierarchyAnabio.url}|` + e.id) - .join(',') - } - if (loinc && loinc.length > 0) { - _loinc = loinc - .map((e) => `${getConfig().features.observation.valueSets.biologyHierarchyLoinc.url}|` + e.id) - .join(',') - } - - _anabio && _loinc - ? requestParams.push( - `${key}${encodeURIComponent(_anabio)}${encodeURIComponent(',')}${encodeURIComponent(_loinc)}` - ) - : _anabio && !_loinc - ? requestParams.push(`${key}${encodeURIComponent(_anabio)}`) - : _loinc && !_anabio - ? requestParams.push(`${key}${encodeURIComponent(_loinc)}`) - : '' - } + if (code.length) + requestParams.push( + `${ObservationParamsKeys.CODE}=${encodeURIComponent(code.map((e) => `${e.system}|${e.id}`).join(','))}` + ) if (validatedStatus) requestParams.push(`${ObservationParamsKeys.VALIDATED_STATUS}=VAL`) requestParams.push( ...mapGenericToRequestParams({ nda, startDate, endDate, executiveUnits, encounterStatus }, ResourceType.OBSERVATION) diff --git a/src/reducers/searchCriteriasReducer.ts b/src/reducers/searchCriteriasReducer.ts index ff60818f7..7190c1ee1 100644 --- a/src/reducers/searchCriteriasReducer.ts +++ b/src/reducers/searchCriteriasReducer.ts @@ -91,6 +91,7 @@ export const initMedSearchCriterias: SearchCriterias = { filters: { nda: '', ipp: '', + code: [], startDate: null, endDate: null, executiveUnits: [], @@ -110,12 +111,11 @@ export const initBioSearchCriterias: SearchCriterias = { filters: { validatedStatus: true, nda: '', - loinc: [], - anabio: [], startDate: null, endDate: null, executiveUnits: [], - encounterStatus: [] + encounterStatus: [], + code: [] } } diff --git a/src/services/aphp/callApi.ts b/src/services/aphp/callApi.ts index 5fbf1c15d..2ce0b96cc 100644 --- a/src/services/aphp/callApi.ts +++ b/src/services/aphp/callApi.ts @@ -11,8 +11,7 @@ import { Cohort, DataRights, CohortRights, - UserAccesses, - HierarchyElementWithSystem + UserAccesses } from 'types' import { AxiosError, AxiosResponse } from 'axios' @@ -23,7 +22,6 @@ import { Condition, DocumentReference, Encounter, - Extension, ImagingStudy, Location, MedicationAdministration, @@ -35,12 +33,8 @@ import { Patient, Procedure, Questionnaire, - QuestionnaireResponse, - ValueSet + QuestionnaireResponse } from 'fhir/r4' -import { getApiResponseResourceOrThrow, getApiResponseResourcesOrThrow } from 'utils/apiHelpers' -import { idSort, labelSort } from 'utils/alphabeticalSort' -import { capitalizeFirstLetter } from 'utils/capitalize' import { Direction, Order, SavedFilter, SavedFiltersResults, SearchByTypes } from 'types/searchCriterias' import { AdministrationParamsKeys, @@ -56,8 +50,6 @@ import { QuestionnaireResponseParamsKeys } from '../../mappers/filters' import { ResourceType } from 'types/requestCriterias' -import { Hierarchy } from 'types/hierarchy' -import { getExtension } from 'utils/fhir' import { getConfig } from 'config' const paramValuesReducer = (accumulator: string, currentValue: string): string => @@ -67,7 +59,7 @@ const paramsReducer = (accumulator: string, currentValue: string): string => const uniq = (item: string, index: number, array: string[]) => array.indexOf(item) === index && item -const lowToleranceTag = encodeURIComponent('https://terminology.eds.aphp.fr/text-fault-tolerant|LOW') +export const LOW_TOLERANCE_TAG = encodeURIComponent('https://terminology.eds.aphp.fr/text-fault-tolerant|LOW') /** * Patient Resource @@ -489,7 +481,7 @@ export const fetchProcedure = async (args: fetchProcedureProps): FHIR_Bundle_Pro if (subject) options = [...options, `subject=${subject}`] // eslint-disable-line if (code) options = [...options, `${ProcedureParamsKeys.CODE}=${code}`] // eslint-disable-line if (source) options = [...options, `${ProcedureParamsKeys.SOURCE}=${source}`] - if (_text) options = [...options, `_text=${encodeURIComponent(_text)}&_tag=${lowToleranceTag}`] + if (_text) options = [...options, `_text=${encodeURIComponent(_text)}&_tag=${LOW_TOLERANCE_TAG}`] if (status) options = [...options, `status=${encodeURIComponent(`${docStatusCodeSystem}|${status}`)}`] if (encounterIdentifier) options = [...options, `${ProcedureParamsKeys.NDA}=${encounterIdentifier}`] if (patientIdentifier) options = [...options, `${ProcedureParamsKeys.IPP}=${patientIdentifier}`] @@ -561,7 +553,7 @@ export const fetchClaim = async (args: fetchClaimProps): FHIR_Bundle_Promise_Res if (_sort) options = [...options, `_sort=${_sortDirection}${_sort},id`] // eslint-disable-line if (patient) options = [...options, `patient=${patient}`] // eslint-disable-line if (diagnosis) options = [...options, `${ClaimParamsKeys.CODE}=${diagnosis}`] // eslint-disable-line - if (_text) options = [...options, `_text=${encodeURIComponent(_text)}&_tag=${lowToleranceTag}`] // eslint-disable-line + if (_text) options = [...options, `_text=${encodeURIComponent(_text)}&_tag=${LOW_TOLERANCE_TAG}`] // eslint-disable-line if (encounterIdentifier) options = [...options, `${ClaimParamsKeys.NDA}=${encounterIdentifier}`] if (patientIdentifier) options = [...options, `${ClaimParamsKeys.IPP}=${patientIdentifier}`] if (minCreated) options = [...options, `${ClaimParamsKeys.DATE}=ge${minCreated}`] @@ -628,7 +620,7 @@ export const fetchCondition = async (args: fetchConditionProps): FHIR_Bundle_Pro if (subject) options = [...options, `subject=${subject}`] // eslint-disable-line if (code) options = [...options, `${ConditionParamsKeys.CODE}=${code}`] // eslint-disable-line if (source) options = [...options, `${ConditionParamsKeys.SOURCE}=${source}`] - if (_text) options = [...options, `_text=${encodeURIComponent(_text)}&_tag=${lowToleranceTag}`] // eslint-disable-line + if (_text) options = [...options, `_text=${encodeURIComponent(_text)}&_tag=${LOW_TOLERANCE_TAG}`] // eslint-disable-line if (encounterIdentifier) options = [...options, `${ConditionParamsKeys.NDA}=${encounterIdentifier}`] // eslint-disable-line if (patientIdentifier) options = [...options, `${ConditionParamsKeys.IPP}=${patientIdentifier}`] if (minRecordedDate) options = [...options, `${ConditionParamsKeys.DATE}=ge${minRecordedDate}`] // eslint-disable-line @@ -662,8 +654,7 @@ type fetchObservationProps = { sortDirection?: Direction _text?: string encounter?: string - loinc?: string - anabio?: string + code?: string subject?: string minDate?: string maxDate?: string @@ -684,8 +675,7 @@ export const fetchObservation = async (args: fetchObservationProps): FHIR_Bundle sortDirection, _text, encounter, - loinc, - anabio, + code, subject, minDate, maxDate, @@ -707,13 +697,9 @@ export const fetchObservation = async (args: fetchObservationProps): FHIR_Bundle if (size !== undefined) options = [...options, `_count=${size}`] if (offset) options = [...options, `_offset=${offset}`] if (_sort) options = [...options, `_sort=${_sortDirection}${_sort.includes('code') ? _sort : `${_sort},id`}`] - if (_text) options = [...options, `_text=${encodeURIComponent(_text)}&_tag=${lowToleranceTag}`] + if (_text) options = [...options, `_text=${encodeURIComponent(_text)}&_tag=${LOW_TOLERANCE_TAG}`] if (encounter) options = [...options, `${ObservationParamsKeys.NDA}=${encounter}`] - if (anabio || loinc) - options = [ - ...options, - `${ObservationParamsKeys.ANABIO_LOINC}=${anabio ? anabio : ''}${anabio && loinc ? ',' : ''}${loinc ? loinc : ''}` - ] // eslint-disable-line + if (code) options = [...options, `${ObservationParamsKeys.ANABIO_LOINC}=${code}`] // eslint-disable-line if (subject) options = [...options, `subject=${subject}`] // eslint-disable-line if (minDate) options = [...options, `${ObservationParamsKeys.DATE}=ge${minDate}`] // eslint-disable-line if (maxDate) options = [...options, `${ObservationParamsKeys.DATE}=le${maxDate}`] // eslint-disable-line @@ -747,6 +733,7 @@ type fetchMedicationRequestProps = { _text?: string encounter?: string patientIds?: string + code?: string subject?: string type?: string[] minDate: string | null @@ -762,6 +749,7 @@ export const fetchMedicationRequest = async ( ): FHIR_Bundle_Promise_Response => { const { id, + code, size, offset, _sort, @@ -790,12 +778,13 @@ export const fetchMedicationRequest = async ( if (_sort) options = [...options, `_sort=${_sortDirection}${_sort},id`] if (subject) options = [...options, `subject=${subject}`] if (encounter) options = [...options, `${PrescriptionParamsKeys.NDA}=${encounter}`] - if (_text) options = [...options, `_text=${encodeURIComponent(_text)}&_tag=${lowToleranceTag}`] + if (_text) options = [...options, `_text=${encodeURIComponent(_text)}&_tag=${LOW_TOLERANCE_TAG}`] if (type && type.length > 0) { const routeUrl = `${getConfig().features.medication.valueSets.medicationPrescriptionTypes.url}|` const urlString = type.map((id) => routeUrl + id).join(',') options = [...options, `${PrescriptionParamsKeys.PRESCRIPTION_TYPES}=${encodeURIComponent(urlString)}`] } + if (code) if (code) options = [...options, `${PrescriptionParamsKeys.CODE}=${code}`] if (minDate) options = [...options, `${PrescriptionParamsKeys.DATE}=ge${minDate}`] if (maxDate) options = [...options, `${PrescriptionParamsKeys.DATE}=le${maxDate}`] if (executiveUnits && executiveUnits.length > 0) @@ -822,6 +811,7 @@ type fetchMedicationAdministrationProps = { size?: number offset?: number _sort?: string + code?: string sortDirection?: Direction _text?: string encounter?: string @@ -840,6 +830,7 @@ export const fetchMedicationAdministration = async ( ): FHIR_Bundle_Promise_Response => { const { id, + code, size, offset, _sort, @@ -868,12 +859,13 @@ export const fetchMedicationAdministration = async ( if (_sort) options = [...options, `_sort=${_sortDirection}${_sort},id`] if (subject) options = [...options, `subject=${subject}`] if (encounter) options = [...options, `${AdministrationParamsKeys.NDA}=${encounter}`] - if (_text) options = [...options, `_text=${encodeURIComponent(_text)}&_tag=${lowToleranceTag}`] + if (_text) options = [...options, `_text=${encodeURIComponent(_text)}&_tag=${LOW_TOLERANCE_TAG}`] if (route && route.length > 0) { const routeUrl = `${getConfig().features.medication.valueSets.medicationAdministrations.url}|` const urlString = route.map((id) => routeUrl + id).join(',') options = [...options, `${AdministrationParamsKeys.ADMINISTRATION_ROUTES}=${encodeURIComponent(urlString)}`] } + if (code) if (code) options = [...options, `${AdministrationParamsKeys.CODE}=${code}`] if (minDate) options = [...options, `${AdministrationParamsKeys.DATE}=ge${minDate}`] // eslint-disable-line if (maxDate) options = [...options, `${AdministrationParamsKeys.DATE}=le${maxDate}`] // eslint-disable-line if (executiveUnits && executiveUnits.length > 0) @@ -941,7 +933,7 @@ export const fetchImaging = async (args: fetchImagingProps): FHIR_Bundle_Promise if (size !== undefined) options = [...options, `_count=${size}`] if (offset) options = [...options, `_offset=${offset}`] if (order) options = [...options, `_sort=${_orderDirection}${order}`] - if (_text) options = [...options, `_text=${encodeURIComponent(_text)}&_tag=${lowToleranceTag}`] + if (_text) options = [...options, `_text=${encodeURIComponent(_text)}&_tag=${LOW_TOLERANCE_TAG}`] if (encounter) options = [...options, `${ImagingParamsKeys.NDA}=${encounter}`] if (ipp) options = [...options, `patient.identifier=${ipp}`] if (minDate) options = [...options, `${ImagingParamsKeys.DATE}=ge${minDate}`] @@ -1048,135 +1040,6 @@ export const fetchLocation = async (args: fetchLocationProps) => { return response } -/** - * - * Retrieve the codeList from FHIR api either from expanding a code or fetching the roots of the valueSet - * @param codeSystem - * @param code - * @param search - * @param noStar - * @param signal - * @returns - */ -const getCodeList = async ( - codeSystem: string, - expandCode?: string, - search?: string, - noStar = true, - signal?: AbortSignal -): Promise<{ code?: string; display?: string; extension?: Extension[]; codeSystem?: string }[] | undefined> => { - if (!expandCode) { - if (search !== undefined && !search.trim()) { - return [] - } - let searchParam = '&only-roots=true' - // if search is * then we fetch the roots of the valueSet - if (search !== '*' && search !== undefined) { - // if noStar is true then we search for the code, else we search for the display - searchParam = `&only-roots=false&${ - noStar ? 'code' : `_tag=text-search-rank&_tag=${lowToleranceTag}&_text` - }=${encodeURIComponent(search.trim())}` - } - // TODO test if it returns all the codes without specifying the count - const res = await apiFhir.get>(`/ValueSet?reference=${codeSystem}${searchParam}`, { - signal: signal - }) - const valueSetBundle = getApiResponseResourcesOrThrow(res) - return valueSetBundle.length > 0 - ? valueSetBundle - .map((entry) => { - return ( - entry.compose?.include[0].concept?.map((code) => ({ - ...code, - codeSystem: entry.compose?.include[0].system - })) || [] - ) //eslint-disable-line - }) - .filter((valueSetPerSystem) => !!valueSetPerSystem) - .reduce((acc, val) => acc.concat(val), []) - : [] - } else { - const json = { - resourceType: 'ValueSet', - url: codeSystem, - compose: { - include: [ - { - filter: [ - { - op: 'is-a', - value: expandCode - } - ] - } - ] - } - } - const res = await apiFhir.post>(`/ValueSet/$expand`, JSON.stringify(json)) - const valueSetExpansion = getApiResponseResourceOrThrow(res).expansion - return valueSetExpansion?.contains?.map((code) => ({ ...code, codeSystem: codeSystem })) - } -} - -export type FetchValueSetOptions = { - valueSetTitle?: string - code?: string - sortingKey?: 'id' | 'label' - search?: string - noStar?: boolean - joinDisplayWithCode?: boolean - filterRoots?: (code: Hierarchy) => boolean - filterOut?: (code: Hierarchy) => boolean -} - -export const fetchValueSet = async ( - codeSystem: string, - options?: FetchValueSetOptions, - signal?: AbortSignal -): Promise => { - const { - code, - valueSetTitle, - sortingKey = 'label', - search, - noStar, - joinDisplayWithCode = true, - filterRoots = () => true, - filterOut = (value: Hierarchy) => value.id === 'APHP generated' - } = options || {} - const codeList = await getCodeList(codeSystem, code, search, noStar, signal) - const sortingFunc = sortingKey === 'id' ? idSort : labelSort - const formattedCodeList = - codeList - ?.map((code) => ({ - id: code.code || '', - label: joinDisplayWithCode - ? `${code.code} - ${capitalizeFirstLetter(code.display)}` - : capitalizeFirstLetter(code.display), - system: code.codeSystem, - subItems: [{ id: 'loading', label: 'loading', subItems: [] as Hierarchy[] }] - })) - .filter((code) => !filterOut(code)) - .sort(sortingFunc) || [] - if (!code && (search === undefined || search === '*') && valueSetTitle) { - return [{ id: '*', label: valueSetTitle, subItems: formattedCodeList.filter((code) => filterRoots(code)) }] - } else { - return formattedCodeList - } -} - -export const fetchSingleCodeHierarchy = async (codeSystem: string, code: string): Promise => { - const codeList = await getCodeList(codeSystem, undefined, code) - if (!codeList || codeList.length === 0) { - return [] - } - return ( - getExtension(codeList[0], getConfig().core.extensions.codeHierarchy) - ?.valueCodeableConcept?.coding?.map((c) => c.code || '') - .filter((c) => !!c) || [] - ) -} - export const fetchAccessExpirations: ( args: AccessExpirationsProps ) => Promise>> = async (args: AccessExpirationsProps) => { diff --git a/src/services/aphp/cohortCreation/fetchObservation.ts b/src/services/aphp/cohortCreation/fetchObservation.ts deleted file mode 100644 index a04e67410..000000000 --- a/src/services/aphp/cohortCreation/fetchObservation.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { displaySort } from 'utils/alphabeticalSort' -import apiFhir from 'services/apiFhir' -import { getApiResponseResources } from 'utils/apiHelpers' -import { FHIR_Bundle_Response, ValueSet } from 'types' -import { ConceptMap } from 'fhir/r4' -import { getExtension } from 'utils/fhir' -import { getConfig } from 'config' - -export type ValueSetWithHierarchy = ValueSet & { hierarchyDisplay: string } - -export const fetchBiologySearch = async ( - searchInput: string -): Promise<{ anabio: ValueSetWithHierarchy[]; loinc: ValueSetWithHierarchy[] }> => { - if (!searchInput) { - return { - anabio: [], - loinc: [] - } - } - - const lowerCaseTrimmedSearchInput = searchInput.toLowerCase().trim() - - const res = await apiFhir.get>( - `/ConceptMap?_count=2000&name=Maps%20to,Concept%20Fhir%20Maps%20To&source-system=${ - getConfig().features.observation.valueSets.biologyHierarchyAnabio.url - }&target-system=${getConfig().features.observation.valueSets.biologyHierarchyAnabio.url},${ - getConfig().features.observation.valueSets.biologyHierarchyLoinc.url - }&_text=${encodeURIComponent( - lowerCaseTrimmedSearchInput.replace(/[\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&') // eslint-disable-line - )}` - ) - - const data = getApiResponseResources(res) - - const loincResults = getSourceData(getConfig().features.observation.valueSets.biologyHierarchyLoinc.url, data).sort( - displaySort - ) - const uniqueLoincResults = getUniqueLoincResults(loincResults) - const anabioResults = getSourceData(getConfig().features.observation.valueSets.biologyHierarchyAnabio.url, data).sort( - displaySort - ) - - return { - anabio: anabioResults ?? [], - loinc: uniqueLoincResults ?? [] - } -} - -const getSourceData = (codeSystem: string, data?: ConceptMap[]): Array => { - const conceptMapHierarchyExtension = getConfig().core.extensions.conceptMapHierarchy - if (!data || data.length === 0) { - return [] - } - - return ( - data - .filter((element) => element.group?.[0].target === codeSystem) - .map( - (element) => - ({ - code: element.group?.[0].element?.[0].target?.[0].code, - display: element.group?.[0].element?.[0].target?.[0].display, - hierarchyDisplay: getExtension(element, conceptMapHierarchyExtension)?.valueString?.replaceAll( - /\d+-|\w\d+-/g, - '' - ) - } as ValueSetWithHierarchy) - ) - .filter((el) => !!el) ?? [] - ) -} - -const getUniqueLoincResults = (loincResults: ValueSetWithHierarchy[]) => { - if (loincResults.length === 0) { - return [] - } - - return loincResults.filter( - ( - (set) => (f) => - !set.has(f.code) && set.add(f.code) - )(new Set()) - ) -} diff --git a/src/services/aphp/mockServiceValueSet.ts b/src/services/aphp/mockServiceValueSet.ts new file mode 100644 index 000000000..7a3e36d0d --- /dev/null +++ b/src/services/aphp/mockServiceValueSet.ts @@ -0,0 +1,478 @@ +import { Back_API_Response } from 'types' +import { FhirHierarchy, Hierarchy, HierarchyWithLabel } from 'types/hierarchy' + +const nodes = [ + { + id: '1', + label: 'Aspirin', + system: 'https://terminology.eds.aphp.fr/atc', + above_levels_ids: '*', + inferior_levels_ids: '11,12' + }, + { + id: '2', + label: 'Ibuprofen', + system: 'https://terminology.eds.aphp.fr/atc', + above_levels_ids: '*', + inferior_levels_ids: '13,14' + }, + { + id: '3', + label: 'Paracetamol', + system: 'https://terminology.eds.aphp.fr/atc', + above_levels_ids: '*', + inferior_levels_ids: '15,16' + }, + { + id: '4', + label: 'Metformin', + system: 'https://terminology.eds.aphp.fr/atc', + above_levels_ids: '*', + inferior_levels_ids: '17,18' + }, + { + id: '5', + label: 'Lisinopril', + system: 'https://terminology.eds.aphp.fr/atc', + above_levels_ids: '*', + inferior_levels_ids: '19,20' + }, + { + id: '6', + label: 'Simvastatin', + system: 'https://terminology.eds.aphp.fr/atc', + above_levels_ids: '*', + inferior_levels_ids: '21,22' + }, + { + id: '7', + label: 'Amoxicillin', + system: 'https://terminology.eds.aphp.fr/atc', + above_levels_ids: '*', + inferior_levels_ids: '23,24' + }, + { + id: '8', + label: 'Azithromycin', + system: 'https://terminology.eds.aphp.fr/atc', + above_levels_ids: '*', + inferior_levels_ids: '25,26' + }, + { + id: '9', + label: 'Omeprazole', + system: 'https://terminology.eds.aphp.fr/atc', + above_levels_ids: '*', + inferior_levels_ids: '27,28' + }, + { + id: '10', + label: 'Hydrochlorothiazide', + system: 'https://terminology.eds.aphp.fr/atc', + above_levels_ids: '*', + inferior_levels_ids: '29,30' + }, + { + id: '11', + label: 'Aspirin 500mg', + system: 'https://terminology.eds.aphp.fr/atc', + above_levels_ids: '1', + inferior_levels_ids: '' + }, + { + id: '12', + label: 'Aspirin 100mg', + system: 'https://terminology.eds.aphp.fr/atc', + above_levels_ids: '1', + inferior_levels_ids: '' + }, + { + id: '13', + label: 'Ibuprofen 200mg', + system: 'https://terminology.eds.aphp.fr/atc', + above_levels_ids: '2', + inferior_levels_ids: '' + }, + { + id: '14', + label: 'Ibuprofen 400mg', + system: 'https://terminology.eds.aphp.fr/atc', + above_levels_ids: '2', + inferior_levels_ids: '' + }, + { + id: '15', + label: 'Paracetamol 500mg', + system: 'https://terminology.eds.aphp.fr/atc', + above_levels_ids: '3', + inferior_levels_ids: '' + }, + { + id: '16', + label: 'Paracetamol 1000mg', + system: 'https://terminology.eds.aphp.fr/atc', + above_levels_ids: '3', + inferior_levels_ids: '' + }, + { + id: '17', + label: 'Metformin 500mg', + system: 'https://terminology.eds.aphp.fr/atc', + above_levels_ids: '4', + inferior_levels_ids: '' + }, + { + id: '18', + label: 'Metformin 1000mg', + system: 'https://terminology.eds.aphp.fr/atc', + above_levels_ids: '4', + inferior_levels_ids: '' + }, + { + id: '19', + label: 'Lisinopril 10mg', + system: 'https://terminology.eds.aphp.fr/atc', + above_levels_ids: '5', + inferior_levels_ids: '' + }, + { + id: '20', + label: 'Lisinopril 20mg', + system: 'https://terminology.eds.aphp.fr/atc', + above_levels_ids: '5', + inferior_levels_ids: '' + }, + { + id: '21', + label: 'Simvastatin 10mg', + system: 'https://terminology.eds.aphp.fr/atc', + above_levels_ids: '6', + inferior_levels_ids: '' + }, + { + id: '22', + label: 'Simvastatin 20mg', + system: 'https://terminology.eds.aphp.fr/atc', + above_levels_ids: '6', + inferior_levels_ids: '' + }, + { + id: '23', + label: 'Amoxicillin 500mg', + system: 'https://terminology.eds.aphp.fr/atc', + above_levels_ids: '7', + inferior_levels_ids: '' + }, + { + id: '24', + label: 'Amoxicillin 1000mg', + system: 'https://terminology.eds.aphp.fr/atc', + above_levels_ids: '7', + inferior_levels_ids: '' + }, + { + id: '25', + label: 'Azithromycin 250mg', + system: 'https://terminology.eds.aphp.fr/atc', + above_levels_ids: '8', + inferior_levels_ids: '' + }, + { + id: '26', + label: 'Azithromycin 500mg', + system: 'https://terminology.eds.aphp.fr/atc', + above_levels_ids: '8', + inferior_levels_ids: '' + }, + { + id: '27', + label: 'Omeprazole 20mg', + system: 'https://terminology.eds.aphp.fr/atc', + above_levels_ids: '9', + inferior_levels_ids: '' + }, + { + id: '28', + label: 'Omeprazole 40mg', + system: 'https://terminology.eds.aphp.fr/atc', + above_levels_ids: '9', + inferior_levels_ids: '' + }, + { + id: '29', + label: 'Hydrochlorothiazide 12.5mg', + system: 'https://terminology.eds.aphp.fr/atc', + above_levels_ids: '10', + inferior_levels_ids: '' + }, + { + id: '30', + label: 'Hydrochlorothiazide 25mg', + system: 'https://terminology.eds.aphp.fr/atc', + above_levels_ids: '10', + inferior_levels_ids: '' + }, + { + id: '31', + label: 'Ciprofloxacin', + system: 'https://terminology.eds.aphp.fr/smt-medicament-ucd', + above_levels_ids: '*', + inferior_levels_ids: '41,42' + }, + { + id: '32', + label: 'Loratadine', + system: 'https://terminology.eds.aphp.fr/smt-medicament-ucd', + above_levels_ids: '*', + inferior_levels_ids: '43,44' + }, + { + id: '33', + label: 'Omeprazole', + system: 'https://terminology.eds.aphp.fr/smt-medicament-ucd', + above_levels_ids: '*', + inferior_levels_ids: '45,46' + }, + { + id: '34', + label: 'Metformin', + system: 'https://terminology.eds.aphp.fr/smt-medicament-ucd', + above_levels_ids: '*', + inferior_levels_ids: '47,48' + }, + { + id: '35', + label: 'Simvastatin', + system: 'https://terminology.eds.aphp.fr/smt-medicament-ucd', + above_levels_ids: '*', + inferior_levels_ids: '49,50' + }, + { + id: '36', + label: 'Amlodipine', + system: 'https://terminology.eds.aphp.fr/smt-medicament-ucd', + above_levels_ids: '*', + inferior_levels_ids: '51,52' + }, + { + id: '37', + label: 'Ibuprofen', + system: 'https://terminology.eds.aphp.fr/smt-medicament-ucd', + above_levels_ids: '*', + inferior_levels_ids: '53,54' + }, + { + id: '38', + label: 'Paracetamol', + system: 'https://terminology.eds.aphp.fr/smt-medicament-ucd', + above_levels_ids: '*', + inferior_levels_ids: '55,56' + }, + { + id: '39', + label: 'Candesartan', + system: 'https://terminology.eds.aphp.fr/smt-medicament-ucd', + above_levels_ids: '*', + inferior_levels_ids: '57,58' + }, + { + id: '40', + label: 'Clopidogrel', + system: 'https://terminology.eds.aphp.fr/smt-medicament-ucd', + above_levels_ids: '*', + inferior_levels_ids: '59,60' + }, + { + id: '41', + label: 'Ciprofloxacin 500mg', + system: 'https://terminology.eds.aphp.fr/smt-medicament-ucd', + above_levels_ids: '31', + inferior_levels_ids: '' + }, + { + id: '42', + label: 'Ciprofloxacin 750mg', + system: 'https://terminology.eds.aphp.fr/smt-medicament-ucd', + above_levels_ids: '31', + inferior_levels_ids: '' + }, + { + id: '43', + label: 'Loratadine 10mg', + system: 'https://terminology.eds.aphp.fr/smt-medicament-ucd', + above_levels_ids: '32', + inferior_levels_ids: '' + }, + { + id: '44', + label: 'Loratadine 5mg', + system: 'https://terminology.eds.aphp.fr/smt-medicament-ucd', + above_levels_ids: '32', + inferior_levels_ids: '' + }, + { + id: '45', + label: 'Omeprazole 20mg', + system: 'https://terminology.eds.aphp.fr/smt-medicament-ucd', + above_levels_ids: '33', + inferior_levels_ids: '' + }, + { + id: '46', + label: 'Omeprazole 40mg', + system: 'https://terminology.eds.aphp.fr/smt-medicament-ucd', + above_levels_ids: '33', + inferior_levels_ids: '' + }, + { + id: '47', + label: 'Metformin 500mg', + system: 'https://terminology.eds.aphp.fr/smt-medicament-ucd', + above_levels_ids: '34', + inferior_levels_ids: '' + }, + { + id: '48', + label: 'Metformin 1000mg', + system: 'https://terminology.eds.aphp.fr/smt-medicament-ucd', + above_levels_ids: '34', + inferior_levels_ids: '' + }, + { + id: '49', + label: 'Simvastatin 10mg', + system: 'https://terminology.eds.aphp.fr/smt-medicament-ucd', + above_levels_ids: '35', + inferior_levels_ids: '' + }, + { + id: '50', + label: 'Simvastatin 20mg', + system: 'https://terminology.eds.aphp.fr/smt-medicament-ucd', + above_levels_ids: '35', + inferior_levels_ids: '' + }, + { + id: '51', + label: 'Amlodipine 5mg', + system: 'https://terminology.eds.aphp.fr/smt-medicament-ucd', + above_levels_ids: '36', + inferior_levels_ids: '' + }, + { + id: '52', + label: 'Amlodipine 10mg', + system: 'https://terminology.eds.aphp.fr/smt-medicament-ucd', + above_levels_ids: '36', + inferior_levels_ids: '' + }, + { + id: '53', + label: 'Ibuprofen 200mg', + system: 'https://terminology.eds.aphp.fr/smt-medicament-ucd', + above_levels_ids: '37', + inferior_levels_ids: '' + }, + { + id: '54', + label: 'Ibuprofen 400mg', + system: 'https://terminology.eds.aphp.fr/smt-medicament-ucd', + above_levels_ids: '37', + inferior_levels_ids: '' + }, + { + id: '55', + label: 'Paracetamol 500mg', + system: 'https://terminology.eds.aphp.fr/smt-medicament-ucd', + above_levels_ids: '38', + inferior_levels_ids: '' + }, + { + id: '56', + label: 'Paracetamol 1000mg', + system: 'https://terminology.eds.aphp.fr/smt-medicament-ucd', + above_levels_ids: '38', + inferior_levels_ids: '' + }, + { + id: '57', + label: 'Candesartan 4mg', + system: 'https://terminology.eds.aphp.fr/smt-medicament-ucd', + above_levels_ids: '39', + inferior_levels_ids: '' + }, + { + id: '58', + label: 'Candesartan 8mg', + system: 'https://terminology.eds.aphp.fr/smt-medicament-ucd', + above_levels_ids: '39', + inferior_levels_ids: '' + }, + { + id: '59', + label: 'Clopidogrel 75mg', + system: 'https://terminology.eds.aphp.fr/smt-medicament-ucd', + above_levels_ids: '40', + inferior_levels_ids: '' + } +] + +export const getHierarchyRoots = ( + codeSystem: string, + valueSetTitle: string, + joinDisplayWithCode = true, + filterRoots: (code: HierarchyWithLabel) => boolean = () => true, + filterOut: (code: HierarchyWithLabel) => boolean = (value: HierarchyWithLabel) => value.id === 'APHP generated', + signal?: AbortSignal +) => { + if (codeSystem === 'https://terminology.eds.aphp.fr/atc') + return { + count: 1, + results: [ + { + id: '*', + label: 'Node ATC', + system: 'https://terminology.eds.aphp.fr/atc', + above_levels_ids: '', + inferior_levels_ids: '1,2,3,4,5,6,7,8,9,10' + } + ] + } + return { + count: 1, + results: [ + { + id: '*', + label: 'Node UCD', + system: 'https://terminology.eds.aphp.fr/smt-medicament-ucd', + above_levels_ids: '', + inferior_levels_ids: '31,32,33,34,35,36,37,38,39,40' + } + ] + } +} + +export const getChildrenFromCodes = async ( + codeSystem: string, + codes: string[], + signal?: AbortSignal +): Promise>> => { + const filtered = nodes.filter((node) => node.system === codeSystem && codes.includes(node.id)) + return { results: filtered, count: filtered.length } +} + +export const searchCodes = async ( + codeSystems: string[], + search: string, + offset: number, + count: number, + signal?: AbortSignal +): Promise>> => { + let filtered = [] + if (search) + filtered = nodes.filter( + (node) => codeSystems.includes(node.system) && node.label.toLowerCase().includes(search.toLowerCase()) + ) + else filtered = nodes.filter((node) => codeSystems.includes(node.system)) + return { results: filtered, count: filtered.length } +} diff --git a/src/services/aphp/serviceBiology.ts b/src/services/aphp/serviceBiology.ts deleted file mode 100644 index c8a24632a..000000000 --- a/src/services/aphp/serviceBiology.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { getConfig } from 'config' -import { fetchValueSet } from './callApi' - -export const fetchAnabioCodes = async (text: string, noStar = false, signal?: AbortSignal) => { - const response = await fetchValueSet( - getConfig().features.observation.valueSets.biologyHierarchyLoinc.url, - { joinDisplayWithCode: false, search: text, noStar: noStar }, - signal - ) - return response.map((elem) => { - return { ...elem, label: `${elem.id} - ${elem.label}` } - }) -} - -export const fetchLoincCodes = async (text: string, noStar = false, signal?: AbortSignal) => { - const response = await fetchValueSet( - getConfig().features.observation.valueSets.biologyHierarchyLoinc.url, - { joinDisplayWithCode: false, search: text, noStar: noStar }, - signal - ) - return response.map((elem) => { - return { ...elem, label: `${elem.id} - ${elem.label}` } - }) -} diff --git a/src/services/aphp/serviceCohortCreation.ts b/src/services/aphp/serviceCohortCreation.ts index c3acf1bc9..0250ad55d 100644 --- a/src/services/aphp/serviceCohortCreation.ts +++ b/src/services/aphp/serviceCohortCreation.ts @@ -1,23 +1,6 @@ import { AxiosResponse } from 'axios' import apiBack from '../apiBackend' - -import { - Cohort, - CountCohort, - DatedMeasure, - FetchRequest, - HierarchyElementWithSystem, - QuerySnapshotInfo, - RequestType, - SimpleCodeType, - Snapshot -} from 'types' -import docTypes from 'assets/docTypes.json' -import { ValueSetWithHierarchy, fetchBiologySearch } from './cohortCreation/fetchObservation' -import { fetchSingleCodeHierarchy, fetchValueSet } from './callApi' -import { VitalStatusLabel } from 'types/searchCriterias' -import { birthStatusData, booleanFieldsData, booleanOpenChoiceFieldsData, vmeData } from 'data/questionnaire_data' -import { Hierarchy } from 'types/hierarchy' +import { Cohort, CountCohort, DatedMeasure, FetchRequest, QuerySnapshotInfo, RequestType, Snapshot } from 'types' import { getConfig } from 'config' export interface IServiceCohortCreation { @@ -43,98 +26,19 @@ export interface IServiceCohortCreation { requestId?: string, uuid?: string ) => Promise - /** * Cette fonction permet de créer un état de `snapshot` pour l'historique d'une requête */ createSnapshot: (id: string, json: string, firstTime?: boolean) => Promise - /** * Cette fonction permet de faire une demande de rapport de faisabilité */ createReport: (id: string) => Promise - /** * Permet de récupérer toutes les informations utiles pour l'utilisation du requeteur */ fetchRequest: (requestId: string, snapshotId?: string) => Promise - fetchSnapshot: (snapshotId: string) => Promise - - fetchAdmissionModes: () => Promise[]> - fetchEntryModes: () => Promise[]> - fetchExitModes: () => Promise[]> - fetchPriseEnChargeType: () => Promise[]> - fetchTypeDeSejour: () => Promise[]> - fetchFileStatus: () => Promise[]> - fetchReason: () => Promise[]> - fetchDestination: () => Promise[]> - fetchProvenance: () => Promise[]> - fetchAdmission: () => Promise[]> - fetchGender: () => Promise[]> - fetchStatus: () => Promise[]> - fetchStatusDiagnostic: () => Promise[]> - fetchDiagnosticTypes: () => Promise[]> - fetchCim10Diagnostic: (searchValue?: string, noStar?: boolean, signal?: AbortSignal) => Promise[]> - fetchCim10Hierarchy: (cim10Parent?: string) => Promise[]> - fetchCcamData: (searchValue?: string, noStar?: boolean, signal?: AbortSignal) => Promise[]> - fetchCcamHierarchy: (ccamParent: string) => Promise[]> - fetchGhmData: (searchValue?: string, noStar?: boolean, signal?: AbortSignal) => Promise[]> - fetchGhmHierarchy: (ghmParent: string) => Promise[]> - fetchDocTypes: () => Promise - fetchMedicationData: ( - searchValue?: string, - noStar?: boolean, - signal?: AbortSignal - ) => Promise - fetchSingleCodeHierarchy: (resourceType: string, code: string) => Promise - fetchAtcHierarchy: (atcParent: string) => Promise[]> - fetchUCDList: (ucd?: string) => Promise[]> - fetchPrescriptionTypes: () => Promise[]> - fetchAdministrations: () => Promise[]> - fetchBiologyData: () => Promise[]> - fetchBiologyHierarchy: (biologyParent?: string) => Promise[]> - fetchBiologySearch: ( - searchInput: string - ) => Promise<{ anabio: ValueSetWithHierarchy[]; loinc: ValueSetWithHierarchy[] }> - fetchModalities: () => Promise[]> - fetchPregnancyMode: () => Promise[]> - fetchMaternalRisks: () => Promise[]> - fetchRisksRelatedToObstetricHistory: () => Promise[]> - fetchRisksOrComplicationsOfPregnancy: () => Promise[]> - fetchCorticotherapie: () => Promise[]> - fetchPrenatalDiagnosis: () => Promise[]> - fetchUltrasoundMonitoring: () => Promise[]> - fetchInUteroTransfer: () => Promise[]> - fetchPregnancyMonitoring: () => Promise[]> - fetchMaturationCorticotherapie: () => Promise[]> - fetchChirurgicalGesture: () => Promise[]> - fetchVme: () => Promise[]> - fetchChildbirth: () => Promise[]> - fetchHospitalChildBirthPlace: () => Promise[]> - fetchOtherHospitalChildBirthPlace: () => Promise[]> - fetchHomeChildBirthPlace: () => Promise[]> - fetchChildbirthMode: () => Promise[]> - fetchMaturationReason: () => Promise[]> - fetchMaturationModality: () => Promise[]> - fetchImgIndication: () => Promise[]> - fetchLaborOrCesareanEntry: () => Promise[]> - fetchPathologyDuringLabor: () => Promise[]> - fetchObstetricalGestureDuringLabor: () => Promise[]> - fetchAnalgesieType: () => Promise[]> - fetchBirthDeliveryWay: () => Promise[]> - fetchInstrumentType: () => Promise[]> - fetchCSectionModality: () => Promise[]> - fetchPresentationAtDelivery: () => Promise[]> - fetchBirthStatus: () => Promise[]> - fetchSetPostpartumHemorrhage: () => Promise[]> - fetchConditionPerineum: () => Promise[]> - fetchExitPlaceType: () => Promise[]> - fetchFeedingType: () => Promise[]> - fetchComplication: () => Promise[]> - fetchExitFeedingMode: () => Promise[]> - fetchExitDiagnostic: () => Promise[]> - fetchEncounterStatus: () => Promise[]> } const servicesCohortCreation: IServiceCohortCreation = { @@ -271,350 +175,7 @@ const servicesCohortCreation: IServiceCohortCreation = { (await apiBack.get(`/cohort/request-query-snapshots/${snapshotId}/`)) || {} return snapshotResponse.data || {} - }, - - fetchAdmissionModes: async () => - fetchValueSet(getConfig().core.valueSets.encounterAdmissionMode.url, { - joinDisplayWithCode: false, - sortingKey: 'id' - }), - fetchEntryModes: async () => - fetchValueSet(getConfig().core.valueSets.encounterEntryMode.url, { joinDisplayWithCode: false, sortingKey: 'id' }), - fetchExitModes: async () => - fetchValueSet(getConfig().core.valueSets.encounterExitMode.url, { joinDisplayWithCode: false, sortingKey: 'id' }), - fetchPriseEnChargeType: async () => - fetchValueSet(getConfig().core.valueSets.encounterVisitType.url, { - joinDisplayWithCode: false, - filterOut: (value) => value.id === 'nachstationär' || value.id === 'z.zt. verlegt' - }), - fetchTypeDeSejour: async () => - fetchValueSet(getConfig().core.valueSets.encounterSejourType.url, { joinDisplayWithCode: false, sortingKey: 'id' }), - fetchFileStatus: async () => - fetchValueSet(getConfig().core.valueSets.encounterFileStatus.url, { joinDisplayWithCode: false, sortingKey: 'id' }), - fetchReason: async () => - fetchValueSet(getConfig().core.valueSets.encounterExitType.url, { joinDisplayWithCode: false, sortingKey: 'id' }), - fetchDestination: async () => - fetchValueSet(getConfig().core.valueSets.encounterDestination.url, { - joinDisplayWithCode: false, - sortingKey: 'id' - }), - fetchProvenance: async () => - fetchValueSet(getConfig().core.valueSets.encounterProvenance.url, { joinDisplayWithCode: false, sortingKey: 'id' }), - fetchAdmission: async () => - fetchValueSet(getConfig().core.valueSets.encounterAdmission.url, { joinDisplayWithCode: false, sortingKey: 'id' }), - fetchGender: async () => - fetchValueSet(getConfig().core.valueSets.demographicGender.url, { joinDisplayWithCode: false, sortingKey: 'id' }), - fetchStatus: async () => { - return [ - { - id: 'false', - label: VitalStatusLabel.ALIVE - }, - { - id: 'true', - label: VitalStatusLabel.DECEASED - } - ] - }, - fetchStatusDiagnostic: async () => { - return [ - { - id: 'actif', - label: 'Actif' - }, - { - id: 'supp', - label: 'Supprimé' - } - ] - }, - fetchDiagnosticTypes: async () => fetchValueSet(getConfig().features.condition.valueSets.conditionStatus.url), - fetchCim10Diagnostic: async (searchValue?: string, noStar?: boolean, signal?: AbortSignal) => - fetchValueSet( - getConfig().features.condition.valueSets.conditionHierarchy.url, - { - valueSetTitle: 'Toute la hiérarchie', - search: searchValue || '', - noStar - }, - signal - ), - fetchCim10Hierarchy: async (cim10Parent?: string) => - fetchValueSet(getConfig().features.condition.valueSets.conditionHierarchy.url, { - valueSetTitle: 'Toute la hiérarchie CIM10', - code: cim10Parent - }), - fetchCcamData: async (searchValue?: string, noStar?: boolean, signal?: AbortSignal) => - fetchValueSet( - getConfig().features.procedure.valueSets.procedureHierarchy.url, - { valueSetTitle: 'Toute la hiérarchie', search: searchValue || '', noStar }, - signal - ), - fetchCcamHierarchy: async (ccamParent?: string) => - fetchValueSet(getConfig().features.procedure.valueSets.procedureHierarchy.url, { - valueSetTitle: 'Toute la hiérarchie CCAM', - code: ccamParent - }), - fetchGhmData: async (searchValue?: string, noStar?: boolean, signal?: AbortSignal) => - fetchValueSet( - getConfig().features.claim.valueSets.claimHierarchy.url, - { valueSetTitle: 'Toute la hiérarchie', search: searchValue || '', noStar }, - signal - ), - fetchGhmHierarchy: async (ghmParent?: string) => - fetchValueSet(getConfig().features.claim.valueSets.claimHierarchy.url, { - valueSetTitle: 'Toute la hiérarchie GHM', - code: ghmParent - }), - fetchDocTypes: () => Promise.resolve(docTypes && docTypes.docTypes.length > 0 ? docTypes.docTypes : []), - fetchMedicationData: async (searchValue?: string, noStar?: boolean, signal?: AbortSignal) => - fetchValueSet( - `${getConfig().features.medication.valueSets.medicationAtc.url},${ - getConfig().features.medication.valueSets.medicationUcd.url - }`, - { - valueSetTitle: 'Toute la hiérarchie', - search: searchValue || '', - noStar - }, - signal - ), - fetchSingleCodeHierarchy: async (resourceType: string, code: string) => { - const codeSystemPerResourceType: { [type: string]: string } = { - Claim: getConfig().features.claim.valueSets.claimHierarchy.url, - Condition: getConfig().features.condition.valueSets.conditionHierarchy.url, - MedicationAdministration: `${getConfig().features.medication.valueSets.medicationAtc.url},${ - getConfig().features.medication.valueSets.medicationUcd.url - }`, - MedicationRequest: `${getConfig().features.medication.valueSets.medicationAtc.url},${ - getConfig().features.medication.valueSets.medicationUcd.url - }`, - Observation: `${getConfig().features.observation.valueSets.biologyHierarchyAnabio.url},${ - getConfig().features.observation.valueSets.biologyHierarchyLoinc.url - }`, - Procedure: getConfig().features.procedure.valueSets.procedureHierarchy.url - } - if (!(resourceType in codeSystemPerResourceType)) { - // TODO log error - return Promise.resolve([] as string[]) - } - return fetchSingleCodeHierarchy(codeSystemPerResourceType[resourceType], code) - }, - fetchAtcHierarchy: async (atcParent?: string) => - fetchValueSet(getConfig().features.medication.valueSets.medicationAtc.url, { - valueSetTitle: 'Toute la hiérarchie Médicament', - code: atcParent, - sortingKey: 'id', - filterRoots: (atcData) => - // V--[ @TODO: This is a hot fix, remove this after a clean of data ]--V - atcData.label.search(new RegExp(/^[A-Z] - /, 'gi')) !== -1 && - atcData.label.search(new RegExp(/^[X-Y] - /, 'gi')) !== 0 - }), - fetchUCDList: async (ucd?: string) => - fetchValueSet(getConfig().features.medication.valueSets.medicationUcd.url, { code: ucd }), - fetchPrescriptionTypes: async () => - fetchValueSet(getConfig().features.medication.valueSets.medicationPrescriptionTypes.url, { - joinDisplayWithCode: false - }), - fetchAdministrations: async () => { - const administrations = await fetchValueSet( - getConfig().features.medication.valueSets.medicationAdministrations.url, - { - joinDisplayWithCode: false - } - ) - return administrations.map((administration) => - administration.id === 'GASTROTOMIE.' ? { ...administration, label: 'Gastrotomie.' } : administration - ) - }, - fetchBiologyData: async (searchValue?: string, noStar?: boolean) => - fetchValueSet( - `${getConfig().features.observation.valueSets.biologyHierarchyAnabio.url},${ - getConfig().features.observation.valueSets.biologyHierarchyLoinc.url - }`, - { - valueSetTitle: 'Toute la hiérarchie', - search: searchValue || '', - noStar, - joinDisplayWithCode: false - } - ), - fetchBiologyHierarchy: async (biologyParent?: string) => - fetchValueSet(getConfig().features.observation.valueSets.biologyHierarchyAnabio.url, { - valueSetTitle: 'Toute la hiérarchie de Biologie', - code: biologyParent, - joinDisplayWithCode: false, - filterRoots: (biologyItem) => - biologyItem.id !== '527941' && - biologyItem.id !== '547289' && - biologyItem.id !== '528247' && - biologyItem.id !== '981945' && - biologyItem.id !== '834019' && - biologyItem.id !== '528310' && - biologyItem.id !== '528049' && - biologyItem.id !== '527570' && - biologyItem.id !== '527614' - }), - fetchBiologySearch: fetchBiologySearch, - fetchModalities: async () => { - const modalities = await fetchValueSet(getConfig().features.imaging.valueSets.imagingModalities.url, { - joinDisplayWithCode: false - }) - return modalities.map((modality) => ({ ...modality, label: `${modality.id} - ${modality.label}` })) - }, - fetchPregnancyMode: async () => - fetchValueSet(getConfig().features.questionnaires.valueSets.pregnancyMode.url, { - joinDisplayWithCode: false, - sortingKey: 'id' - }), - fetchMaternalRisks: async () => - fetchValueSet(getConfig().features.questionnaires.valueSets.maternalRisks.url, { - joinDisplayWithCode: false, - sortingKey: 'id' - }), - fetchRisksRelatedToObstetricHistory: async () => - fetchValueSet(getConfig().features.questionnaires.valueSets.risksRelatedToObstetricHistory.url, { - joinDisplayWithCode: false, - sortingKey: 'id' - }), - fetchRisksOrComplicationsOfPregnancy: async () => - fetchValueSet(getConfig().features.questionnaires.valueSets.risksOrComplicationsOfPregnancy.url, { - joinDisplayWithCode: false, - sortingKey: 'id' - }), - fetchCorticotherapie: async () => { - return booleanFieldsData - }, - fetchPrenatalDiagnosis: async () => { - return booleanFieldsData - }, - fetchUltrasoundMonitoring: async () => { - return booleanFieldsData - }, - fetchInUteroTransfer: async () => { - return booleanOpenChoiceFieldsData - }, - fetchPregnancyMonitoring: async () => { - return booleanFieldsData - }, - fetchMaturationCorticotherapie: async () => { - return booleanOpenChoiceFieldsData - }, - fetchChirurgicalGesture: async () => - fetchValueSet(getConfig().features.questionnaires.valueSets.chirurgicalGesture.url, { - joinDisplayWithCode: false, - sortingKey: 'id' - }), - fetchVme: async () => { - return vmeData - }, - fetchChildbirth: async () => { - return booleanOpenChoiceFieldsData - }, - fetchHospitalChildBirthPlace: async () => { - return booleanFieldsData - }, - fetchOtherHospitalChildBirthPlace: async () => { - return booleanFieldsData - }, - fetchHomeChildBirthPlace: async () => { - return booleanFieldsData - }, - fetchChildbirthMode: async () => - fetchValueSet(getConfig().features.questionnaires.valueSets.childBirthMode.url, { - joinDisplayWithCode: false, - sortingKey: 'id' - }), - fetchMaturationReason: async () => - fetchValueSet(getConfig().features.questionnaires.valueSets.maturationReason.url, { - joinDisplayWithCode: false, - sortingKey: 'id' - }), - - fetchMaturationModality: async () => - fetchValueSet(getConfig().features.questionnaires.valueSets.maturationModality.url, { - joinDisplayWithCode: false, - sortingKey: 'id' - }), - fetchImgIndication: async () => - fetchValueSet(getConfig().features.questionnaires.valueSets.imgIndication.url, { - joinDisplayWithCode: false, - sortingKey: 'id' - }), - fetchLaborOrCesareanEntry: async () => - fetchValueSet(getConfig().features.questionnaires.valueSets.laborOrCesareanEntry.url, { - joinDisplayWithCode: false, - sortingKey: 'id' - }), - fetchPathologyDuringLabor: async () => - fetchValueSet(getConfig().features.questionnaires.valueSets.pathologyDuringLabor.url, { - joinDisplayWithCode: false, - sortingKey: 'id' - }), - fetchObstetricalGestureDuringLabor: async () => - fetchValueSet(getConfig().features.questionnaires.valueSets.obstetricalGestureDuringLabor.url, { - joinDisplayWithCode: false - }), - fetchAnalgesieType: async () => - fetchValueSet(getConfig().features.questionnaires.valueSets.analgesieType.url, { - joinDisplayWithCode: false, - sortingKey: 'id' - }), - fetchBirthDeliveryWay: async () => - fetchValueSet(getConfig().features.questionnaires.valueSets.birthDeliveryWay.url, { - joinDisplayWithCode: false, - sortingKey: 'id' - }), - fetchInstrumentType: async () => - fetchValueSet(getConfig().features.questionnaires.valueSets.instrumentType.url, { - joinDisplayWithCode: false, - sortingKey: 'id' - }), - fetchCSectionModality: async () => - fetchValueSet(getConfig().features.questionnaires.valueSets.cSectionModality.url, { - joinDisplayWithCode: false, - sortingKey: 'id' - }), - fetchPresentationAtDelivery: async () => - fetchValueSet(getConfig().features.questionnaires.valueSets.presentationAtDelivery.url, { - joinDisplayWithCode: false - }), - fetchBirthStatus: async () => { - return birthStatusData - }, - fetchSetPostpartumHemorrhage: async () => { - return booleanOpenChoiceFieldsData - }, - fetchConditionPerineum: async () => - fetchValueSet(getConfig().features.questionnaires.valueSets.conditionPerineum.url, { - joinDisplayWithCode: false, - sortingKey: 'id' - }), - fetchExitPlaceType: async () => - fetchValueSet(getConfig().features.questionnaires.valueSets.exitPlaceType.url, { - joinDisplayWithCode: false, - sortingKey: 'id' - }), - fetchFeedingType: async () => - fetchValueSet(getConfig().features.questionnaires.valueSets.feedingType.url, { - joinDisplayWithCode: false, - sortingKey: 'id' - }), - fetchComplication: async () => { - return booleanFieldsData - }, - fetchExitFeedingMode: async () => - fetchValueSet(getConfig().features.questionnaires.valueSets.exitFeedingMode.url, { - joinDisplayWithCode: false, - sortingKey: 'id' - }), - fetchExitDiagnostic: async () => - fetchValueSet(getConfig().features.questionnaires.valueSets.exitDiagnostic.url, { - joinDisplayWithCode: false, - sortingKey: 'id' - }), - fetchEncounterStatus: async () => - fetchValueSet(getConfig().core.valueSets.encounterStatus.url, { joinDisplayWithCode: false, sortingKey: 'id' }) + } } export default servicesCohortCreation diff --git a/src/services/aphp/serviceCohorts.ts b/src/services/aphp/serviceCohorts.ts index 01d7a271c..d996dd75a 100644 --- a/src/services/aphp/serviceCohorts.ts +++ b/src/services/aphp/serviceCohorts.ts @@ -577,6 +577,7 @@ const servicesCohorts: IServiceCohorts = { orderBy, searchInput, filters: { + code, nda, ipp, startDate, @@ -607,6 +608,7 @@ const servicesCohorts: IServiceCohorts = { encounterStatus: encounterStatus.map(({ id }) => id), minDate: startDate ?? '', maxDate: endDate ?? '', + code: code.map((code) => encodeURI(`${code.system}|${code.id}`)).join(','), uniqueFacet: ['subject'] }) @@ -633,7 +635,8 @@ const servicesCohorts: IServiceCohorts = { executiveUnits.length > 0 || encounterStatus.length > 0 || (administrationRoutes && administrationRoutes.length > 0) || - (prescriptionTypes && prescriptionTypes.length > 0) + (prescriptionTypes && prescriptionTypes.length > 0) || + code.length const [medicationList, allMedicationList] = await Promise.all([ fetcher(filters), @@ -686,7 +689,7 @@ const servicesCohorts: IServiceCohorts = { searchCriterias: { orderBy, searchInput, - filters: { validatedStatus, nda, ipp, loinc, anabio, startDate, endDate, executiveUnits, encounterStatus } + filters: { validatedStatus, nda, ipp, code, startDate, endDate, executiveUnits, encounterStatus } } } = options const atLeastAFilter = @@ -697,8 +700,7 @@ const servicesCohorts: IServiceCohorts = { !!endDate || executiveUnits.length > 0 || encounterStatus.length > 0 || - (loinc && loinc.length > 0) || - (anabio && anabio.length > 0) + code.length const [biologyList, allBiologyList] = await Promise.all([ fetchObservation({ @@ -716,8 +718,7 @@ const servicesCohorts: IServiceCohorts = { uniqueFacet: ['subject'], minDate: startDate ?? '', maxDate: endDate ?? '', - loinc: loinc.map((code) => code.id).join(), - anabio: anabio.map((code) => code.id).join(), + code: code.map((code) => encodeURI(`${code.system}|${code.id}`)).join(','), rowStatus: validatedStatus }), atLeastAFilter diff --git a/src/services/aphp/servicePatients.ts b/src/services/aphp/servicePatients.ts index 3d2d79dc7..92d428271 100644 --- a/src/services/aphp/servicePatients.ts +++ b/src/services/aphp/servicePatients.ts @@ -905,8 +905,8 @@ export const postFiltersService = async ( return response.data } -export const getFiltersService = async (fhir_resource: ResourceType, next?: string | null) => { - const LIMIT = 10 +export const getFiltersService = async (fhir_resource: ResourceType, next?: string | null, limit = 10) => { + const LIMIT = limit const OFFSET = 0 try { const response = await getFilters(fhir_resource, LIMIT, OFFSET, next) diff --git a/src/services/aphp/servicePerimeters.ts b/src/services/aphp/servicePerimeters.ts index beba9d438..371d0f108 100644 --- a/src/services/aphp/servicePerimeters.ts +++ b/src/services/aphp/servicePerimeters.ts @@ -21,7 +21,7 @@ import { fetchAccessExpirations, fetchEncounter, fetchPatient, fetchPerimeterAcc import { AxiosResponse } from 'axios' import apiBackend from '../apiBackend' -import { FetchScopeOptions, Rights, SourceType } from 'types/scope' +import { FetchScopeOptions, Rights, SourceType, System } from 'types/scope' import { scopeLevelsToRequestParam } from 'utils/perimeters' import { mapParamsToNetworkParams } from 'utils/url' import { Hierarchy } from 'types/hierarchy' @@ -73,13 +73,19 @@ export interface IServicePerimeters { * - IOrganization[] */ - mapRightsToScopeElement: (item: ReadRightPerimeter) => ScopeElement + mapRightsToScopeElement: (item: ReadRightPerimeter) => Hierarchy - mapPerimeterToScopeElement: (item: ScopeElement) => ScopeElement + mapPerimeterToScopeElement: (item: ScopeElement) => Hierarchy - getRights: (options: FetchScopeOptions, signal?: AbortSignal) => Promise> + getRights: ( + options: FetchScopeOptions, + signal?: AbortSignal + ) => Promise>> - getPerimeters: (options?: FetchScopeOptions, signal?: AbortSignal) => Promise> + getPerimeters: ( + options?: FetchScopeOptions, + signal?: AbortSignal + ) => Promise>> getAccessExpirations: (accessExpirationsProps: AccessExpirationsProps) => Promise @@ -196,6 +202,7 @@ const servicesPerimeters: IServicePerimeters = { { id: Rights.EXPIRED, name: '', + label: '', source_value: '', above_levels_ids: '', inferior_levels_ids: '', @@ -203,7 +210,8 @@ const servicesPerimeters: IServicePerimeters = { type: '', cohort_id: '', cohort_size: '', - full_path: '' + full_path: '', + system: System.ScopeTree } ] else population = response @@ -211,7 +219,7 @@ const servicesPerimeters: IServicePerimeters = { return population }, - mapRightsToScopeElement: (item: ReadRightPerimeter): ScopeElement => { + mapRightsToScopeElement: (item: ReadRightPerimeter): Hierarchy => { const { perimeter, read_role, @@ -231,12 +239,14 @@ const servicesPerimeters: IServicePerimeters = { return { ...perimeter, id: perimeter.id.toString(), + system: System.ScopeTree, + label: `${perimeter.source_value} - ${perimeter.name}`, access: servicesPerimeters.getAccessFromRights(rights), rights } }, - mapPerimeterToScopeElement: (item: ScopeElement): ScopeElement => { + mapPerimeterToScopeElement: (item: ScopeElement): Hierarchy => { const { id, name, @@ -253,6 +263,8 @@ const servicesPerimeters: IServicePerimeters = { return { id: id.toString(), name, + system: System.ScopeTree, + label: `${source_value} - ${name}`, source_value, type, parent_id, @@ -264,8 +276,11 @@ const servicesPerimeters: IServicePerimeters = { } }, - getRights: async (options?: FetchScopeOptions, signal?: AbortSignal): Promise> => { - const response: Back_API_Response = { results: [], count: 0 } + getRights: async ( + options?: FetchScopeOptions, + signal?: AbortSignal + ): Promise>> => { + const response: Back_API_Response> = { results: [], count: 0 } try { let baseUrl = 'accesses/perimeters/patient-data/rights/' const params = [] @@ -294,8 +309,8 @@ const servicesPerimeters: IServicePerimeters = { getPerimeters: async ( options?: FetchScopeOptions, signal?: AbortSignal - ): Promise> => { - const response: Back_API_Response = { results: [], count: 0 } + ): Promise>> => { + const response: Back_API_Response> = { results: [], count: 0 } try { let baseUrl = 'accesses/perimeters/' const params = [] diff --git a/src/services/aphp/servicePmsi.ts b/src/services/aphp/servicePmsi.ts deleted file mode 100644 index e8561d30d..000000000 --- a/src/services/aphp/servicePmsi.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { getConfig } from 'config' -import { fetchValueSet } from './callApi' - -export const fetchConditionCodes = async (text: string, noStar = false, signal?: AbortSignal) => { - const response = await fetchValueSet( - getConfig().features.condition.valueSets.conditionHierarchy.url, - { joinDisplayWithCode: false, search: text, noStar: noStar }, - signal - ) - return response.map((elem) => { - return { ...elem, label: `${elem.id} - ${elem.label}` } - }) -} -export const fetchProcedureCodes = async (text: string, noStar = false, signal?: AbortSignal) => { - const response = await fetchValueSet( - getConfig().features.procedure.valueSets.procedureHierarchy.url, - { joinDisplayWithCode: false, search: text, noStar: noStar }, - signal - ) - return response.map((elem) => { - return { ...elem, label: `${elem.id} - ${elem.label}` } - }) -} - -export const fetchClaimCodes = async (text: string, noStar = false, signal?: AbortSignal) => { - const response = await fetchValueSet( - getConfig().features.claim.valueSets.claimHierarchy.url, - { joinDisplayWithCode: false, search: text, noStar: noStar }, - signal - ) - return response.map((elem) => { - return { ...elem, label: `${elem.id} - ${elem.label}` } - }) -} diff --git a/src/services/aphp/serviceValueSets.ts b/src/services/aphp/serviceValueSets.ts new file mode 100644 index 000000000..b3b7f33a2 --- /dev/null +++ b/src/services/aphp/serviceValueSets.ts @@ -0,0 +1,273 @@ +import { Extension, ValueSet, ValueSetComposeIncludeConcept, ValueSetExpansion } from 'fhir/r4' +import apiFhir from 'services/apiFhir' +import { Back_API_Response, FHIR_API_Response, FHIR_Bundle_Response } from 'types' +import { FhirHierarchy, FhirItem, Hierarchy, HierarchyWithLabel } from 'types/hierarchy' +import { getApiResponseResourceOrThrow, getApiResponseResourcesOrThrow } from 'utils/apiHelpers' +import { capitalizeFirstLetter } from 'utils/capitalize' +import { getConfig } from 'config' +import { LOW_TOLERANCE_TAG } from './callApi' +import { sortArray } from 'utils/arrays' + +export const UNKOWN_HIERARCHY_CHAPTER = 'UNKNOWN' +export const HIERARCHY_ROOT = '*' + +const isDataNonQuali = (system: string) => { + switch (system) { + case getConfig().features.observation.valueSets.biologyHierarchyAnabio.url: + case getConfig().features.medication.valueSets.medicationAtc.url: + return true + } + return false +} + +const getParentIds = (extensions?: Extension[], id?: string): string[] => { + const parentIds = [HIERARCHY_ROOT] + extensions + ?.find((ext) => ext.url === getConfig().core.extensions.codeHierarchy) + ?.valueCodeableConcept?.coding?.forEach((code) => { + if (code.code && code.code !== id) parentIds.push(code.code) + }) + return parentIds +} + +/** + * Map a FhirHierarchy to a HierarchyWithLabelAndSystem + * Warning: the fhirHierarchy may have not its parentIds, childrenIds, and subItems initialized so + * `above_levels_ids`, `inferior_levels_ids` may be empty whereas there should be some value here + * @param fhirHierarchy + * @returns + */ + +const mapAbandonedChildren = (children: Hierarchy[]) => { + return children.map((child) => + child.above_levels_ids === HIERARCHY_ROOT && !child.inferior_levels_ids && isDataNonQuali(child.system) + ? { ...child, above_levels_ids: `${HIERARCHY_ROOT},${UNKOWN_HIERARCHY_CHAPTER}` } + : child + ) +} + +const mapFhirHierarchyToHierarchyWithLabelAndSystem = ( + fhirHierarchy: FhirHierarchy +): Hierarchy => { + return { + id: fhirHierarchy.id, + label: fhirHierarchy.label, + system: fhirHierarchy.system, + above_levels_ids: fhirHierarchy.parentIds?.join(',') || '', + inferior_levels_ids: fhirHierarchy.childrenIds?.join(',') || '' + } +} + +const mapCodesToFhirItems = ( + codes: ValueSetComposeIncludeConcept[], + codeSystem: string, + codeInLabel: boolean +): FhirItem[] => { + return sortArray( + codes.map((code) => ({ + id: code.code, + label: codeInLabel + ? `${capitalizeFirstLetter(code.code)} - ${capitalizeFirstLetter(code.display)}` + : capitalizeFirstLetter(code.display), + system: codeSystem + })), + 'label' + ) +} + +const formatValuesetExpansion = ( + valueSetExpansion?: ValueSetExpansion +): Back_API_Response> => { + const codeList: Array = + valueSetExpansion?.contains?.map((code) => ({ + id: code.code as string, // it will always be defined + system: code.system as string, // it will always be defined + label: code.display as string, // it will always be defined + childrenIds: code.contains?.map((child) => child.code as string) || [], + parentIds: getParentIds(code.extension, code.code) + })) || [] + return { + results: codeList.map((e) => mapFhirHierarchyToHierarchyWithLabelAndSystem(e)), + count: valueSetExpansion?.total || codeList.length + } +} + +const formatCodesFromValueSetReponse = (valueSetBundle: ValueSet[]) => { + return valueSetBundle.map((entry) => entry.compose?.include[0].concept?.map((code) => code) || []) +} + +/** + * Fetch the partial hierarchy from a certain node with a specific code value + * the subItems won't have their subItems, childrenIds, and parentIds initialized + * You must then call getFhirCode on subItems to expand the hierarchy + * @param codeSystem the code system from which belongs the code + * @param codes the codes to get the partial hierarchy from + * @param signal the abort signal to cancel the request + * @returns the partial hierarchy from the codes + */ +export const getChildrenFromCodes = async ( + codeSystem: string, + codes: string[], + signal?: AbortSignal +): Promise>> => { + if (codes.length === 0) { + return { + results: [], + count: 0 + } + } + const json = { + resourceType: 'Parameters', + parameter: [ + { + name: 'count', + valueInteger: 999 + }, + { + name: 'valueSet', + resource: { + resourceType: 'ValueSet', + url: codeSystem, + compose: { + include: [ + { + filter: codes.map((code) => ({ + op: 'is-a', + value: code + })) + } + ] + } + } + }, + { + name: 'excludeNested', + valueString: 'false' + } + ] + } + const res = await apiFhir.post>(`/ValueSet/$expand`, JSON.stringify(json), { + signal: signal + }) + return formatValuesetExpansion(getApiResponseResourceOrThrow(res).expansion) +} + +/** + * Search nodes matching the search string and retrieve the partial hierarchy for theses nodes + * the subItems won't have their subItems, childrenIds, and parentIds initialized + * You must then call getFhirCode on subItems to expand the hierarchy + * @param codeSystems the code systems to search in + * @param search the search string + * @param offset the offset + * @param count the size of the result + * @param signal the abort signal to cancel the request + * @returns the partial hierarchy for the nodes matching the search string + */ +export const searchInValueSets = async ( + codeSystems: string[], + search: string, + offset?: number, + count?: number, + signal?: AbortSignal +): Promise>> => { + let options = '' + if (offset !== undefined) options += `&offset=${offset}` + if (count !== undefined) options += `&count=${count}` + const searchValue = search || HIERARCHY_ROOT + const res = await apiFhir.get>( + `/ValueSet/$expand?url=${codeSystems.join(',')}&filter=${encodeURIComponent( + searchValue + )}&excludeNested=false&_tag=text-search-rank&_tag=${LOW_TOLERANCE_TAG}${options}`, + { signal } + ) + const response = formatValuesetExpansion(getApiResponseResourceOrThrow(res).expansion) + response.results = mapAbandonedChildren(response.results) + return response +} + +/** + * Get the complete list of a specific code system + * @param codeSystem the code system to search in + * @param codeInLabel the code is included in the Fhir Items labels + * @param signal the abort signal to cancel the request + * @returns the complete list of the code system + */ +export const getCodeList = async ( + codeSystem: string, + codeInLabel = false, + signal?: AbortSignal +): Promise> => { + const res = await apiFhir.get>(`/ValueSet?reference=${codeSystem}`, { + signal: signal + }) + const valueSetBundle = getApiResponseResourcesOrThrow(res) + const codeList = formatCodesFromValueSetReponse(valueSetBundle)[0] + const fhirItems = mapCodesToFhirItems(codeList, codeSystem, codeInLabel) + return { + results: fhirItems, + count: codeList.length + } +} + +export const getHierarchyRoots = async ( + codeSystem: string, + valueSetTitle: string, + filterRoots: (code: HierarchyWithLabel) => boolean = () => true, + filterOut: (code: HierarchyWithLabel) => boolean = (value: HierarchyWithLabel) => value.id === 'APHP generated', + signal?: AbortSignal +): Promise>> => { + const res = await apiFhir.get>( + `/ValueSet?only-roots=true&reference=${codeSystem}&_sort=code`, + { signal } + ) + const valueSetBundle = getApiResponseResourcesOrThrow(res) + const codeList = formatCodesFromValueSetReponse(valueSetBundle) + .filter((valueSetPerSystem) => !!valueSetPerSystem) + .reduce((acc, val) => acc.concat(val), []) + .map((code) => ({ + id: code.code, + label: capitalizeFirstLetter(code.display), + system: codeSystem + })) + .filter((code) => !filterOut(code)) + + const chapters = codeList + .filter((code) => filterRoots(code)) + .map((e) => mapFhirHierarchyToHierarchyWithLabelAndSystem(e)) + let toBeAdoptedCodes = codeList + .filter((code) => !filterRoots(code)) + .map((e) => mapFhirHierarchyToHierarchyWithLabelAndSystem(e)) + const childrenIds = chapters.map((code) => code.id) + let subItems: Hierarchy[] | undefined = undefined + if (toBeAdoptedCodes.length) { + const unknownChildren = toBeAdoptedCodes.map((child) => ({ + ...child, + above_levels_ids: `${HIERARCHY_ROOT},${UNKOWN_HIERARCHY_CHAPTER}` + })) + const unknownChapter: Hierarchy = { + id: UNKOWN_HIERARCHY_CHAPTER, + label: `${UNKOWN_HIERARCHY_CHAPTER}`, + system: codeSystem, + above_levels_ids: HIERARCHY_ROOT, + inferior_levels_ids: unknownChildren.map((code) => code.id).join(','), + subItems: unknownChildren + } + const chaptersEntities = (await getChildrenFromCodes(codeSystem, childrenIds)).results + childrenIds.push(UNKOWN_HIERARCHY_CHAPTER) + subItems = [...chaptersEntities, unknownChapter] + } + let results = [ + { + id: HIERARCHY_ROOT, + label: valueSetTitle, + system: codeSystem, + childrenIds, + parentIds: [] + } + ].map((e) => ({ ...mapFhirHierarchyToHierarchyWithLabelAndSystem(e), subItems })) + + return { + results: results, + count: codeList.length + } +} diff --git a/src/state/biology.ts b/src/state/biology.ts deleted file mode 100644 index 94f9da296..000000000 --- a/src/state/biology.ts +++ /dev/null @@ -1,150 +0,0 @@ -import { createSlice, createAsyncThunk } from '@reduxjs/toolkit' - -import { RootState } from 'state' -import { impersonate, login, logout } from 'state/me' - -import services from 'services/aphp' -import { Hierarchy } from 'types/hierarchy' - -export type BiologyState = { - loading: boolean - list: Hierarchy[] - openedElement: string[] -} - -const defaultInitialState: BiologyState = { - loading: false, - list: [], - openedElement: [] -} - -const initBiologyHierarchy = createAsyncThunk( - 'biology/fetchElements', - async (DO_NOT_USE, { getState }) => { - try { - const state = getState().biology - const { list } = state - - const biologyList = list.length === 0 ? await services.cohortCreation.fetchBiologyHierarchy() : list - - return { - ...state, - loading: false, - list: biologyList - } - } catch (error) { - console.error('Erreur lors de la récupération de la hierarchie de Biologie', error) - throw error - } - } -) - -const fetchBiology = createAsyncThunk( - 'biology/fetchBiology', - async (DO_NOT_USE, { getState }) => { - const state = getState().biology - const biologyList = await services.cohortCreation.fetchBiologyHierarchy() - - return { - ...state, - list: biologyList, - openedElement: [], - loading: false - } - } -) - -type ExpandBiologyElementsParams = { - rowId: string - selectedItems?: Hierarchy[] -} -const expandBiologyElement = createAsyncThunk( - 'scope/expandBiologyElement', - async ({ rowId, selectedItems }, { getState }) => { - const state = getState().biology - const listElement = state.list - const openedElement = state.openedElement - - let _rootRows = listElement ? [...listElement] : [] - let _openedElement = openedElement ? [...openedElement] : [] - let savedSelectedItems = selectedItems ? [...selectedItems] : [] - - const index = _openedElement.indexOf(rowId) - if (index !== -1) { - _openedElement = _openedElement.filter((id) => id !== rowId) - } else { - _openedElement = [..._openedElement, rowId] - - const replaceSubItems = async (items: Hierarchy[]) => { - let _items: Hierarchy[] = [] - for (let item of items) { - // Replace sub items element by response of back-end - if (item.id === rowId) { - const foundItem = item.subItems ? item.subItems.find((i: Hierarchy) => i.id === 'loading') : true - if (foundItem) { - let subItems: Hierarchy[] = [] - subItems = await services.cohortCreation.fetchBiologyHierarchy(item.id) - - item = { ...item, subItems: subItems } - } - } else if (item.subItems && item.subItems.length !== 0) { - item = { ...item, subItems: await replaceSubItems(item.subItems) } - } - _items = [..._items, item] - - // Check if element is selected, if true => add sub items to savedSelectedItems - const isSelected = savedSelectedItems.find((savedSelectedItem) => savedSelectedItem.id === item.id) - if (isSelected !== undefined && item.subItems && item.subItems.length > 0) { - savedSelectedItems = [...savedSelectedItems, ...item.subItems] - } - } - return _items - } - - _rootRows = await replaceSubItems(listElement) - } - - return { - ...state, - loading: false, - list: _rootRows, - openedElement: _openedElement, - savedSelectedItems: savedSelectedItems.filter((savedSelectedItem) => savedSelectedItem.id !== 'loading') - } - } -) - -const biologySlice = createSlice({ - name: 'biology', - initialState: defaultInitialState, - reducers: { - clearBiologyHierarchy: () => { - return { - loading: false, - list: [], - openedElement: [] - } - } - }, - extraReducers: (builder) => { - builder.addCase(login, () => defaultInitialState) - builder.addCase(logout.fulfilled, () => defaultInitialState) - builder.addCase(impersonate, () => defaultInitialState) - // initBiologyHierarchy - builder.addCase(initBiologyHierarchy.pending, (state) => ({ ...state, loading: true })) - builder.addCase(initBiologyHierarchy.fulfilled, (state, action) => ({ ...state, ...action.payload })) - builder.addCase(initBiologyHierarchy.rejected, (state) => ({ ...state })) - // fetchBiologyHierarchy - builder.addCase(fetchBiology.pending, (state) => ({ ...state, loading: true })) - builder.addCase(fetchBiology.fulfilled, (state, action) => ({ ...state, ...action.payload })) - builder.addCase(fetchBiology.rejected, (state) => ({ ...state })) - // expandBiologyElement - builder.addCase(expandBiologyElement.pending, (state) => ({ ...state, loading: true })) - builder.addCase(expandBiologyElement.fulfilled, (state, action) => ({ ...state, ...action.payload })) - builder.addCase(expandBiologyElement.rejected, (state) => ({ ...state })) - } -}) - -export default biologySlice.reducer -export { initBiologyHierarchy, fetchBiology, expandBiologyElement } -export const { clearBiologyHierarchy } = biologySlice.actions diff --git a/src/state/medication.ts b/src/state/medication.ts deleted file mode 100644 index 432fb30b6..000000000 --- a/src/state/medication.ts +++ /dev/null @@ -1,168 +0,0 @@ -import { createSlice, createAsyncThunk } from '@reduxjs/toolkit' - -import { RootState } from 'state' -import { impersonate, login, logout } from 'state/me' - -import services from 'services/aphp' -import { Hierarchy } from 'types/hierarchy' - -export type MedicationState = { - syncLoading?: number - loading: boolean - list: Hierarchy[] - openedElement: string[] - ucdList: Hierarchy[] -} - -const defaultInitialState: MedicationState = { - loading: false, - list: [], - openedElement: [], - ucdList: [] -} - -const initMedicationHierarchy = createAsyncThunk( - 'medication/fetchElements', - async (DO_NOT_USE, { getState }) => { - try { - const state = getState().medication - const { list } = state - - const medicationList = list.length === 0 ? await services.cohortCreation.fetchAtcHierarchy('') : list - - const medicationUCDList = await services.cohortCreation.fetchUCDList('') - - return { - ...state, - loading: false, - list: medicationList, - ucdList: medicationUCDList - } - } catch (error) { - console.error(error) - throw error - } - } -) - -const fetchMedication = createAsyncThunk( - 'medication/fetchMedication', - async (DO_NOT_USE, { getState }) => { - const state = getState().medication - const medicationList = await services.cohortCreation.fetchAtcHierarchy('') - const medicationUCDList = await services.cohortCreation.fetchUCDList('') - - return { - ...state, - list: medicationList, - openedElement: [], - ucdList: medicationUCDList, - loading: false - } - } -) - -type ExpandMedicationElementParams = { - rowId: string - selectedItems?: Hierarchy[] -} - -const expandMedicationElement = createAsyncThunk( - 'scope/expandMedicationElement', - async ({ rowId, selectedItems }, { getState }) => { - const state = getState().medication - const listElement = state.list - const openedElement = state.openedElement - - let _rootRows = listElement ? [...listElement] : [] - let _openedElement = openedElement ? [...openedElement] : [] - let savedSelectedItems = selectedItems ? [...selectedItems] : [] - - const index = _openedElement.indexOf(rowId) - if (index !== -1) { - _openedElement = _openedElement.filter((id) => id !== rowId) - } else { - _openedElement = [..._openedElement, rowId] - - const replaceSubItems = async (items: Hierarchy[]) => { - let _items: Hierarchy[] = [] - for (let item of items) { - // Replace sub items element by response of back-end - if (item.id === rowId) { - const foundItem = item.subItems ? item.subItems.find((i: Hierarchy) => i.id === 'loading') : true - if (foundItem) { - let subItems: Hierarchy[] = [] - subItems = await services.cohortCreation.fetchAtcHierarchy(item.id) - - item = { ...item, subItems: subItems } - } - } else if (item.subItems && item.subItems.length !== 0) { - item = { ...item, subItems: await replaceSubItems(item.subItems) } - } - _items = [..._items, item] - - // Check if element is selected, if true => add sub items to savedSelectedItems - const isSelected = savedSelectedItems.find((savedSelectedItem) => savedSelectedItem.id === item.id) - if (isSelected !== undefined && item.subItems && item.subItems.length > 0) { - savedSelectedItems = [...savedSelectedItems, ...item.subItems] - } - } - return _items - } - - _rootRows = await replaceSubItems(listElement) - } - - return { - ...state, - loading: false, - list: _rootRows, - openedElement: _openedElement, - savedSelectedItems: savedSelectedItems.filter((savedSelectedItem) => savedSelectedItem.id !== 'loading') - } - } -) - -const medicationSlice = createSlice({ - name: 'medication', - initialState: defaultInitialState as MedicationState, - reducers: { - clearMedicationHierarchy: () => { - return { - loading: false, - list: [], - openedElement: [], - ucdList: [] - } - } - }, - extraReducers: (builder) => { - builder.addCase(login, () => defaultInitialState) - builder.addCase(logout.fulfilled, () => defaultInitialState) - builder.addCase(impersonate, () => defaultInitialState) - // initMedicationHierarchy - builder.addCase(initMedicationHierarchy.pending, (state) => ({ ...state, loading: true })) - builder.addCase(initMedicationHierarchy.fulfilled, (state, action) => ({ ...state, ...action.payload })) - builder.addCase(initMedicationHierarchy.rejected, (state) => ({ ...state })) - // fetchMedication - builder.addCase(fetchMedication.pending, (state) => ({ ...state, loading: true })) - builder.addCase(fetchMedication.fulfilled, (state, action) => ({ ...state, ...action.payload })) - builder.addCase(fetchMedication.rejected, (state) => ({ ...state })) - // expandMedicationElement - builder.addCase(expandMedicationElement.pending, (state) => ({ - ...state, - loading: true, - syncLoading: (state.syncLoading ?? 0) + 1 - })) - builder.addCase(expandMedicationElement.fulfilled, (state, action) => ({ - ...state, - ...action.payload, - syncLoading: (state.syncLoading ?? 0) - 1 - })) - builder.addCase(expandMedicationElement.rejected, (state) => ({ ...state })) - } -}) - -export default medicationSlice.reducer -export { initMedicationHierarchy, fetchMedication, expandMedicationElement } -export const { clearMedicationHierarchy } = medicationSlice.actions diff --git a/src/state/message.ts b/src/state/message.ts index 271a5b0af..abf1a9c44 100644 --- a/src/state/message.ts +++ b/src/state/message.ts @@ -10,10 +10,8 @@ import { } from './cohortCreation' import { addProject, editProject, deleteProject, fetchProjects } from './project' import { addRequest, editRequest, deleteRequest, fetchRequests, moveRequests, deleteRequests } from './request' -import { expandBiologyElement, fetchBiology, initBiologyHierarchy } from './biology' import { addCohort, deleteCohort, editCohort, fetchCohorts } from './cohort' import { favoriteExploredCohort, fetchExploredCohort } from './exploredCohort' -import { expandMedicationElement, fetchMedication, initMedicationHierarchy } from './medication' import { fetchAllProcedures, fetchBiology as fetchBiologyPatient, @@ -23,7 +21,6 @@ import { fetchPatientInfo, fetchPmsi } from './patient' -import { expandPmsiElement, fetchClaim, fetchCondition, fetchProcedure, initPmsiHierarchy } from './pmsi' import { CanceledError } from 'axios' export type MessageState = null | { @@ -129,30 +126,6 @@ const setMessageSlice = createSlice({ type: 'error', content: 'Une erreur est survenue lors de la sauvegarde de la requête' })) - builder.addCase(initBiologyHierarchy.rejected, () => ({ - type: 'error', - content: 'Une erreur est survenue lors de la récupération de la hiérarchie de biologie' - })) - builder.addCase(fetchBiology.rejected, () => ({ - type: 'error', - content: 'Une erreur est survenue lors de la récupération de la hiérarchie de biologie' - })) - builder.addCase(expandBiologyElement.rejected, () => ({ - type: 'error', - content: 'Une erreur est survenue lors de la récupération des enfants dans la hiérarchie de biologie' - })) - builder.addCase(initMedicationHierarchy.rejected, () => ({ - type: 'error', - content: 'Une erreur est survenue lors de la récupération de la hiérarchie des médicaments' - })) - builder.addCase(fetchMedication.rejected, () => ({ - type: 'error', - content: 'Une erreur est survenue lors de la récupération de la hiérarchie des médicaments' - })) - builder.addCase(expandMedicationElement.rejected, () => ({ - type: 'error', - content: 'Une erreur est survenue lors de la récupération des enfants la hiérarchie des médicaments' - })) builder.addCase(fetchCohorts.rejected, () => ({ type: 'error', content: 'Une erreur est survenue lors de la récupération des cohortes' @@ -209,26 +182,6 @@ const setMessageSlice = createSlice({ type: 'error', content: 'Une erreur est survenue lors de la récupération des données de biologie du patient' })) - builder.addCase(initPmsiHierarchy.rejected, () => ({ - type: 'error', - content: 'Une erreur est survenue lors de la récupération de la hiérarchie des PMSI' - })) - builder.addCase(fetchCondition.rejected, () => ({ - type: 'error', - content: 'Une erreur est survenue lors de la récupération de la hiérarchie des diagnostics CIM10' - })) - builder.addCase(fetchClaim.rejected, () => ({ - type: 'error', - content: 'Une erreur est survenue lors de la récupération de la hiérarchie des GHM' - })) - builder.addCase(fetchProcedure.rejected, () => ({ - type: 'error', - content: 'Une erreur est survenue lors de la récupération de la hiérarchie des actes' - })) - builder.addCase(expandPmsiElement.rejected, () => ({ - type: 'error', - content: 'Une erreur est survenue lors de la récupération des enfants de la hiérarchie des PMSI' - })) builder.addCase(fetchProjects.rejected, () => ({ type: 'error', content: 'Une erreur est survenue lors de la récupération des projets' diff --git a/src/state/pmsi.ts b/src/state/pmsi.ts deleted file mode 100644 index 552552e6d..000000000 --- a/src/state/pmsi.ts +++ /dev/null @@ -1,286 +0,0 @@ -import { createAsyncThunk, createSlice } from '@reduxjs/toolkit' - -import { RootState } from 'state' -import { impersonate, login, logout } from 'state/me' - -import services from 'services/aphp' -import { Hierarchy } from 'types/hierarchy' - -export type PmsiElementType = { - loading: boolean - list: Hierarchy[] - openedElement: string[] -} - -type PmsiState = { - claim: PmsiElementType - condition: PmsiElementType - procedure: PmsiElementType - syncLoading?: number -} - -const defaultInitialState: PmsiState = { - claim: { - loading: false, - list: [], - openedElement: [] - }, - condition: { - loading: false, - list: [], - openedElement: [] - }, - procedure: { - loading: false, - list: [], - openedElement: [] - }, - syncLoading: 0 -} - -const initPmsiHierarchy = createAsyncThunk( - 'pmsi/fetchElements', - async (DO_NOT_USE, { getState }) => { - try { - const state = getState().pmsi - const { claim, condition, procedure } = state - - const claimList = claim.list.length === 0 ? await services.cohortCreation.fetchGhmHierarchy('') : claim.list - const conditionList = - condition.list.length === 0 ? await services.cohortCreation.fetchCim10Hierarchy('') : condition.list - const procedureList = - procedure.list.length === 0 ? await services.cohortCreation.fetchCcamHierarchy('') : procedure.list - - return { - ...state, - claim: { - ...state.claim, - loading: false, - list: claimList - }, - condition: { - ...state.condition, - loading: false, - list: conditionList - }, - procedure: { - ...state.procedure, - loading: false, - list: procedureList - } - } - } catch (error) { - console.error(error) - throw error - } - } -) - -const fetchCondition = createAsyncThunk( - 'pmsi/fetchCondition', - async (DO_NOT_USE, { getState }) => { - const state = getState().pmsi - const conditionList = await services.cohortCreation.fetchCim10Hierarchy('') - - return { - ...state.condition, - list: conditionList, - openedElement: [], - loading: false - } - } -) - -const fetchClaim = createAsyncThunk( - 'pmsi/fetchClaim', - async (DO_NOT_USE, { getState }) => { - const state = getState().pmsi - const claimList = await services.cohortCreation.fetchGhmHierarchy('') - - return { - ...state.claim, - list: claimList, - openedElement: [], - loading: false - } - } -) - -const fetchProcedure = createAsyncThunk( - 'pmsi/fetchProcedure', - async (DO_NOT_USE, { getState }) => { - const state = getState().pmsi - const procedureList = await services.cohortCreation.fetchCcamHierarchy('') - - return { - ...state.procedure, - list: procedureList, - openedElement: [], - loading: false - } - } -) - -type ExpandPmsiElementParams = { - rowId: string - keyElement: 'claim' | 'condition' | 'procedure' - selectedItems?: Hierarchy[] -} - -const expandPmsiElement = createAsyncThunk( - 'scope/expandPmsiElement', - async ({ rowId, keyElement, selectedItems }, { getState }) => { - const state = getState().pmsi - const listElement = state[keyElement].list - const openedElement = state[keyElement].openedElement - - let _rootRows = listElement ? [...listElement] : [] - let _openedElement = openedElement ? [...openedElement] : [] - let savedSelectedItems = selectedItems ? [...selectedItems] : [] - - const index = _openedElement.indexOf(rowId) - if (index !== -1) { - _openedElement = _openedElement.filter((id) => id !== rowId) - } else { - _openedElement = [..._openedElement, rowId] - - const replaceSubItems = async (items: Hierarchy[]) => { - let _items: Hierarchy[] = [] - for (let item of items) { - // Replace sub items element by response of back-end - if (item.id === rowId) { - const foundItem = item.subItems ? item.subItems.find((i: Hierarchy) => i.id === 'loading') : true - if (foundItem) { - let subItems: Hierarchy[] = [] - if (keyElement === 'claim') { - subItems = await services.cohortCreation.fetchGhmHierarchy(item.id) - } - if (keyElement === 'condition') { - subItems = await services.cohortCreation.fetchCim10Hierarchy(item.id) - } - if (keyElement === 'procedure') { - subItems = await services.cohortCreation.fetchCcamHierarchy(item.id) - } - - item = { ...item, subItems: subItems } - } - } else if (item.subItems && item.subItems.length !== 0) { - item = { ...item, subItems: await replaceSubItems(item.subItems) } - } - _items = [..._items, item] - - // Check if element is selected, if true => add sub items to savedSelectedItems - const isSelected = savedSelectedItems.find((savedSelectedItem) => savedSelectedItem.id === item.id) - if (isSelected !== undefined && item.subItems && item.subItems.length > 0) { - savedSelectedItems = [...savedSelectedItems, ...item.subItems] - } - } - return _items - } - - _rootRows = await replaceSubItems(listElement) - } - - return { - ...state, - [keyElement]: { - ...state[keyElement], - list: _rootRows, - openedElement: _openedElement, - loading: false - }, - savedSelectedItems: savedSelectedItems.filter((savedSelectedItem) => savedSelectedItem.id !== 'loading') - } - } -) - -const pmsiSlice = createSlice({ - name: 'pmsi', - initialState: defaultInitialState, - reducers: { - clearPmsiHierarchy: () => { - return { - claim: { - loading: false, - list: [], - openedElement: [] - }, - condition: { - loading: false, - list: [], - openedElement: [] - }, - procedure: { - loading: false, - list: [], - openedElement: [] - } - } - } - }, - extraReducers: (builder) => { - builder.addCase(login, () => defaultInitialState) - builder.addCase(logout.fulfilled, () => defaultInitialState) - builder.addCase(impersonate, () => defaultInitialState) - // initPmsiHierarchy - builder.addCase(initPmsiHierarchy.pending, (state) => ({ - ...state, - claim: { ...state.claim, loading: true }, - condition: { ...state.condition, loading: true }, - procedure: { ...state.procedure, loading: true } - })) - builder.addCase(initPmsiHierarchy.fulfilled, (state, action) => ({ ...state, ...action.payload })) - builder.addCase(initPmsiHierarchy.rejected, (state) => ({ ...state })) - // fetchCondition - builder.addCase(fetchCondition.pending, (state) => ({ - ...state, - condition: { ...state.condition, loading: true } - })) - builder.addCase(fetchCondition.fulfilled, (state, action) => ({ - ...state, - condition: { ...state.condition, ...action.payload } - })) - builder.addCase(fetchCondition.rejected, (state) => ({ ...state })) - // fetchClaim - builder.addCase(fetchClaim.pending, (state) => ({ - ...state, - claim: { ...state.claim, loading: true } - })) - builder.addCase(fetchClaim.fulfilled, (state, action) => ({ - ...state, - claim: { ...state.claim, ...action.payload } - })) - builder.addCase(fetchClaim.rejected, (state) => ({ ...state })) - // fetchProcedure - builder.addCase(fetchProcedure.pending, (state) => ({ - ...state, - procedure: { ...state.procedure, loading: true } - })) - builder.addCase(fetchProcedure.fulfilled, (state, action) => ({ - ...state, - procedure: { ...state.procedure, ...action.payload } - })) - builder.addCase(fetchProcedure.rejected, (state) => ({ ...state })) - // expandPmsiElement - builder.addCase(expandPmsiElement.pending, (state) => ({ - ...state, - claim: { ...state.claim, loading: true }, - condition: { ...state.condition, loading: true }, - procedure: { ...state.procedure, loading: true }, - syncLoading: (state.syncLoading ?? 0) + 1 - })) - builder.addCase(expandPmsiElement.fulfilled, (state, action) => ({ - ...state, - ...action.payload, - ...{ syncLoading: (state.syncLoading ?? 0) - 1 } - })) - builder.addCase(expandPmsiElement.rejected, (state) => ({ - ...state, - ...{ syncLoading: (state.syncLoading ?? 0) - 1 } - })) - } -}) - -export default pmsiSlice.reducer -export { initPmsiHierarchy, fetchCondition, fetchClaim, fetchProcedure, expandPmsiElement } -export const { clearPmsiHierarchy } = pmsiSlice.actions diff --git a/src/state/scope.ts b/src/state/scope.ts index 559b1631a..2a652dcf4 100644 --- a/src/state/scope.ts +++ b/src/state/scope.ts @@ -1,29 +1,34 @@ -import { createSlice } from '@reduxjs/toolkit' +import { createEntityAdapter, createSelector, createSlice } from '@reduxjs/toolkit' import { impersonate, login, logout } from './me' import { ScopeElement } from 'types' -import { Hierarchy } from 'types/hierarchy' +import { CodesCache, Hierarchy } from 'types/hierarchy' +import { RootState } from 'state' +import { mapCacheToCodes } from 'utils/hierarchy/hierarchy' export type ScopeState = { rights: Hierarchy[] codes: { - rights: Hierarchy[] - perimeters: Hierarchy[] + rights: ReturnType + perimeters: ReturnType } openPopulation: number[] } +const fetchedRightsAdapter = createEntityAdapter>() +const fetchedPerimetersAdapter = createEntityAdapter>() + const defaultInitialState: ScopeState = { rights: [], codes: { - rights: [], - perimeters: [] + rights: fetchedRightsAdapter.getInitialState(), + perimeters: fetchedPerimetersAdapter.getInitialState() }, openPopulation: [] } const scopeSlice = createSlice({ name: 'scope', - initialState: defaultInitialState as ScopeState, + initialState: defaultInitialState, reducers: { closeAllOpenedPopulation: (state) => { return { @@ -37,17 +42,10 @@ const scopeSlice = createSlice({ rights: action.payload.rights || [] } }, - saveFetchedRights: (state, action) => { - return { - ...state, - codes: { ...state.codes, rights: action.payload || [] } - } - }, - saveFetchedPerimeters: (state, action) => { - return { - ...state, - codes: { ...state.codes, perimeters: action.payload || [] } - } + saveScopeCodes: (state, action) => { + action.payload.isRights + ? fetchedRightsAdapter.setOne(state.codes.rights, action.payload.values) + : fetchedPerimetersAdapter.setOne(state.codes.perimeters, action.payload.values) } }, extraReducers: (builder) => { @@ -57,5 +55,19 @@ const scopeSlice = createSlice({ } }) +const selectPerimeterCodes = fetchedPerimetersAdapter.getSelectors((state: RootState) => state.scope.codes.perimeters) +const selectRightsCodes = fetchedRightsAdapter.getSelectors((state: RootState) => state.scope.codes.rights) + +const selectByAccess = createSelector( + [ + selectPerimeterCodes.selectAll, + selectRightsCodes.selectAll, + (state: RootState, isRights: boolean) => ({ isRights }) + ], + (perimeterCodes, rightsCodes, { isRights }) => (isRights ? rightsCodes : perimeterCodes) +) + +export const selectScopeCodes = createSelector([selectByAccess], (codes) => mapCacheToCodes(codes)) + export default scopeSlice.reducer -export const { closeAllOpenedPopulation, saveRights, saveFetchedRights, saveFetchedPerimeters } = scopeSlice.actions +export const { closeAllOpenedPopulation, saveRights, saveScopeCodes } = scopeSlice.actions diff --git a/src/state/store.ts b/src/state/store.ts index fdfe5a002..44aee3a38 100644 --- a/src/state/store.ts +++ b/src/state/store.ts @@ -10,19 +10,15 @@ import localforage from 'localforage' import cohortCreation from './cohortCreation' import exploredCohort from './exploredCohort' import autoLogout from './autoLogout' -import medication from './medication' import criteria from './criteria' import message from './message' import patient from './patient' import project from './project' import request from './request' -import biology from './biology' import cohort from './cohort' import drawer from './drawer' import scope from './scope' -import pmsi from './pmsi' import me from './me' -import syncHierarchyTable from './syncHierarchyTable' import warningDialog from './warningDialog' import valueSets from './valueSets' @@ -42,12 +38,8 @@ const rootReducer = combineReducers({ request, cohort, scope, - pmsi, - medication, - biology, patient, autoLogout, - syncHierarchyTable, warningDialog }) diff --git a/src/state/syncHierarchyTable.ts b/src/state/syncHierarchyTable.ts deleted file mode 100644 index a640360dd..000000000 --- a/src/state/syncHierarchyTable.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { AsyncThunk, createAsyncThunk, createSlice } from '@reduxjs/toolkit' -import { RootState } from './index' -import { HierarchyTree } from '../types' -import { SelectedCriteriaType } from 'types/requestCriterias' -import { impersonate, login, logout } from './me' - -const initialState: HierarchyTree = { - code: undefined, - loading: 0 -} - -const pushSyncHierarchyTable: AsyncThunk = createAsyncThunk< - HierarchyTree, - HierarchyTree, - { state: RootState } ->('syncHierarchyTable/pushSyncHierarchyTable', async (newState: HierarchyTree, { getState }) => { - return { loading: getState().syncHierarchyTable.loading, code: newState?.code } -}) -const incrementLoadingSyncHierarchyTable: AsyncThunk = createAsyncThunk< - HierarchyTree, - void, - { state: RootState } ->('syncHierarchyTable/incrementLoadingSyncHierarchyTable', async (DO_NOT_USE, { getState }) => { - return getState().syncHierarchyTable -}) -const decrementLoadingSyncHierarchyTable: AsyncThunk = createAsyncThunk< - HierarchyTree, - void, - { state: RootState } ->('syncHierarchyTable/decrementLoadingSyncHierarchyTable', async (DO_NOT_USE, { getState }) => { - return getState().syncHierarchyTable -}) -const initSyncHierarchyTable: AsyncThunk = createAsyncThunk< - HierarchyTree, - SelectedCriteriaType, - { state: RootState } ->('syncHierarchyTable/initSyncHierarchyTable', (selectedCriteria: SelectedCriteriaType, { getState }) => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const selectedResource: any = (getState().cohortCreation.request ?? {}).selectedCriteria?.find( - (item) => item.id === selectedCriteria.id - ) - const newState: HierarchyTree = selectedResource - ? { - code: selectedResource ? selectedResource.code : 'loading', - loading: 0 - } - : initialState - return newState -}) - -const synchroSlice = createSlice({ - name: 'syncHierarchyTable', - initialState: initialState, - reducers: {}, - extraReducers: (builder) => { - builder.addCase(login, () => initialState) - builder.addCase(logout.fulfilled, () => initialState) - builder.addCase(impersonate, () => initialState) - builder.addCase(pushSyncHierarchyTable.fulfilled, (state, action) => ({ - ...action.payload, - ...{ loading: state.loading ?? 0 } - })) - builder.addCase(initSyncHierarchyTable.fulfilled, (state, action) => ({ - ...action.payload - })) - builder.addCase(incrementLoadingSyncHierarchyTable.pending, (state) => ({ - ...state, - ...{ loading: (state.loading ?? 0) + 1 } - })) - builder.addCase(decrementLoadingSyncHierarchyTable.fulfilled, (state) => { - return { - ...state, - ...{ loading: (state.loading ?? 0) - 1 } - } - }) - } -}) -export { - pushSyncHierarchyTable, - initSyncHierarchyTable, - incrementLoadingSyncHierarchyTable, - decrementLoadingSyncHierarchyTable -} -export default synchroSlice.reducer diff --git a/src/state/valueSets.ts b/src/state/valueSets.ts index 4d92113a0..74913fc39 100644 --- a/src/state/valueSets.ts +++ b/src/state/valueSets.ts @@ -1,14 +1,12 @@ -import { createSlice, createEntityAdapter, PayloadAction } from '@reduxjs/toolkit' -import { HierarchyElementWithSystem } from 'types' +import { createSlice, createEntityAdapter, PayloadAction, createSelector } from '@reduxjs/toolkit' +import { CodesCache, FhirItem } from 'types/hierarchy' import { logout } from './me' import { LabelObject } from 'types/searchCriterias' +import { RootState } from 'state' +import { mapCacheToCodes } from 'utils/hierarchy/hierarchy' -type ValueSetOptions = { - id: string - options: HierarchyElementWithSystem[] -} -const valueSetsAdapter = createEntityAdapter() +const valueSetsAdapter = createEntityAdapter>() const valueSetsSlice = createSlice({ name: 'valueSets', @@ -19,7 +17,7 @@ const valueSetsSlice = createSlice({ cache: {} as { [system: string]: LabelObject[] } }), reducers: { - addValueSets: valueSetsAdapter.addMany, + saveValueSets: (state, action) => valueSetsAdapter.setMany(state, action.payload), updateCache: (state, action: PayloadAction<{ [system: string]: LabelObject[] }>) => { return { ...state, @@ -34,5 +32,14 @@ const valueSetsSlice = createSlice({ } }) -export const { addValueSets, updateCache } = valueSetsSlice.actions +const valueSetsSelectors = valueSetsAdapter.getSelectors((state: RootState) => state.valueSets) + +const selectByIds = createSelector( + [valueSetsSelectors.selectAll, (state: RootState, ids: string[]) => ids], + (valueSets, ids) => valueSets.filter((valueSet) => ids.includes(valueSet.id)) +) + +export const selectValueSetCodes = createSelector([selectByIds], (valueSets) => mapCacheToCodes(valueSets)) + +export const { updateCache, saveValueSets } = valueSetsSlice.actions export default valueSetsSlice.reducer diff --git a/src/types.ts b/src/types.ts index e7eb62547..47f799e14 100644 --- a/src/types.ts +++ b/src/types.ts @@ -32,7 +32,7 @@ import { SelectedCriteriaType } from 'types/requestCriterias' import { ExportTableType } from 'components/Dashboard/ExportModal/export_table' -import { Hierarchy } from 'types/hierarchy' +import { Hierarchy, HierarchyElementWithSystem } from 'types/hierarchy' import { SearchByTypes } from 'types/searchCriterias' import { PMSILabel } from 'types/patient' @@ -378,7 +378,13 @@ export type CriteriaItemType = { type FetchFunctionVariant = | (() => Promise) - | ((searchValue?: string, noStar?: boolean, signal?: AbortSignal) => Promise[]>) + | (( + searchValue?: string, + exactSearch?: boolean, + signal?: AbortSignal + ) => Promise>>) + +export type ResearchType = string | boolean | AbortSignal | undefined export type ValueSet = { code: string @@ -698,9 +704,10 @@ export type IPatientImaging = { page: number } -export type TabType = { +export type TabType = { label: TL id: T + active?: boolean icon?: ReactElement wrapped?: boolean } @@ -732,13 +739,6 @@ export type DTTB_ButtonType = { icon?: ReactElement onClick: (args?: any) => void } -export type HierarchyTree = null | { - code?: Hierarchy[] - loading?: number -} - -export type HierarchyElementWithSystem = Hierarchy & { system?: string } - export type ScopeElement = { id: string name: string diff --git a/src/types/hierarchy.ts b/src/types/hierarchy.ts index 50d2765fd..144fecb8d 100644 --- a/src/types/hierarchy.ts +++ b/src/types/hierarchy.ts @@ -1,4 +1,4 @@ -import { SelectedStatus } from 'types' +import { LoadingStatus, SelectedStatus } from 'types' export enum Mode { EXPAND, @@ -10,12 +10,72 @@ export enum Mode { UNSELECT_ALL } -export type Hierarchy = T & { - id: S - above_levels_ids: string - inferior_levels_ids: string - status?: SelectedStatus - subItems?: Hierarchy[] +export type HierarchyInfo = { + tree: Hierarchy[] + count: number + page: number, + system: string } +export type AbstractTree = DATA & { + id: ID + subItems?: AbstractTree[] +} + +export type HierarchyWithLabel = AbstractTree + +export type HierarchyWithLabelAndSystem = HierarchyWithLabel<{ system: string } & S> + +export type FhirItem = { + id: string + label: string + parentIds?: string[] + childrenIds?: string[] + system: string +} + +export type FhirHierarchy = AbstractTree + +export type Hierarchy = AbstractTree< + S, + T & { + label: string + above_levels_ids: string + inferior_levels_ids: string + system: string + status?: SelectedStatus + } +> + +export type HierarchyElementWithSystem = Hierarchy + export type InfiniteMap = Map + +export type HierarchyLoadingStatus = { + init: LoadingStatus + search: LoadingStatus + expand: LoadingStatus +} + +export type GroupedBySystem = { + system: string + codes: Hierarchy[] +} + +export type Codes = Map>> + +export type CodesCache = { + id: string + options: { [key: string]: Hierarchy } +} + +export enum SearchMode { + EXPLORATION, + RESEARCH +} + +export enum SearchModeLabel { + EXPLORATION = 'Exploration', + RESEARCH = 'Recherche' +} + diff --git a/src/types/requestCriterias.ts b/src/types/requestCriterias.ts index 0686f69cb..31645df63 100644 --- a/src/types/requestCriterias.ts +++ b/src/types/requestCriterias.ts @@ -1,5 +1,5 @@ import { ScopeElement, SimpleCodeType } from 'types' -import { Hierarchy } from './hierarchy' +import { FhirItem, Hierarchy } from './hierarchy' import { DocumentAttachmentMethod, DurationRangeType, LabelObject, SearchByTypes } from './searchCriterias' export enum MedicationLabel { @@ -155,7 +155,6 @@ export enum CriteriaDataKey { BIOLOGY_DATA = 'biologyData', MODALITIES = 'modalities', DOC_TYPES = 'docTypes', - STATUS_DIAGNOSTIC = 'statusDiagnostic', PREGNANCY_MODE = 'pregnancyMode', MATERNAL_RISKS = 'maternalRisks', RISKS_RELATED_TO_OBSTETRIC_HISTORY = 'risksRelatedToObstetricHistory', @@ -201,8 +200,8 @@ export type CcamDataType = CommonCriteriaDataType & WithEncounterDateDataType & WithEncounterStatusDataType & { type: CriteriaType.PROCEDURE - hierarchy: undefined - code: LabelObject[] | null + //hierarchy: undefined + code: Hierarchy[] source: string | null label: undefined } @@ -212,7 +211,7 @@ export type Cim10DataType = CommonCriteriaDataType & WithEncounterDateDataType & WithEncounterStatusDataType & { type: CriteriaType.CONDITION - code: LabelObject[] | null + code: Hierarchy[] source: string | null diagnosticType: LabelObject[] | null label: undefined @@ -248,7 +247,7 @@ export type GhmDataType = CommonCriteriaDataType & WithEncounterDateDataType & WithEncounterStatusDataType & { type: CriteriaType.CLAIM - code: LabelObject[] | null + code: Hierarchy[] label: undefined } @@ -363,7 +362,7 @@ export type MedicationDataType = CommonCriteriaDataType & WithOccurenceCriteriaDataType & WithEncounterDateDataType & WithEncounterStatusDataType & { - code: LabelObject[] | null + code: Hierarchy[] administration: LabelObject[] | null } & ( | { @@ -378,8 +377,7 @@ export type ObservationDataType = CommonCriteriaDataType & WithEncounterDateDataType & WithEncounterStatusDataType & { type: CriteriaType.OBSERVATION - code: LabelObject[] | null - isLeaf: boolean + code: Hierarchy[] searchByValue: [number | null, number | null] valueComparator: Comparators } diff --git a/src/types/scope.ts b/src/types/scope.ts index 3a68cdf71..b3203534e 100644 --- a/src/types/scope.ts +++ b/src/types/scope.ts @@ -26,3 +26,7 @@ export enum SourceType { export enum Rights { EXPIRED = 'expired' } + +export enum System { + ScopeTree = "scope_tree" +} \ No newline at end of file diff --git a/src/types/searchCriterias.ts b/src/types/searchCriterias.ts index 7e9b66b36..396c5ed4b 100644 --- a/src/types/searchCriterias.ts +++ b/src/types/searchCriterias.ts @@ -70,6 +70,7 @@ export enum DirectionLabel { DESC = 'Décroissant' } export enum Order { + LABEL = 'label', CODE = 'code', RESULT_SIZE = 'result_size', FAVORITE = 'favorite', @@ -101,7 +102,8 @@ export enum Order { MEDICATION_ATC = 'medication-atc', MEDICATION_UCD = 'medication-ucd', PRESCRIPTION_TYPES = 'category-name', - ADMINISTRATION_MODE = 'route' + ADMINISTRATION_MODE = 'route', + DISPLAY = 'display' } export enum SearchByTypes { TEXT = '_text', @@ -138,8 +140,6 @@ export enum FilterKeys { PRESCRIPTION_TYPES = 'prescriptionTypes', CODE = 'code', NDA = 'nda', - ANABIO = 'anabio', - LOINC = 'loinc', DIAGNOSTIC_TYPES = 'diagnosticTypes', START_DATE = 'startDate', END_DATE = 'endDate', @@ -239,12 +239,12 @@ export type PMSIFilters = GenericFilter & { export type MedicationFilters = GenericFilter & { prescriptionTypes?: LabelObject[] administrationRoutes?: LabelObject[] - ipp?: string + ipp?: string, + code: LabelObject[] } export type BiologyFilters = GenericFilter & { - loinc: LabelObject[] - anabio: LabelObject[] + code: LabelObject[] validatedStatus: boolean ipp?: string } diff --git a/src/types/valueSet.ts b/src/types/valueSet.ts new file mode 100644 index 000000000..aaa1643ae --- /dev/null +++ b/src/types/valueSet.ts @@ -0,0 +1,36 @@ +import { HierarchyWithLabel } from './hierarchy' + +export enum References { + ATC, + UCD, + UCD_13, + LOINC, + ANABIO, + GHM, + CIM10, + CCAM +} + +export enum ReferencesLabel { + ATC = 'ATC', + UCD = 'UCD', + UCD_13 = 'UCD 13', + LOINC = 'Loinc', + ANABIO = 'Anabio', + GHM = 'Ghm', + CIM10 = 'Cim10', + CCAM = 'Ccam' +} + +export type Reference = { + id: References + label: string + title: string + standard: boolean + url: string + checked: boolean + isHierarchy: boolean + joinDisplayWithCode: boolean + joinDisplayWithSystem: boolean + filterRoots?: (code: HierarchyWithLabel) => boolean +} \ No newline at end of file diff --git a/src/utils/alphabeticalSort.ts b/src/utils/alphabeticalSort.ts deleted file mode 100644 index a16e63499..000000000 --- a/src/utils/alphabeticalSort.ts +++ /dev/null @@ -1,46 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -import moment from 'moment' -import { Direction } from 'types/searchCriterias' - -export const descendingComparator = (a: any, b: any, orderBy: string | ((obj: any) => any)): number => { - const fieldExtractor = - typeof orderBy === 'string' || orderBy instanceof String ? (obj: any) => obj[orderBy as string] : orderBy - const dateA = moment(new Date(fieldExtractor(a))) - const dateB = moment(new Date(fieldExtractor(b))) - - if (dateA.isValid() && dateB.isValid()) { - return dateA.isSameOrBefore(dateB) ? -1 : 1 - } - if (fieldExtractor(b) < fieldExtractor(a)) { - return -1 - } - if (fieldExtractor(b) > fieldExtractor(a)) { - return 1 - } - return 0 -} - -export const getComparator = ( - order: Direction, - orderBy: string | ((obj: any) => any) -): ((a: any, b: any) => number) => { - return order === Direction.DESC - ? (a: any, b: any): number => descendingComparator(a, b, orderBy) - : (a: any, b: any): number => -descendingComparator(a, b, orderBy) -} - -export const displaySort = getComparator(Direction.DESC, (obj: any) => obj.display) - -export const idSort = getComparator(Direction.ASC, (obj: any) => obj.id) - -export const labelSort = getComparator(Direction.ASC, (obj: any) => obj.label) - -export const stableSort = (array: any[], comparator: any): any[] => { - const stabilizedThis = array.map((el, index) => [el, index]) - stabilizedThis.sort((a, b) => { - const order = comparator(a[0], b[0]) - if (order !== 0) return order - return a[1] - b[1] - }) - return stabilizedThis.map((el) => el[0]) -} diff --git a/src/utils/arrays.ts b/src/utils/arrays.ts index d6337228a..77326dd39 100644 --- a/src/utils/arrays.ts +++ b/src/utils/arrays.ts @@ -15,8 +15,8 @@ export const addElement = (toAdd: WithId, elems: WithId[]) => return [...elems] } -export const removeElement = (toAdd: WithId, elems: WithId[]) => { - const existingIndex = elems.findIndex((elem) => elem.id === toAdd.id) +export const removeElement = (toRemove: WithId, elems: WithId[]) => { + const existingIndex = elems.findIndex((elem) => elem.id === toRemove.id) if (existingIndex > -1) { elems.splice(existingIndex, 1) } @@ -39,7 +39,7 @@ export const arrayToMap = (array: T[], value: S): Map => { return resultMap } -export const sort = (array: T[], attr: keyof T): T[] => { +export const sortArray = (array: T[], attr: keyof T): T[] => { try { array.sort((a, b) => a[attr].localeCompare(b[attr])) } catch { diff --git a/src/utils/cohortCreation.test.ts b/src/utils/cohortCreation.test.ts new file mode 100644 index 000000000..600e48ed5 --- /dev/null +++ b/src/utils/cohortCreation.test.ts @@ -0,0 +1,81 @@ +import { SelectedCriteriaType } from 'types/requestCriterias' +import { + procedurePeudonimizedCriteria, + ippNominativeCriteria, + ippEmptyCriteria, + encounterPseudonimizedCriteria, + encounterPseudoAgeCriteria, + encounterNominativeAgeCriteria, + patientPseudonimizedCriteria, + patientPseudonimizedAgeCriteria, + patientNominativeAge0Criteria, + patientNominativeAge1Criteria, + patientNominativeBirthdates, + patientNominativeDeathDates, + criteriasArrayWtihNominativeData, + criteriaArrayWithNoNominativeData +} from '../__tests__/data/cohortCreation.' +import { checkNominativeCriteria } from './cohortCreation' + +describe('test of checkNominativeCriteria', () => { + it('should return false if selectedCriteria is an empty array', () => { + const selectedCriteria: SelectedCriteriaType[] = [] + expect(checkNominativeCriteria(selectedCriteria)).toBe(false) + }) + it('should return false if selectedCriteria contain procedure pseudonimized criteria', () => { + const selectedCriteria: SelectedCriteriaType[] = procedurePeudonimizedCriteria + expect(checkNominativeCriteria(selectedCriteria)).toBe(false) + }) + it('should return false if selectedCriteria contain patient pseudonimized criteria', () => { + const selectedCriteria: SelectedCriteriaType[] = patientPseudonimizedCriteria + expect(checkNominativeCriteria(selectedCriteria)).toBe(false) + }) + it('should return false if selectedCriteria contains patient.age pseudonimized', () => { + const selectedCriteria: SelectedCriteriaType[] = patientPseudonimizedAgeCriteria + expect(checkNominativeCriteria(selectedCriteria)).toBe(false) + }) + it('should return true if selectedCriteria contain patient.age[0] nominative', () => { + const selectedCriteria: SelectedCriteriaType[] = patientNominativeAge0Criteria + expect(checkNominativeCriteria(selectedCriteria)).toBe(true) + }) + it('should return true if selectedCriteria contain patient.age[1] nominative', () => { + const selectedCriteria: SelectedCriteriaType[] = patientNominativeAge1Criteria + expect(checkNominativeCriteria(selectedCriteria)).toBe(true) + }) + it('should return true if selectedCriteria contains a patient birthdates', () => { + const selectedCriteria: SelectedCriteriaType[] = patientNominativeBirthdates + expect(checkNominativeCriteria(selectedCriteria)).toBe(true) + }) + it('should return true if selectedCriteria contains a patient deathDates', () => { + const selectedCriteria: SelectedCriteriaType[] = patientNominativeDeathDates + expect(checkNominativeCriteria(selectedCriteria)).toBe(true) + }) + it('should return true if selectedCriteria contains a nominative criteria', () => { + const selectedCriteria: SelectedCriteriaType[] = criteriasArrayWtihNominativeData + expect(checkNominativeCriteria(selectedCriteria)).toBe(true) + }) + it("should return false if selectedCriteria doesn't contains a nominative criteria", () => { + const selectedCriteria: SelectedCriteriaType[] = criteriaArrayWithNoNominativeData + expect(checkNominativeCriteria(selectedCriteria)).toBe(false) + }) + it('should return true if selectedCriteria contains a nominative IPP.search', () => { + const selectedCriteria: SelectedCriteriaType[] = ippNominativeCriteria + expect(checkNominativeCriteria(selectedCriteria)).toBe(true) + }) + it('should return true if selectedCriteria contains an IPP criteria', () => { + const selectedCriteria: SelectedCriteriaType[] = ippEmptyCriteria + expect(checkNominativeCriteria(selectedCriteria)).toBe(true) + }) + it('should return false if selectedCriteria contains an encounter pseudonimized criteria', () => { + const selectedCriteria: SelectedCriteriaType[] = encounterPseudonimizedCriteria + expect(checkNominativeCriteria(selectedCriteria)).toBe(false) + }) + it('should return false if selectedCriteria contains a peudonimize encounter.age', () => { + const selectedCriteria: SelectedCriteriaType[] = encounterPseudoAgeCriteria + expect(checkNominativeCriteria(selectedCriteria)).toBe(false) + }) + it('should return true if selectedCriteria contains a nominative encounter.age', () => { + const selectedCriteria: SelectedCriteriaType[] = encounterNominativeAgeCriteria + expect(checkNominativeCriteria(selectedCriteria)).toBe(true) + }) +}) diff --git a/src/utils/cohortCreation.ts b/src/utils/cohortCreation.ts index 8bd3c7da0..6c4ed9c98 100644 --- a/src/utils/cohortCreation.ts +++ b/src/utils/cohortCreation.ts @@ -73,7 +73,6 @@ import { pregnancyForm } from 'data/pregnancyData' import { hospitForm } from 'data/hospitData' import { editAllCriteria, editAllCriteriaGroup, pseudonimizeCriteria, buildCohortCreation } from 'state/cohortCreation' import { AppDispatch } from 'state' -import { Hierarchy } from 'types/hierarchy' import { AdministrationParamsKeys, ClaimParamsKeys, @@ -89,6 +88,7 @@ import { QuestionnaireResponseParamsKeys } from 'mappers/filters' import { getConfig } from 'config' +import { Hierarchy, HierarchyElementWithSystem } from 'types/hierarchy' const REQUETEUR_VERSION = 'v1.6.0' @@ -263,7 +263,7 @@ export const cleanNominativeCriterias = ( dispatch(editAllCriteria(cleanedSelectedCriteria)) dispatch(pseudonimizeCriteria()) if (selectedPopulation != undefined && selectedPopulation.length > 0) { - dispatch(buildCohortCreation({ selectedPopulation: selectedPopulation })) + dispatch(buildCohortCreation({ selectedPopulation: selectedPopulation as Hierarchy[] })) } else { dispatch(buildCohortCreation({ selectedPopulation: null })) } @@ -373,10 +373,7 @@ export const buildDocumentFilter = (criterion: DocumentDataType): string[] => { export const buildConditionFilter = (criterion: Cim10DataType): string[] => { return [ 'subject.active=true', - filtersBuilders( - ConditionParamsKeys.CODE, - buildLabelObjectFilter(criterion.code, getConfig().features.condition.valueSets.conditionHierarchy.url) - ), + filtersBuilders(ConditionParamsKeys.CODE, buildLabelObjectFilter(criterion.code, true)), filtersBuilders(ConditionParamsKeys.DIAGNOSTIC_TYPES, buildLabelObjectFilter(criterion.diagnosticType)), criterion.source ? buildSimpleFilter(criterion.source, ProcedureParamsKeys.SOURCE) : '', filtersBuilders(ConditionParamsKeys.EXECUTIVE_UNITS, buildEncounterServiceFilter(criterion.encounterService)), @@ -398,7 +395,7 @@ export const buildProcedureFilter = (criterion: CcamDataType): string[] => { 'subject.active=true', filtersBuilders( ProcedureParamsKeys.CODE, - buildLabelObjectFilter(criterion.code, getConfig().features.procedure.valueSets.procedureHierarchy.url) + buildLabelObjectFilter(criterion.code, true) ), filtersBuilders(ProcedureParamsKeys.EXECUTIVE_UNITS, buildEncounterServiceFilter(criterion.encounterService)), filtersBuilders(ProcedureParamsKeys.ENCOUNTER_STATUS, buildLabelObjectFilter(criterion.encounterStatus)), @@ -420,7 +417,7 @@ export const buildClaimFilter = (criterion: GhmDataType): string[] => { 'patient.active=true', filtersBuilders( ClaimParamsKeys.CODE, - buildLabelObjectFilter(criterion.code, getConfig().features.claim.valueSets.claimHierarchy.url) + buildLabelObjectFilter(criterion.code, true) ), filtersBuilders(ClaimParamsKeys.EXECUTIVE_UNITS, buildEncounterServiceFilter(criterion.encounterService)), filtersBuilders(ClaimParamsKeys.ENCOUNTER_STATUS, buildLabelObjectFilter(criterion.encounterStatus)), @@ -451,10 +448,7 @@ export const buildMedicationFilter = (criterion: MedicationDataType): string[] = : AdministrationParamsKeys.EXECUTIVE_UNITS, buildEncounterServiceFilter(criterion.encounterService) ), - filtersBuilders( - PrescriptionParamsKeys.CODE, - buildLabelObjectFilter(criterion.code, getConfig().features.medication.valueSets.medicationAtc.url, true) - ), + filtersBuilders(PrescriptionParamsKeys.CODE, buildLabelObjectFilter(criterion.code, true)), filtersBuilders( criterion.type === CriteriaType.MEDICATION_REQUEST ? PrescriptionParamsKeys.ENCOUNTER_STATUS @@ -487,10 +481,7 @@ export const buildMedicationFilter = (criterion: MedicationDataType): string[] = export const buildObservationFilter = (criterion: ObservationDataType): string[] => { return [ `subject.active=true&${ObservationParamsKeys.VALIDATED_STATUS}=${BiologyStatus.VALIDATED}`, - filtersBuilders( - ObservationParamsKeys.ANABIO_LOINC, - buildLabelObjectFilter(criterion.code, getConfig().features.observation.valueSets.biologyHierarchyAnabio.url) - ), + filtersBuilders(ObservationParamsKeys.ANABIO_LOINC, buildLabelObjectFilter(criterion.code, true)), filtersBuilders(ObservationParamsKeys.EXECUTIVE_UNITS, buildEncounterServiceFilter(criterion.encounterService)), filtersBuilders(ObservationParamsKeys.ENCOUNTER_STATUS, buildLabelObjectFilter(criterion.encounterStatus)), filtersBuilders(ObservationParamsKeys.DATE, buildDateFilter(criterion.startOccurrence[0], 'ge')), @@ -1176,7 +1167,6 @@ const unbuildProcedureCriteria = async (element: RequeteurCriteriaType): Promise startOccurrence: [null, null], source: null, label: undefined, - hierarchy: undefined, encounterService: [], occurrenceComparator: null, encounterStatus: [], @@ -1259,6 +1249,7 @@ const unbuildClaimCriteria = async (element: RequeteurCriteriaType): Promise => { + console.log('test unbuild') const currentCriterion: MedicationDataType = { ...unbuildCommonCriteria(element), title: element.name ?? 'Critère de médicament', @@ -2113,7 +2104,6 @@ export const getDataFromFetch = async ( (criterion.type === CriteriaType.MEDICATION_REQUEST || criterion.type === CriteriaType.MEDICATION_ADMINISTRATION)) ) - if (currentSelectedCriteria) { for (const currentcriterion of currentSelectedCriteria) { if ( @@ -2134,7 +2124,10 @@ export const getDataFromFetch = async ( const prevData = prevDataCache[dataKey]?.find((data: any) => data.id === code?.id) const codeData = prevData ? [prevData] : await _criterion.fetch[dataKey]?.(code?.id, true) const existingCodes = criteriaDataCache.data[dataKey] || [] - criteriaDataCache.data[dataKey] = [...existingCodes, ...(codeData || [])] + criteriaDataCache.data[dataKey] = [ + ...existingCodes, + ...((codeData as HierarchyElementWithSystem[]) || []) + ] } } } @@ -2226,44 +2219,3 @@ export const joinRequest = async (oldJson: string, newJson: string, parentId: nu criteriaGroup } } - -export const findSelectedInListAndSubItems = ( - selectedItems: Hierarchy[], - searchedItem: Hierarchy | undefined, - pmsiHierarchy: Hierarchy[], - valueSetSystem?: string -): boolean => { - if (!searchedItem || !selectedItems || selectedItems.length === 0) return false - selectedItems = selectedItems.filter(({ id }) => id !== 'loading') - const foundItem = selectedItems.find((selectedItem) => { - if (selectedItem.id === searchedItem.id || (selectedItem.id == '*' && valueSetSystem !== 'UCD')) { - return true - } - return selectedItem.subItems - ? findSelectedInListAndSubItems(selectedItem.subItems, searchedItem, pmsiHierarchy) - : false - }) - if (foundItem) { - return true - } - if ( - searchedItem.subItems && - searchedItem.subItems.length > 0 && - !(searchedItem.subItems.length === 1 && searchedItem.subItems[0].id === 'loading') - ) { - const numberOfSubItemsSelected = searchedItem.subItems?.filter((searchedSubItem: any) => - selectedItems.find((selectedItem) => selectedItem.id === searchedSubItem.id) - )?.length - if (searchedItem.subItems?.length === numberOfSubItemsSelected) { - return true - } - const isSingleItemNotSelected = (searchedItem.subItems?.length ?? 0 - (numberOfSubItemsSelected ?? 0)) === 1 - if (isSingleItemNotSelected) { - const singleItemNotSelected = searchedItem.subItems?.find((searchedSubItem: any) => - selectedItems.find((selectedItem) => selectedItem.id !== searchedSubItem.id) - ) - return findSelectedInListAndSubItems(selectedItems, singleItemNotSelected, pmsiHierarchy) - } - } - return false -} diff --git a/src/utils/displayValueSetSystem.ts b/src/utils/displayValueSetSystem.ts deleted file mode 100644 index fb6902686..000000000 --- a/src/utils/displayValueSetSystem.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { getConfig } from 'config' - -export const displaySystem = (system?: string) => { - switch (system) { - case getConfig().features.medication.valueSets.medicationAtc.url: - return 'ATC: ' - case getConfig().features.medication.valueSets.medicationUcd.url: - return 'UCD: ' - default: - return '' - } -} diff --git a/src/utils/filters.ts b/src/utils/filters.ts index 80c7631c8..111997f19 100644 --- a/src/utils/filters.ts +++ b/src/utils/filters.ts @@ -17,6 +17,7 @@ import { ScopeElement, SimpleCodeType, ValueSet } from 'types' import { getDurationRangeLabel } from './age' import { CohortsType, CohortsTypeLabel } from 'types/cohorts' import { Hierarchy } from 'types/hierarchy' +import { getFullLabelFromCode } from './valueSets' export const getCohortsTypeLabel = (type: CohortsType): string => { switch (type) { @@ -62,8 +63,6 @@ export const removeFilter = (key: FilterKeys, value: FilterValue, filters: F) case FilterKeys.DOC_TYPES: case FilterKeys.STATUS: case FilterKeys.MODALITY: - case FilterKeys.LOINC: - case FilterKeys.ANABIO: case FilterKeys.CODE: case FilterKeys.FORM_NAME: case FilterKeys.ENCOUNTER_STATUS: @@ -124,14 +123,14 @@ export const getFilterLabel = (key: FilterKeys, value: FilterValue): string => { return `IPP : ${value}` } if (key === FilterKeys.CODE) { - return `Code : ${(value as LabelObject).label}` + return `Code : ${getFullLabelFromCode(value as LabelObject)}` } if (key === FilterKeys.SOURCE) { return `Source : ${value}` } if (key === FilterKeys.EXECUTIVE_UNITS) { - return `Unité exécutrice : ${(value as Hierarchy).source_value} - ${ - (value as Hierarchy).name + return `Unité exécutrice : ${(value as Hierarchy).source_value} - ${ + (value as Hierarchy).name }` } if (key === FilterKeys.DOC_STATUSES) { @@ -149,12 +148,6 @@ export const getFilterLabel = (key: FilterKeys, value: FilterValue): string => { if (key === FilterKeys.PRESCRIPTION_TYPES) { return `Type de prescription : ${capitalizeFirstLetter((value as LabelObject)?.label as string)}` } - if (key === FilterKeys.ANABIO) { - return `Code ANABIO : ${(value as LabelObject).label}` - } - if (key === FilterKeys.LOINC) { - return `Code LOINC : ${(value as LabelObject).label}` - } if (key === FilterKeys.STATUS) { return `Statut : ${(value as ValueSet)?.display}` } @@ -194,8 +187,6 @@ export const selectFiltersAsArray = (filters: Filters) => { case FilterKeys.EXECUTIVE_UNITS: case FilterKeys.STATUS: case FilterKeys.MODALITY: - case FilterKeys.LOINC: - case FilterKeys.ANABIO: case FilterKeys.CODE: case FilterKeys.FORM_NAME: case FilterKeys.ENCOUNTER_STATUS: diff --git a/src/utils/hierarchy.ts b/src/utils/hierarchy.ts deleted file mode 100644 index d65eab444..000000000 --- a/src/utils/hierarchy.ts +++ /dev/null @@ -1,233 +0,0 @@ -import { SelectedStatus } from 'types' -import { Hierarchy, InfiniteMap, Mode } from 'types/hierarchy' -import { arrayToMap } from './arrays' - -export const cleanNodes = (nodes: Hierarchy[]) => { - return nodes.map((item) => ({ ...item, subItems: undefined, status: undefined })) -} - -export const mapHierarchyToMap = (hierarchy: Hierarchy[]) => { - return hierarchy.reduce((resultMap: Map>, item) => { - resultMap.set(item.id, item) - return resultMap - }, new Map()) -} - -const getMissingIds = (prevCodes: Map>, codes: Map) => { - const missingCodes: string[] = [] - for (const [key] of codes) { - const isFound = prevCodes.get(key) - if (!isFound) missingCodes.push(key) - } - return missingCodes -} - -const addAllFetchedIds = (codes: Map>, results: Hierarchy[]) => { - const resultsMap = mapHierarchyToMap(results) - return new Map([...codes, ...resultsMap]) -} - -export const getMissingCodes = async ( - baseTree: Hierarchy[], - prevCodes: Map>, - newCodes: Hierarchy[], - mode: Mode, - fetchHandler: (ids: string) => Promise[]> -) => { - const newCodesMap = mapHierarchyToMap(newCodes) - let allCodes = new Map([...prevCodes, ...newCodesMap]) - let missingIds: string[] = [] - let above: string[] = [] - if (mode === Mode.EXPAND) missingIds = getMissingIds(allCodes, arrayToMap(getInferiorLevels(newCodes), null)) - else { - above = getAboveLevels(newCodes, baseTree) - missingIds = getMissingIds(allCodes, arrayToMap(above, null)) - } - if (missingIds.length) { - const ids = missingIds.join(',') - const fetched = await fetchHandler(ids) - allCodes = addAllFetchedIds(allCodes, fetched) - } - if (mode !== Mode.EXPAND) { - const children = getInferiorLevels(above.map((id) => allCodes.get(id)!)) - missingIds = getMissingIds(allCodes, arrayToMap(children, null)) - if (missingIds.length) { - const ids = missingIds.join(',') - const childrenResponse = await fetchHandler(ids) - allCodes = addAllFetchedIds(allCodes, childrenResponse) - } - } - return allCodes -} - -const getMissingNode = (key: string, node: Hierarchy, codes: Map>) => { - const code = codes.get(key) ?? null - if (!node) if (code) node = { ...code } - return node -} - -const getMissingSubItems = (node: Hierarchy, codes: Map>) => { - const subItems: Hierarchy[] = [] - const levels = node.inferior_levels_ids?.split(',') - levels.forEach((id) => { - const foundCode = codes.get(id) - if (foundCode) subItems.push({ ...foundCode }) - }) - return subItems.length ? subItems : [] -} - -export const buildHierarchy = ( - baseTree: Hierarchy[], - endCodes: Hierarchy[], - codes: Map>, - selected: Hierarchy[], - mode: Mode -) => { - const buildBranch = ( - node: Hierarchy, - path: [string, InfiniteMap], - codes: Map>, - selected: Map>, - mode: Mode - ) => { - const [key, nextPath] = path - node = getMissingNode(key, node, codes) - if (nextPath.size) { - if (!node.subItems) node.subItems = getMissingSubItems(node, codes) - for (const [nextKey, nextValue] of nextPath) { - const index = node.subItems.findIndex((elem) => elem.id === nextKey) - if (index !== undefined && index > -1) { - const item = buildBranch(node.subItems[index], [nextKey, nextValue], codes, selected, mode) - node.subItems[index] = item - } - node.status = getItemSelectedStatus(node) - } - } else { - if (mode === Mode.EXPAND && !node.subItems) node.subItems = getMissingSubItems(node, codes) - if (mode === Mode.SELECT || mode === Mode.SELECT_ALL) node.status = SelectedStatus.SELECTED - if (mode === Mode.UNSELECT || mode === Mode.UNSELECT_ALL) node.status = SelectedStatus.NOT_SELECTED - if (mode !== Mode.SEARCH && mode !== Mode.INIT) updateBranchStatus(node, node.status) - } - if ((mode === Mode.SEARCH || mode === Mode.INIT) && selected.get(key)) { - node.status = SelectedStatus.SELECTED - updateBranchStatus(node, SelectedStatus.SELECTED) - } - return node - } - - const paths = getPaths(baseTree, endCodes, mode === Mode.UNSELECT_ALL || mode === Mode.SELECT_ALL) - const uniquePaths = getUniquePath(paths) - if (mode === Mode.INIT) baseTree = [] - for (const [key, value] of uniquePaths) { - const index = baseTree.findIndex((elem) => elem.id === key) - const branch = buildBranch(baseTree[index] || null, [key, value], codes, mapHierarchyToMap(selected), mode) - if (branch && index > -1) baseTree[index] = branch - else baseTree.push(branch) - } - return [...baseTree] -} - -export const getHierarchyDisplay = (defaultLevels: Hierarchy[], tree: Hierarchy[]) => { - let branches: Hierarchy[] = [] - if (defaultLevels.length && tree.length) - branches = defaultLevels.map((item) => { - const path = item.above_levels_ids ? [...getAboveLevelsWithRights(item, tree), ...[item.id]] : [item.id] - return findBranch(path, tree) || { id: 'notFound' } - }) - return branches -} - -const findBranch = (path: string[], tree: Hierarchy[]): Hierarchy => { - let branch: Hierarchy = { id: 'empty' } as Hierarchy - const key = path[0] - const index = tree.findIndex((item) => item.id === key) - const next = tree[index] - if (path.length === 1) { - branch = next - } else if (next && next.subItems) { - if (next.status !== SelectedStatus.INDETERMINATE) - next.subItems = next.subItems.map((item) => ({ ...item, status: next.status })) - branch = findBranch(path.slice(1), next.subItems) - } - return branch -} - -const getPaths = (baseTree: Hierarchy[], endCodes: Hierarchy[], selectAll: boolean) => { - let paths = endCodes.map((item) => - item.above_levels_ids ? [...getAboveLevelsWithRights(item, baseTree), ...[item.id]] : [item.id] - ) - if (selectAll) { - for (const path of paths) { - const lastId = path[path.length - 1] - paths = paths.filter((path) => { - const index = path.findIndex((id) => id === lastId) - if (index < 0 || index === path.length - 1) return true - return false - }) - } - } - return paths -} - -const getUniquePath = (paths: string[][]): InfiniteMap => { - const tree = new Map() - for (const path of paths) { - let currentNode = tree - for (const id of path) { - if (!currentNode.has(id)) { - currentNode.set(id, new Map()) - } - currentNode = currentNode.get(id) - } - } - return tree -} - -const updateBranchStatus = (node: Hierarchy, status: SelectedStatus | undefined) => { - if (status !== undefined && status !== SelectedStatus.INDETERMINATE && node.subItems) { - for (const subItem of node.subItems) { - subItem.status = status === SelectedStatus.SELECTED ? SelectedStatus.SELECTED : SelectedStatus.NOT_SELECTED - updateBranchStatus(subItem, status) - } - } - return node -} - -export const getItemSelectedStatus = (item: Hierarchy): SelectedStatus => { - if (item.subItems?.every((item) => item.status === SelectedStatus.SELECTED)) return SelectedStatus.SELECTED - if (item.subItems?.every((item) => item.status === SelectedStatus.NOT_SELECTED || item.status === undefined)) - return SelectedStatus.NOT_SELECTED - return SelectedStatus.INDETERMINATE -} - -export const getSelectedCodes = (list: Hierarchy[]) => { - const get = (hierarchy: Hierarchy, selectedCodes: Hierarchy[]) => { - if (hierarchy.status === SelectedStatus.INDETERMINATE) - hierarchy.subItems?.forEach((subItem) => get(subItem, selectedCodes)) - if (hierarchy.status === SelectedStatus.SELECTED) selectedCodes.push(hierarchy) - return selectedCodes - } - const selectedCodes = list.flatMap((hierarchy) => get(hierarchy, [])) - return selectedCodes -} - -const getAboveLevelsWithRights = (item: Hierarchy, baseTree: Hierarchy[]) => { - const levels = (item.above_levels_ids || '').split(',') - if (baseTree.find((item) => item.id === levels[0])) return levels - const ids = baseTree.map((code) => code.id) - const startIndex = levels.findIndex((level) => ids.includes(level)) - if (startIndex > -1) return levels.slice(startIndex) - return [] -} - -const getAboveLevels = (hierarchy: Hierarchy[], baseTree: Hierarchy[]) => { - return hierarchy.flatMap((item) => - [...getAboveLevelsWithRights(item, baseTree)].filter((item) => item && item !== 'null' && item !== 'undefined') - ) -} - -const getInferiorLevels = (hierarchy: Hierarchy[]) => { - return hierarchy - .flatMap((item) => (item.inferior_levels_ids || '').split(',')) - .filter((item) => item && item !== 'null' && item !== 'undefined') -} diff --git a/src/utils/hierarchy/hierarchy.test.ts b/src/utils/hierarchy/hierarchy.test.ts new file mode 100644 index 000000000..6060b2e3f --- /dev/null +++ b/src/utils/hierarchy/hierarchy.test.ts @@ -0,0 +1,127 @@ +// Importer les types nécessaires et la fonction à tester +import { Hierarchy, Mode } from 'types/hierarchy' +import { buildTree } from './hierarchy' // Remplacez par le chemin réel + +// Mock des fonctions utilisées dans buildTree +import { getMissingNode, getMissingSubItems, getItemSelectedStatus, updateBranchStatus } from './hierarchy' +import { SelectedStatus } from 'types' + +/*jest.mock('./path_to_your_file', () => ({ + ...jest.requireActual('./path_to_your_file'), + getMissingNode: jest.fn((key, node, codes) => node), + getMissingSubItems: jest.fn((node, codes) => []), + getItemSelectedStatus: jest.fn((node) => SelectedStatus.NOT_SELECTED), + updateBranchStatus: jest.fn((node, status) => {}) +}))*/ + +describe('buildTree', () => { + beforeEach(() => { + jest.clearAllMocks() + }) + + it('should correctly build the tree with Mode.EXPAND', () => { + const baseTree: Hierarchy<{}, string>[] = [ + { + id: '1', + + label: 'root', + above_levels_ids: '', + inferior_levels_ids: '', + system: 'system1', + subItems: [] + } + ] + + const endCodes: Hierarchy<{}, string>[] = [] + const codes = new Map>() + const selected: Hierarchy<{}, string>[] = [] + + const result = buildTree(baseTree, endCodes, codes, selected, Mode.EXPAND) + + expect(result).toEqual(baseTree) + expect(getMissingNode).toHaveBeenCalled() + expect(getMissingSubItems).toHaveBeenCalled() + }) + + it('should correctly handle Mode.SELECT', () => { + const baseTree: Hierarchy<{}, string>[] = [ + { + id: '1', + label: 'root', + above_levels_ids: '', + inferior_levels_ids: '', + system: 'system1', + subItems: [] + } + ] + + const endCodes: Hierarchy<{}, string>[] = [] + const codes = new Map>() + const selected: Hierarchy<{}, string>[] = [] + + const result = buildTree(baseTree, endCodes, codes, selected, Mode.SELECT) + + expect(result[0].status).toBe(SelectedStatus.SELECTED) + expect(getMissingNode).toHaveBeenCalled() + }) + + it('should correctly handle Mode.UNSELECT', () => { + const baseTree: Hierarchy<{}, string>[] = [ + { + id: '1', + + label: 'root', + above_levels_ids: '', + inferior_levels_ids: '', + system: 'system1', + subItems: [], + status: SelectedStatus.SELECTED + } + ] + + const endCodes: Hierarchy<{}, string>[] = [] + const codes = new Map>() + const selected: Hierarchy<{}, string>[] = [] + + const result = buildTree(baseTree, endCodes, codes, selected, Mode.UNSELECT) + + expect(result[0].status).toBe(SelectedStatus.NOT_SELECTED) + expect(getMissingNode).toHaveBeenCalled() + }) + + it('should correctly handle Mode.SEARCH', () => { + const baseTree: Hierarchy<{}, string>[] = [ + { + id: '1', + + label: 'root', + above_levels_ids: '', + inferior_levels_ids: '', + system: 'system1', + subItems: [] + } + ] + + const endCodes: Hierarchy<{}, string>[] = [] + const codes = new Map>() + const selected: Hierarchy<{}, string>[] = [ + { + id: '1', + + label: 'root', + above_levels_ids: '', + inferior_levels_ids: '', + system: 'system1', + status: SelectedStatus.SELECTED, + subItems: [] + } + ] + + const result = buildTree(baseTree, endCodes, codes, selected, Mode.SEARCH) + + expect(result[0].status).toBe(SelectedStatus.SELECTED) + expect(updateBranchStatus).toHaveBeenCalledWith(result[0], SelectedStatus.SELECTED) + }) + + // Ajoutez d'autres tests selon vos besoins... +}) diff --git a/src/utils/hierarchy/hierarchy.ts b/src/utils/hierarchy/hierarchy.ts new file mode 100644 index 000000000..5de392f58 --- /dev/null +++ b/src/utils/hierarchy/hierarchy.ts @@ -0,0 +1,366 @@ +import { SelectedStatus } from 'types' +import { Codes, CodesCache, GroupedBySystem, Hierarchy, InfiniteMap, Mode } from 'types/hierarchy' +import { arrayToMap } from '../arrays' +import { HIERARCHY_ROOT, UNKOWN_HIERARCHY_CHAPTER } from 'services/aphp/serviceValueSets' + +export const mapCodesToCache = (codes: Codes>) => { + const valueSetOptions: CodesCache[] = [] + codes.forEach((innerMap, outerKey) => { + const options: Record> = {} + innerMap.forEach((hierarchy, innerKey) => (options[innerKey] = cleanNode(hierarchy))) + valueSetOptions.push({ + id: outerKey, + options + }) + }) + return valueSetOptions +} + +export const mapCacheToCodes = (valueSets: CodesCache[]): Codes> => { + const codes: Codes> = new Map() + valueSets.forEach((valueSet) => { + const innerMap: Map> = new Map() + Object.entries(valueSet.options).forEach(([key, hierarchy]) => innerMap.set(key, hierarchy)) + codes.set(valueSet.id, innerMap) + }) + return codes +} + +export const getHierarchyRootCodes = (tree: Hierarchy[]) => { + let codes: Map> = new Map() + for (const root of tree) { + if (root.subItems) { + codes = new Map([...codes, ...mapHierarchyToMap(root.subItems)]) + const unknownChapter = root.subItems.find((item) => item.id === UNKOWN_HIERARCHY_CHAPTER) + if (unknownChapter && unknownChapter.subItems) + codes = new Map([...codes, ...mapHierarchyToMap(unknownChapter.subItems)]) + } + } + return codes +} + +export const cleanNode = (node: Hierarchy) => { + return { ...node, subItems: undefined, status: undefined } +} + +export const mapHierarchyToMap = (hierarchy: Hierarchy[]) => { + return hierarchy.reduce((resultMap: Map>, item) => { + resultMap.set(item.id, item) + return resultMap + }, new Map()) +} + +const getMissingIds = (prevCodes: Map>, codes: Map) => { + const missingCodes: string[] = [] + for (const [key] of codes) { + const isFound = prevCodes.get(key) + if (!isFound) missingCodes.push(key) + } + return missingCodes +} + +const addAllFetchedIds = (codes: Map>, results: Hierarchy[]) => { + const resultsMap = mapHierarchyToMap(results) + return new Map([...codes, ...resultsMap]) +} + +export const getMissingCodesWithSystems = async ( + trees: Map[]>, + groupBySystem: GroupedBySystem[], + codes: Codes>, + fetchHandler: (ids: string, system: string) => Promise[]> +) => { + let allCodes: Codes> = new Map(codes) + for (const group of groupBySystem) { + const tree = trees.get(group.system) || [] + const codesBySystem = codes.get(group.system) || new Map() + const newCodes = await getMissingCodes(tree, codesBySystem, group.codes, group.system, Mode.SEARCH, fetchHandler) + allCodes.set(group.system, newCodes) + } + return allCodes +} + +export const getMissingCodes = async ( + baseTree: Hierarchy[], + prevCodes: Map>, + newCodes: Hierarchy[], + system: string, + mode: Mode, + fetchHandler: (ids: string, system: string) => Promise[]> +) => { + const newCodesMap = mapHierarchyToMap(newCodes) + let allCodes = new Map([...prevCodes, ...newCodesMap]) + let missingIds: string[] = [] + let above: string[] = [] + if (mode === Mode.EXPAND) missingIds = getMissingIds(allCodes, arrayToMap(getInferiorLevels(newCodes), null)) + else { + above = getAboveLevels(newCodes, baseTree) + missingIds = getMissingIds(allCodes, arrayToMap(above, null)) + } + //console.log('test missing', system, missingIds) + if (missingIds.length) { + const ids = missingIds.join(',') + const fetched = await fetchHandler(ids, system) + allCodes = addAllFetchedIds(allCodes, fetched) + } + if (mode !== Mode.EXPAND) { + const children = getInferiorLevels(above.map((key) => allCodes.get(key)!)) + missingIds = getMissingIds(allCodes, arrayToMap(children, null)) + if (missingIds.length) { + const ids = missingIds.join(',') + const childrenResponse = await fetchHandler(ids, system) + allCodes = addAllFetchedIds(allCodes, childrenResponse) + } + } + return allCodes +} + +export const getMissingNode = (id: string, node: Hierarchy, codes: Map>) => { + const code = codes.get(id) ?? null + if (!node) if (code) node = { ...code } + return node +} + +export const getMissingSubItems = (node: Hierarchy, codes: Map>) => { + const subItems: Hierarchy[] = [] + const levels = node.inferior_levels_ids?.split(',') + levels.forEach((key) => { + const foundCode = codes.get(key) + if (foundCode) subItems.push({ ...foundCode }) + }) + return subItems.length ? subItems : [] +} + +export const buildMultipleTrees = ( + trees: Map[]>, + groupBySystem: GroupedBySystem[], + codes: Codes>, + selected: Codes>, + mode: Mode +) => { + for (const group of groupBySystem) { + const tree = trees.get(group.system) || [] + const codesBySystem = codes.get(group.system) || new Map() + const selectedBySystem = selected.get(group.system) || new Map() + const newTree = buildTree([...tree], group.system, group.codes, codesBySystem, selectedBySystem, mode) + trees.set(group.system, newTree) + } + return new Map(trees) +} + +/** + * @template T - The type of the data stored in the hierarchy nodes. + * @param {Hierarchy[]} baseTree - The base tree structure to build upon. + * @param {Hierarchy[]} endCodes - The end codes used for building the tree. + * @param {Map>} codes - A map of codes to hierarchy nodes. + * @param {Hierarchy[]} selected - A list of selected hierarchy nodes. + * @param {Mode} mode - The mode to operate in, affecting how the tree is built and updated. + * @returns {Hierarchy[]} The built tree structure. + */ +export const buildTree = ( + baseTree: Hierarchy[], + system: string, + endCodes: Hierarchy[], + codes: Map>, + selected: Map>, + mode: Mode +) => { + const buildBranch = ( + node: Hierarchy, + system: string, + path: [string, InfiniteMap], + codes: Map>, + selected: Map>, + mode: Mode + ) => { + const [currentPath, nextPath] = path + node = getMissingNode(currentPath, node, codes) + if (nextPath.size) { + if (!node.subItems) node.subItems = getMissingSubItems(node, codes) + for (const [nextKey, nextValue] of nextPath) { + let index = node.subItems.findIndex((elem) => elem.id === nextKey) + if (index > -1) { + const item = buildBranch(node.subItems[index], system, [nextKey, nextValue], codes, selected, mode) + node.subItems[index] = item + } + node.status = getItemSelectedStatus(node) + } + } else { + if (mode === Mode.EXPAND && !node.subItems) node.subItems = getMissingSubItems(node, codes) + if (mode === Mode.SELECT) node.status = SelectedStatus.SELECTED + if (mode === Mode.UNSELECT) node.status = SelectedStatus.NOT_SELECTED + if (mode !== Mode.SEARCH && mode !== Mode.INIT) updateBranchStatus(node, node.status) + } + if ((mode === Mode.SEARCH || mode === Mode.INIT) && (selected.get(currentPath) || selected.get(HIERARCHY_ROOT))) { + node.status = SelectedStatus.SELECTED + updateBranchStatus(node, SelectedStatus.SELECTED) + } + return node + } + const paths = getPaths(baseTree, endCodes, mode === Mode.UNSELECT_ALL || mode === Mode.SELECT_ALL) + const uniquePaths = getUniquePath(paths) + console.log('test select paths', uniquePaths) + if (mode === Mode.INIT || mode === Mode.SEARCH) baseTree = [] + for (const [key, value] of uniquePaths) { + const index = baseTree.findIndex((elem) => elem.id === key) + const branch = buildBranch(baseTree[index] || null, system, [key, value], codes, selected, mode) + if (branch && index > -1) baseTree[index] = branch + else if (index === -1) baseTree.push(branch) + } + return [...baseTree] +} + +export const groupBySystem = (codes: Hierarchy[]) => { + const systemMap = new Map[]>() + for (const hierarchy of codes) { + const system = hierarchy.system + if (!systemMap.has(system)) { + systemMap.set(system, []) + } + systemMap.get(system)!.push(hierarchy) + } + const groupedHierarchies: GroupedBySystem[] = [] + systemMap.forEach((codes, system) => { + groupedHierarchies.push({ system, codes }) + }) + return groupedHierarchies +} + +export const getHierarchyDisplay = (defaultLevels: Hierarchy[], tree: Hierarchy[]) => { + let branches: Hierarchy[] = [] + if (defaultLevels.length && tree.length) + branches = defaultLevels.map((item) => { + const path = item.above_levels_ids ? [...getAboveLevelsWithRights(item, tree), ...[item.id]] : [item.id] + return findBranch(path, tree) || { id: 'notFound' } + }) + return branches +} + +export const getListDisplay = (toDisplay: Hierarchy[], trees: Map[]>) => { + let branches: Hierarchy[] = [] + toDisplay.forEach((node) => { + const currentTree = trees.get(node.system) + if (currentTree) { + const foundNode = getHierarchyDisplay([node], currentTree)[0] + branches.push(foundNode) + } + }) + return branches +} + +const findBranch = (path: string[], tree: Hierarchy[]): Hierarchy => { + let branch: Hierarchy = { id: 'empty' } as Hierarchy + const key = path[0] + const index = tree.findIndex((item) => item.id === key) + const next = tree[index] + if (path.length === 1) { + branch = next + } else if (next && next.subItems) { + if (next.status !== SelectedStatus.INDETERMINATE) + next.subItems = next.subItems.map((item) => ({ ...item, status: next.status })) + branch = findBranch(path.slice(1), next.subItems) + } + return branch +} + +const getPaths = (baseTree: Hierarchy[], endCodes: Hierarchy[], selectAll: boolean) => { + let paths = endCodes.map((item) => + item.above_levels_ids ? [...getAboveLevelsWithRights(item, baseTree), ...[item.id]] : [item.id] + ) + if (selectAll) { + for (const path of paths) { + const lastId = path[path.length - 1] + paths = paths.filter((path) => { + const index = path.findIndex((id) => id === lastId) + if (index < 0 || index === path.length - 1) return true + return false + }) + } + } + return paths +} + +const getUniquePath = (paths: string[][]): InfiniteMap => { + const tree = new Map() + for (const path of paths) { + let currentNode = tree + for (const id of path) { + if (!currentNode.has(id)) { + currentNode.set(id, new Map()) + } + currentNode = currentNode.get(id) + } + } + return tree +} + +export const updateBranchStatus = (node: Hierarchy, status: SelectedStatus | undefined) => { + if (status !== undefined && status !== SelectedStatus.INDETERMINATE && node.subItems) { + for (const subItem of node.subItems) { + subItem.status = status === SelectedStatus.SELECTED ? SelectedStatus.SELECTED : SelectedStatus.NOT_SELECTED + updateBranchStatus(subItem, status) + } + } + return node +} + +export const getItemSelectedStatus = (item: Hierarchy): SelectedStatus => { + if (item.subItems?.every((item) => item.status === SelectedStatus.SELECTED)) return SelectedStatus.SELECTED + if (item.subItems?.every((item) => item.status === SelectedStatus.NOT_SELECTED || item.status === undefined)) + return SelectedStatus.NOT_SELECTED + return SelectedStatus.INDETERMINATE +} + +export const getSelectedCodesFromTree = (tree: Hierarchy[]) => { + const get = (node: Hierarchy, selectedCodes: Hierarchy[]) => { + if (node.status === SelectedStatus.INDETERMINATE) node.subItems?.forEach((subItem) => get(subItem, selectedCodes)) + if (node.status === SelectedStatus.SELECTED) selectedCodes.push(node) + return selectedCodes + } + const selectedCodes = mapHierarchyToMap(tree.flatMap((hierarchy) => get(hierarchy, []))) + return selectedCodes +} + +export const getSelectedCodesFromTrees = (trees: Map[]>, prevCodes: Codes) => { + const selectedCodes: Codes> = new Map() + trees.forEach((tree, key) => { + const isFound = prevCodes.get(key) + if (isFound && isFound.get(HIERARCHY_ROOT)) selectedCodes.set(key, isFound) + else selectedCodes.set(key, getSelectedCodesFromTree(tree)) + }) + return selectedCodes +} + +const getAboveLevelsWithRights = (item: Hierarchy, baseTree: Hierarchy[]) => { + const levels = (item.above_levels_ids || '').split(',') + if (baseTree.find((item) => item.id === levels[0])) return levels + const ids = baseTree.map((code) => code.id) + const startIndex = levels.findIndex((level) => ids.includes(level)) + if (startIndex > -1) return levels.slice(startIndex) + return [] +} + +const getAboveLevels = (hierarchy: Hierarchy[], baseTree: Hierarchy[]) => { + return hierarchy.flatMap((item) => { + return [...getAboveLevelsWithRights(item, baseTree)].filter( + (level) => level && level !== 'null' && level !== 'undefined' + ) + }) +} + +const getInferiorLevels = (hierarchy: Hierarchy[]) => { + return hierarchy.flatMap((item) => + (item?.inferior_levels_ids || '').split(',').filter((level) => level && level !== 'null' && level !== 'undefined') + ) +} + +export const createHierarchyRoot = (system: string, status?: SelectedStatus) => { + return { + id: HIERARCHY_ROOT, + label: 'Toute la hiérarchie', + above_levels_ids: '', + inferior_levels_ids: '', + system, + status + } +} diff --git a/src/utils/map.ts b/src/utils/map.ts new file mode 100644 index 000000000..19b70315d --- /dev/null +++ b/src/utils/map.ts @@ -0,0 +1,5 @@ +export const replaceInMap = (id: K, value: V, map: Map) => { + const newMap = new Map(map) + newMap.set(id, value) + return newMap +} \ No newline at end of file diff --git a/src/utils/mappers.ts b/src/utils/mappers.ts index 454eb2163..371236112 100644 --- a/src/utils/mappers.ts +++ b/src/utils/mappers.ts @@ -33,18 +33,12 @@ const replaceTime = (date?: string) => { return date?.replace('T00:00:00Z', '') ?? null } -export const buildLabelObjectFilter = ( - criterion: LabelObject[] | undefined | null, - hierarchyUrl?: string, - system?: boolean -) => { +export const buildLabelObjectFilter = (criterion: LabelObject[] | undefined | null, system?: boolean) => { if (criterion && criterion.length > 0) { let filter = '' - criterion.find((code) => code.id === '*') - ? (filter = `${hierarchyUrl}|*`) - : (filter = `${criterion - .map((item) => (system && item.system ? `${item.system}|${item.id}` : item.id)) - .reduce(searchReducer)}`) + filter = `${criterion + .map((item) => (system && item.system ? `${item.system}|${item.id}` : item.id)) + .reduce(searchReducer)}` return filter } return '' @@ -52,7 +46,8 @@ export const buildLabelObjectFilter = ( export const unbuildLabelObjectFilter = (currentCriterion: any, filterName: string, values?: string | null) => { const valuesIds = values?.split(',') || [] - const newArray = valuesIds?.map((value) => (value.includes('|*') ? { id: '*' } : { id: value })) + + const newArray = valuesIds?.map((value) => (/*value.includes('|*') ? { id: '*' } : */{ id: value })) if (newArray) { currentCriterion[filterName] = currentCriterion ? [...currentCriterion[filterName], ...newArray] : newArray } diff --git a/src/utils/pmsi.ts b/src/utils/pmsi.ts deleted file mode 100644 index 96e814a62..000000000 --- a/src/utils/pmsi.ts +++ /dev/null @@ -1,564 +0,0 @@ -import { expandPmsiElement } from 'state/pmsi' -import { AsyncThunk } from '@reduxjs/toolkit' -import { AppDispatch, RootState } from '../state' -import { - decrementLoadingSyncHierarchyTable, - incrementLoadingSyncHierarchyTable, - initSyncHierarchyTable, - pushSyncHierarchyTable -} from '../state/syncHierarchyTable' -import { expandMedicationElement } from '../state/medication' -import { expandBiologyElement } from '../state/biology' -import services from 'services/aphp' -import { CommonCriteriaDataType, CriteriaType, SelectedCriteriaType } from 'types/requestCriterias' -import { Condition } from 'fhir/r4' -import { Hierarchy } from 'types/hierarchy' -import { LabelObject } from 'types/searchCriterias' - -/** - * @description : get the last diagnosis labels - * @param mainDiagnosisList : the main diagnosis list - * @returns : the last three diagnosis labels - */ -export const getLastDiagnosisLabels = (mainDiagnosisList: Condition[]) => { - const mainDiagnosisLabels = mainDiagnosisList.map((diagnosis) => diagnosis.code?.coding?.[0].display) - const lastThreeDiagnosisLabels = mainDiagnosisLabels - .filter((diagnosis, index) => mainDiagnosisLabels.indexOf(diagnosis) === index) - .slice(0, 3) - .join(' - ') - - return lastThreeDiagnosisLabels -} - -/** - * This function is called when a user select an element of pmsi hierarchy - * return selected items that should be saved - */ -export const getHierarchySelection = (row: any, selectedItems: any[] | undefined, rootRows: any[]): any[] => { - selectedItems = (selectedItems ?? []).map( - (selectedItem) => findEquivalentRowInItemAndSubItems(selectedItem, rootRows).equivalentRow ?? selectedItem - ) - const { flattedSelectedItems } = flatItems(selectedItems) - let savedSelectedItems = flattedSelectedItems - const isFound = savedSelectedItems?.find((item) => item.id === row.id) - - const getRowAndChildren = (parent: any): any[] => { - const getChild: (subItem: any) => any[] = (subItem: any) => { - if (subItem?.id === 'loading') return [] - - return [subItem, ...(subItem.subItems ? subItem.subItems.map((subItem: any) => getChild(subItem)) : [])].flat() - } - - return [ - parent, - ...(parent.subItems - ? parent.id === 'loading' - ? [] - : parent.subItems.map((subItem: any) => getChild(subItem)) - : []) - ].flat() - } - - const deleteRowAndChildren = (parent: any): any[] => { - const elemToDelete = getRowAndChildren(parent) - - savedSelectedItems = savedSelectedItems.filter((row) => !elemToDelete.some(({ id }) => id === row.id)) - - return savedSelectedItems - } - - if (isFound) { - savedSelectedItems = deleteRowAndChildren(row) - } else { - savedSelectedItems = [...savedSelectedItems, ...getRowAndChildren(row)] - } - - let _savedSelectedItems: any[] = [] - const checkIfParentIsChecked = (rows: any[]): void => { - for (let index = 0; index < rows.length; index++) { - const row = rows[index] - if ( - !row.subItems || - (row && row.subItems.length === 0) || - (row && row.subItems.length === 1 && row.subItems[0].id === 'loading') - ) { - continue - } - - const selectedChildren = row.subItems - ? // eslint-disable-next-line - row.subItems.filter((child: { id: any }) => - savedSelectedItems.find((selectedChild) => selectedChild.id === child.id) - ) - : [] - const foundItem = savedSelectedItems.find(({ id }) => id === row.id) - - if (row && row.subItems && selectedChildren.length === row.subItems.length && !foundItem) { - savedSelectedItems = [...savedSelectedItems, row] - } else if ( - row && - row.subItems && - row.subItems.length > 0 && - selectedChildren && - selectedChildren.length !== row.subItems.length - ) { - savedSelectedItems = savedSelectedItems.filter(({ id }) => id !== row.id) - } - - // Need a real fix .. 🥲 - // // Protection: - // // When the user select a scope, reload, select and unselect a subitems the parent have subitems = [] - // // So, replace the parent and the condition `selectedChildren.length !== foundItem.subItems.length` can be validated - - const indexOfItem = foundItem && savedSelectedItems ? savedSelectedItems.indexOf(foundItem) : -1 - if (indexOfItem !== -1) { - savedSelectedItems[indexOfItem] = row - } - - if (row.subItems) checkIfParentIsChecked(row.subItems) - } - } - - while (savedSelectedItems.length !== _savedSelectedItems.length) { - _savedSelectedItems = savedSelectedItems - checkIfParentIsChecked(rootRows) - } - - savedSelectedItems = savedSelectedItems.filter((item, index, array) => array.indexOf(item) === index) - - return savedSelectedItems -} - -export const optimizeHierarchySelection = < - T extends { - id: string - subItems?: Hierarchy[] - } ->( - selectedItems: T[], - rootRows: T[] -): T[] => { - // If you chenge this code, change it too inside: PopulationCard.tsx:31 and Scope.jsx:25 - selectedItems = selectedItems.map( - (selectedItem) => findEquivalentRowInItemAndSubItems(selectedItem, rootRows).equivalentRow ?? selectedItem - ) - const updatedRows: any[] = [] - const newSelectedItems = selectedItems.filter((item, index, array) => { - // reemove double item - const foundItem = array.find(({ id }) => item.id === id) - const currentIndex = foundItem ? array.indexOf(foundItem) : -1 - if (index !== currentIndex) return false - - if (!item.subItems || (item.subItems && item.subItems.length === 1 && item.subItems[0].id === 'loading')) { - if (item && item.subItems && !(item.subItems.length === 1 && item.subItems[0].id === 'loading')) { - updatedRows.push({ ...item }) - } - } - const returnParentElement: (_array: any[], parentArray: any | undefined) => any | undefined = ( - _array, - parentArray - ) => { - let parentElement: any | undefined = undefined - if (!_array) return - for (const element of _array) { - if (parentElement) continue - - if (element.id === item.id) { - parentElement = parentArray - } - - if ( - !parentElement && - element && - element.subItems && - element.subItems.length > 0 && - element.subItems[0].id !== 'loading' - ) { - parentElement = returnParentElement(element.subItems, element) - } - } - return parentElement - } - - const parentItem: any = returnParentElement(rootRows, undefined) - - if (parentItem !== undefined) { - const selectedChildren = - parentItem.subItems && parentItem.subItems.length > 0 - ? parentItem.subItems.filter((subItem: { id: string }) => !!array.find(({ id }) => id === subItem.id)) - : [] - - if ( - selectedChildren.length === (parentItem.subItems && parentItem.subItems.length) && - selectedItems.find((item) => item.id === parentItem.id) - ) { - // Si item + TOUS LES AUTRES child sont select. => Delete it - return false - } else { - // Sinon => Keep it - return true - } - } else { - if ( - !item.subItems || - (item.subItems && item.subItems.length === 0) || - (item.subItems && item.subItems.length > 0 && item.subItems[0].id === 'loading') - ) { - // Si pas d'enfant, pas de check => Keep it - return true - } - - const selectedChildren = - item.subItems && item.subItems.length > 0 - ? item.subItems.filter((subItem: any) => !!array.find(({ id }) => id === subItem.id)) - : [] - - if (selectedChildren.length === 0 || selectedChildren.length === item.subItems.length) { - // Si tous les enfants sont check => Keep it - return true - } else { - // Sinon => Delete it - return false - } - } - }) - - return newSelectedItems.map((newSelectedItem) => { - const updatedRow = updatedRows.find((updatedRow) => updatedRow.id === newSelectedItem.id) - return updatedRow ?? newSelectedItem - }) -} - -export const checkIfIndeterminated: (_row: any, selectedItems: any) => boolean | undefined = (_row, selectedItems) => { - // Si que un loading => false - if (_row.subItems && _row.subItems.length > 0 && _row.subItems[0].id === 'loading') { - return false - } - const checkChild: (item: any) => boolean = (item) => { - const numberOfSubItemsSelected = selectedItems - ? item.subItems?.filter((subItem: any) => - selectedItems.find((selectedItems: any) => selectedItems.id === subItem.id) - )?.length - : 0 - - if (numberOfSubItemsSelected) { - // Si un des sub elem qui est check => true - return true - } else if (item.subItems?.length >= numberOfSubItemsSelected) { - // Si un des sub-sub (ou sub-sub-sub ...) elem qui est check => true - let isCheck = false - for (const child of item.subItems) { - if (isCheck) continue - isCheck = !!checkChild(child) - } - return isCheck - } else { - // Sinon => false - return false - } - } - return checkChild(_row) -} - -/** - * @description : find the same {item} in the {rows} list. Because {item} may not have sub items but, its equivalent may have sub items already loaded. - * @param item : the item to find (using its id). - * @param rows : the list where the search will be performed. - * @returns : the item the has the same id field value as the one of {item} param. The difference between the two ones is that the found one could have every field already loaded (like sub items field). - */ -export const findEquivalentRowInItemAndSubItems: ( - item: any, - rows: any[] | undefined -) => { equivalentRow: any | undefined; parentsList: any[] } = (item: any, rows: any[] | undefined) => { - let equivalentRow: any | undefined = undefined - const parentsList: any[] = [] - if (!rows) return { equivalentRow: equivalentRow, parentsList: parentsList } - for (const row of rows) { - if (row.id === item.id) { - parentsList.push(row) - equivalentRow = row - break - } else { - const { equivalentRow: lastNode, parentsList: newParentsList } = findEquivalentRowInItemAndSubItems( - item, - row.subItems - ) - if (lastNode) { - equivalentRow = lastNode - parentsList.push(...newParentsList) - break - } - } - } - return { equivalentRow: equivalentRow, parentsList: parentsList } -} - -export const flatItems = (items: any[] | undefined): { flattedSelectedItems: any[]; selectedIds: string[] } => { - const selectedIds: string[] = [] - const flattedSelectedItems: any[] = [] - const flat = (items: any[] | undefined): void => { - if (!items) return - items.forEach((item) => { - if (item.id === 'loading') { - return - } - if (selectedIds?.indexOf(item.id) === -1) { - selectedIds.push(item.id) - flattedSelectedItems?.push(item) - } - if (item.subItems) flat(item.subItems) - }) - } - flat(items) - return { flattedSelectedItems: flattedSelectedItems, selectedIds: selectedIds } -} - -export const initSyncHierarchyTableEffect = async ( - resourceHierarchy: Hierarchy[], - selectedCriteria: SelectedCriteriaType | null, - selectedCodes: Hierarchy[], - fetchResource: AsyncThunk, - resourceType: CriteriaType, - dispatch: AppDispatch, - isFetchedResource?: boolean -): Promise => { - if (!isFetchedResource) { - await dispatch(fetchResource()) - } - if (!selectedCriteria) { - await dispatch(pushSyncHierarchyTable({ code: [] })) - } else { - await dispatch(initSyncHierarchyTable(selectedCriteria)) - } - dispatch(incrementLoadingSyncHierarchyTable()) - resourceHierarchy = await expandHierarchyCodes( - selectedCodes, - selectedCodes, - resourceHierarchy, - resourceType, - dispatch - ) - dispatch(decrementLoadingSyncHierarchyTable()) -} - -export const onChangeSelectedCriteriaEffect = async ( - codesToExpand: Hierarchy[], - selectedCodes: Hierarchy[], - resourceHierarchy: Hierarchy[], - resourceType: CriteriaType, - dispatch: AppDispatch -): Promise => { - await expandHierarchyCodes(codesToExpand, selectedCodes, resourceHierarchy, resourceType, dispatch) - dispatch(pushSyncHierarchyTable({ code: selectedCodes })) -} - -const isExpanded = (itemToExpand: Hierarchy | undefined): boolean => { - if (itemToExpand?.subItems?.length > 0 && itemToExpand?.subItems[0].id !== 'loading') { - return true - } else { - return false - } -} - -const expandRequest = async ( - codeToExpand: string, - selectedCodes: Hierarchy[], - resourceType: CriteriaType, - dispatch: AppDispatch -): Promise[] | undefined> => { - let type: 'claim' | 'condition' | 'procedure' - if (resourceType.toLowerCase() === CriteriaType.MEDICATION_REQUEST.toLowerCase()) { - const expandedMedication = await dispatch( - expandMedicationElement({ - rowId: codeToExpand, - selectedItems: selectedCodes - }) - ).unwrap() - return expandedMedication.list - } else if (resourceType.toLowerCase() === CriteriaType.OBSERVATION.toLowerCase()) { - const expandedBiology = await dispatch( - expandBiologyElement({ - rowId: codeToExpand, - selectedItems: selectedCodes - }) - ).unwrap() - return expandedBiology.list - } else if (resourceType.toLowerCase() === CriteriaType.CLAIM.toLowerCase()) { - type = 'claim' - } else if (resourceType.toLowerCase() === CriteriaType.PROCEDURE.toLowerCase()) { - type = 'procedure' - } else if (resourceType.toLowerCase() === CriteriaType.CONDITION.toLowerCase()) { - type = 'condition' - } else { - return undefined - } - const expandedPmsiElements = await dispatch( - expandPmsiElement({ - keyElement: type, - rowId: codeToExpand, - selectedItems: selectedCodes - }) - ).unwrap() - return expandedPmsiElements[type].list -} - -export const expandItem = async ( - codeToExpand: string, - selectedCodes: Hierarchy[], - resourceHierarchy: Hierarchy[], - resourceType: CriteriaType, - dispatch: AppDispatch -): Promise[]> => { - const equivalentRow = findEquivalentRowInItemAndSubItems( - { id: codeToExpand, label: 'loading' }, - resourceHierarchy - ).equivalentRow - let newResourceHierarchy = undefined - if (isExpanded(equivalentRow)) { - newResourceHierarchy = resourceHierarchy - } else { - newResourceHierarchy = await expandRequest(codeToExpand, selectedCodes, resourceType, dispatch) - } - return newResourceHierarchy || [] -} - -const expandSingleResourceItem = async ( - codeToExpand: Hierarchy, - selectedCodes: Hierarchy[], - resourceHierarchy: Hierarchy[], - resourceType: CriteriaType, - dispatch: AppDispatch -): Promise[]> => { - if ( - !codeToExpand || - (selectedCodes.find((item) => item.id === codeToExpand.id) && - findEquivalentRowInItemAndSubItems(codeToExpand, resourceHierarchy).equivalentRow) - ) - return resourceHierarchy - let newResourceHierarchy: Hierarchy[] = resourceHierarchy - const expandItemAndSubItems = async ( - itemToExpand: Hierarchy, - resourceType: CriteriaType - ): Promise[]> => { - newResourceHierarchy = await expandItem( - itemToExpand?.id, - selectedCodes, - newResourceHierarchy, - resourceType, - dispatch - ) - - if (!selectedCodes.find((selectedItem) => selectedItem.id === itemToExpand.id)) { - if (!isExpanded(itemToExpand)) { - const updatedItem = - findEquivalentRowInItemAndSubItems(itemToExpand, newResourceHierarchy).equivalentRow ?? itemToExpand - itemToExpand = updatedItem ?? itemToExpand - } - const subItems = - itemToExpand.subItems?.filter((item: Hierarchy) => parentsList.find((code) => code.id === item.id)) ?? - [] - for await (const item of subItems) { - newResourceHierarchy = await expandItemAndSubItems(item, resourceType) - } - } - return newResourceHierarchy - } - const getHigherParentFromList = (parentsList: Hierarchy[]): Hierarchy | undefined => { - const higherParentCode: Hierarchy | undefined = newResourceHierarchy?.[0].subItems - ? newResourceHierarchy[0].subItems.find(({ id }: Hierarchy) => - parentsList.find((code) => code.id === id) - ) - : undefined - return higherParentCode - } - - const getHigherParent = async ( - code: Hierarchy - ): Promise<{ higherParentCode: Hierarchy | undefined; parentsList: any[] }> => { - const { parentsList: parentsListByAlreadyFetched } = findEquivalentRowInItemAndSubItems( - codeToExpand, - newResourceHierarchy - ) - let higherParentCode: Hierarchy | undefined = getHigherParentFromList(parentsListByAlreadyFetched) - if (higherParentCode) { - return { higherParentCode: higherParentCode, parentsList: parentsListByAlreadyFetched } - } else if (!higherParentCode) { - const response = await services.cohortCreation.fetchSingleCodeHierarchy(resourceType, code.id) - const parentsListByFetch = response - ? response.map((item) => { - return { id: item, label: 'loading', subItems: [] } - }) - : [] - higherParentCode = getHigherParentFromList(parentsListByFetch) - - return { higherParentCode: higherParentCode, parentsList: parentsListByFetch } - } - return higherParentCode - } - const { higherParentCode, parentsList } = await getHigherParent(codeToExpand) - if (!higherParentCode) return newResourceHierarchy - - await expandItemAndSubItems(higherParentCode, resourceType) - return newResourceHierarchy -} -const expandHierarchyCodes = async ( - codesToExpand: Hierarchy[], - selectedCodes: Hierarchy[], - resourceHierarchy: Hierarchy[], - resourceType: CriteriaType, - dispatch: AppDispatch -): Promise[]> => { - let newResourceHierarchy = resourceHierarchy - for await (const itemToExpand of codesToExpand) { - newResourceHierarchy = await expandSingleResourceItem( - itemToExpand, - selectedCodes, - newResourceHierarchy, - resourceType, - dispatch - ) - } - resourceHierarchy = newResourceHierarchy - return resourceHierarchy -} -export const syncOnChangeFormValue = async ( - key: string, - value: any, - resourceHierarchy: Hierarchy[], - setDefaultCriteria: (value: React.SetStateAction) => void, - selectedTab: string, - resourceType: CriteriaType, - dispatch: AppDispatch -): Promise => { - setDefaultCriteria((selectedCriteria) => { - const newSelectedCriteria: any = selectedCriteria ? { ...selectedCriteria } : {} - newSelectedCriteria[key] = value - if (key === 'code') { - const optimizedHierarchySelection = optimizeHierarchySelection(newSelectedCriteria.code, resourceHierarchy) - newSelectedCriteria[key] = optimizedHierarchySelection - dispatch(pushSyncHierarchyTable({ code: optimizedHierarchySelection })) - if (selectedTab === 'form') { - if ( - selectedCriteria.type !== 'IPPList' && - selectedCriteria.type !== 'DocumentReference' && - selectedCriteria.type !== 'Encounter' && - selectedCriteria.type !== 'Patient' && - selectedCriteria.type !== 'ImagingStudy' && - selectedCriteria.type !== 'Pregnancy' && - selectedCriteria.type !== 'Hospit' - ) { - expandHierarchyCodes( - optimizedHierarchySelection, - // because the function is caca anyways - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (selectedCriteria as any).code?.map((code: LabelObject) => ({ ...code, subItems: [] })) ?? [], - resourceHierarchy, - resourceType, - dispatch - ) - } - } - } - return newSelectedCriteria - }) -} diff --git a/src/utils/requestCriterias.tsx b/src/utils/requestCriterias.tsx index 7ab676a40..1da4d2e37 100644 --- a/src/utils/requestCriterias.tsx +++ b/src/utils/requestCriterias.tsx @@ -17,7 +17,7 @@ import { } from 'types/searchCriterias' import allDocTypes from 'assets/docTypes.json' import { getDurationRangeLabel } from './age' -import { displaySystem } from './displayValueSetSystem' +import { getFullLabelFromCode } from './valueSets' import { CriteriaState } from 'state/criteria' import { Tooltip, Typography } from '@mui/material' import { Hierarchy } from 'types/hierarchy' @@ -58,8 +58,9 @@ const getLabelFromCriteriaObject = ( ) => { const criterionData = criteriaState.cache.find((criteriaCache) => criteriaCache.criteriaType === resourceType)?.data if (criterionData === null || values === null) return '' - const criterion = criterionData?.[name] || [] + console.log('test criterion cache', criteriaState) + console.log('test criterion codes', values) if (criterion !== 'loading') { // eslint-disable-next-line @typescript-eslint/no-explicit-any const removeDuplicates = (array: any[], key: string) => { @@ -67,8 +68,9 @@ const getLabelFromCriteriaObject = ( } const labels = removeDuplicates(criterion, 'id') .filter((obj) => values.map((value) => value.id).includes(obj.id)) - .map((obj: LabelObject) => `${displaySystem(obj.system)} ${obj.label}`) + .map((obj: LabelObject) => getFullLabelFromCode(obj)) + /*const labels = values.map((obj: LabelObject) => displaySystem(obj))*/ const tooltipTitle = labels.join(' - ') return ( diff --git a/src/utils/string.ts b/src/utils/string.ts new file mode 100644 index 000000000..d4b05311d --- /dev/null +++ b/src/utils/string.ts @@ -0,0 +1,4 @@ +export const capitalizeFirstLetter = (str: string) => { + if (!str) return str + return str.charAt(0).toUpperCase() + str.slice(1) +} diff --git a/src/utils/valueSets.ts b/src/utils/valueSets.ts new file mode 100644 index 000000000..6403a7fc7 --- /dev/null +++ b/src/utils/valueSets.ts @@ -0,0 +1,41 @@ +import { getConfig } from 'config' +import { getReferences } from 'data/valueSets' +import { HIERARCHY_ROOT } from 'services/aphp/serviceValueSets' +import { Hierarchy } from 'types/hierarchy' +import { LabelObject } from 'types/searchCriterias' + +export const getValueSetsFromSystems = (systems: string[]) => { + return getReferences(getConfig()).filter((reference) => systems.includes(reference.url)) +} + +export const isDisplayedWithCode = (system: string) => { + const isFound = getValueSetsFromSystems([system])?.[0] + if (isFound && isFound.joinDisplayWithCode) return true + return false +} + +export const isDisplayedWithSystem = (system: string) => { + const isFound = getValueSetsFromSystems([system])?.[0] + if (isFound && isFound.joinDisplayWithSystem) return true + return false +} + +export const getLabelFromCode = (code: Hierarchy) => { + if (isDisplayedWithCode(code.system) && code.id !== HIERARCHY_ROOT) return `${code.id} - ${code.label}` + return code.label +} + +export const getFullLabelFromCode = (code: LabelObject) => { + let label = '' + if (code.system) { + if (isDisplayedWithSystem(code.system)) label+= `${getLabelFromSystem(code.system)} - ` + if (isDisplayedWithCode(code.system) && code.id !== HIERARCHY_ROOT) label += `${code.id} - ` + } + label += code.label + return label +} + +export const getLabelFromSystem = (system: string) => { + const isFound = getValueSetsFromSystems([system])?.[0] + return isFound?.label || '' +} diff --git a/src/views/Dashboard/Dashboard.tsx b/src/views/Dashboard/index.tsx similarity index 98% rename from src/views/Dashboard/Dashboard.tsx rename to src/views/Dashboard/index.tsx index 47cf0d94d..7b439cac6 100644 --- a/src/views/Dashboard/Dashboard.tsx +++ b/src/views/Dashboard/index.tsx @@ -3,8 +3,8 @@ import { Link, useParams, useLocation } from 'react-router-dom' import { Grid, Tabs, Tab } from '@mui/material' import CohortPreview from 'components/Dashboard/Preview/Preview' -import PatientList from 'components/Dashboard/PatientList/PatientList' -import Documents from 'components/Dashboard/Documents/Documents' +import PatientList from 'components/Dashboard/PatientList' +import Documents from 'components/Dashboard/Documents' import TopBar from 'components/TopBar/TopBar' import CohortCreation from 'views/CohortCreation/CohortCreation' diff --git a/src/views/Patient/Patient.tsx b/src/views/Patient/index.tsx similarity index 97% rename from src/views/Patient/Patient.tsx rename to src/views/Patient/index.tsx index 89586e42d..4f9aced9e 100644 --- a/src/views/Patient/Patient.tsx +++ b/src/views/Patient/index.tsx @@ -8,10 +8,10 @@ import PatientHeader from 'components/Patient/PatientHeader/PatientHeader' import PatientPreview from 'components/Patient/PatientPreview/PatientPreview' import PatientSidebar from 'components/Patient/PatientSidebar/PatientSidebar' import PatientTimeline from 'components/Patient/PatientTimeline/PatientTimeline' -import PatientMedication from 'components/Patient/PatientMedication/PatientMedication' +import PatientMedication from 'components/Patient/PatientMedication' import PatientBiology from 'components/Patient/PatientBiology/PatientBiology' -import PatientImaging from 'components/Patient/PatientImaging/PatientImaging' -import PatientPMSI from 'components/Patient/PatientPMSI/PatientPMSI' +import PatientImaging from 'components/Patient/PatientImaging' +import PatientPMSI from 'components/Patient/PatientPMSI' import PatientForms from 'components/Patient/PatientForms' import TopBar from 'components/TopBar/TopBar' import useStyles from './styles' diff --git a/src/views/Welcome/Welcome.tsx b/src/views/Welcome/Welcome.tsx index 6c3920259..d16fa0202 100644 --- a/src/views/Welcome/Welcome.tsx +++ b/src/views/Welcome/Welcome.tsx @@ -16,9 +16,6 @@ import { useAppDispatch, useAppSelector } from 'state' import { fetchCohorts } from 'state/cohort' import { fetchProjects } from 'state/project' import { fetchRequests } from 'state/request' -import { initPmsiHierarchy } from 'state/pmsi' -import { initMedicationHierarchy } from 'state/medication' -import { initBiologyHierarchy } from 'state/biology' import { AccessExpiration, RequestType } from 'types' import useStyles from './styles' import { CohortsType } from 'types/cohorts' @@ -76,9 +73,6 @@ const Welcome = () => { useEffect(() => { dispatch(fetchProjects()) dispatch(fetchRequests()) - dispatch(initPmsiHierarchy()) - dispatch(initMedicationHierarchy()) - dispatch(initBiologyHierarchy()) fetchCohortsPreview() }, [])