From 17e206fde1a497c8fe65762b8be55de23f8f1794 Mon Sep 17 00:00:00 2001 From: aetchego <34280393+aetchego@users.noreply.github.com> Date: Thu, 9 Jan 2025 14:11:39 +0100 Subject: [PATCH] feat(request): new code system field input - Ref gestion-de-projet#2371 (#1064) feat(request): new code system field input - Ref gestion-de-projet#2371 (#1064) --- .gitignore | 3 +- package.json | 5 +- .../cohortCreation/cohortCreation.test.ts | 12 +- .../data/cohortCreation/claimCriteria.ts | 20 +- .../data/cohortCreation/cohortCreation.ts | 5 +- .../data/cohortCreation/conditionCriteria.ts | 13 +- .../data/cohortCreation/documentCriteria.ts | 5 +- .../data/cohortCreation/encounterCriteria.ts | 5 +- .../data/cohortCreation/medicationCriteria.ts | 49 +- .../cohortCreation/observationCriteria.ts | 11 +- .../data/cohortCreation/procedureCriteria.ts | 14 +- src/__tests__/utilsFunction/hierarchy.test.ts | 594 ++++++++++++++++++ .../ControlPanel/ControlPanel.tsx | 2 +- .../CreationCohort/DataList_Criteria.tsx | 244 ++++--- .../AdvancedInputs/AdvancedInputs.tsx | 140 ----- .../AdvancedInputs/index.tsx | 142 +++++ .../BiologySearch/BiologySearch.tsx | 269 -------- .../components/BiologySearch/styles.ts | 88 --- .../components/Form/BiologyForm.tsx | 403 ------------ .../BiologyForm/components/Form/styles.ts | 53 -- .../components/Hierarchy/BiologyHierarchy.tsx | 241 ------- .../components/Hierarchy/styles.ts | 102 --- .../CriteriaRightPanel/BiologyForm/index.tsx | 401 +++++++++--- .../CriteriaRightPanel/BiologyForm/styles.ts | 11 - .../CCAM/components/Form/styles.ts | 53 -- .../components/Hierarchy/CCAMHierarchy.tsx | 240 ------- .../CCAM/components/Hierarchy/styles.ts | 102 --- .../CriteriaRightPanel/CCAM/index.tsx | 112 ---- .../CriteriaRightPanel/CCAM/styles.ts | 11 - .../Form/CCAMForm.tsx => CcamForm/index.tsx} | 173 +++-- .../Cim10Form/components/Form/Cim10Form.tsx | 261 -------- .../Cim10Form/components/Form/styles.ts | 53 -- .../components/Hierarchy/Cim10Hierarchy.tsx | 244 ------- .../Cim10Form/components/Hierarchy/styles.ts | 101 --- .../CriteriaRightPanel/Cim10Form/index.tsx | 299 ++++++--- .../CriteriaRightPanel/Cim10Form/styles.ts | 10 - .../DemographicForm/index.tsx | 18 +- .../{DocumentsForm.tsx => index.tsx} | 13 +- .../EncounterForm/index.tsx | 10 +- .../GHM/components/Hierarchy/GhmHierarchy.tsx | 231 ------- .../GHM/components/Hierarchy/styles.ts | 102 --- .../CriteriaRightPanel/GHM/index.tsx | 102 --- .../CriteriaRightPanel/GHM/styles.ts | 10 - .../Form/GhmForm.tsx => GHMForm/index.tsx} | 151 ++--- .../CriteriaRightPanel/HospitForm/index.tsx | 8 +- .../CriteriaRightPanel/ImagingForm/index.tsx | 4 +- .../components/Form/MedicationForm.tsx | 287 --------- .../MedicationForm/components/Form/styles.ts | 53 -- .../Hierarchy/MedicationHierarchy.tsx | 323 ---------- .../components/Hierarchy/styles.ts | 116 ---- .../MedicationForm/index.tsx | 318 +++++++--- .../MedicationForm/styles.ts | 10 - .../CriteriaRightPanel/PregnantForm/index.tsx | 8 +- .../Form/styles.ts => formStyles.ts} | 4 +- .../{LogicalOperator.tsx => index.tsx} | 0 .../PopulationCard/PopulationCard.tsx | 4 +- .../components/PopulationRightPanel.tsx | 81 --- .../{DiagramView.tsx => index.tsx} | 33 +- src/components/CreationCohort/Requeteur.tsx | 3 +- .../Dashboard/BiologyList/index.tsx | 63 +- .../Documents/{Documents.tsx => index.tsx} | 39 +- .../Dashboard/ExportModal/exportUtils.ts | 49 +- src/components/Dashboard/FormsList/index.tsx | 12 +- .../Dashboard/ImagingList/index.tsx | 19 +- .../Dashboard/MedicationList/index.tsx | 169 ++--- src/components/Dashboard/PMSIList/index.tsx | 41 +- .../{PatientList.tsx => index.tsx} | 32 +- src/components/DataTable/DataTablePatient.tsx | 2 +- .../AdministrationTypesFilter/index.tsx | 3 +- src/components/Filters/AnabioFilter/index.tsx | 41 -- src/components/Filters/CodeFilter/index.tsx | 27 +- .../Filters/DiagnosticTypesFilter/index.tsx | 3 +- .../Filters/ExecutiveUnitsFilter/index.tsx | 6 +- src/components/Filters/LoincFilter/index.tsx | 41 -- .../Filters/PrescriptionTypesFilter/index.tsx | 3 +- src/components/Hierarchy/CodesWithSystems.tsx | 54 ++ src/components/Hierarchy/SelectedCodes.tsx | 62 ++ src/components/Hierarchy/styles.ts | 33 + .../{PatientBiology.tsx => index.tsx} | 81 ++- .../{PatientDocs.tsx => index.tsx} | 47 +- .../PatientForms/MaternityForms/Timeline.tsx | 35 +- .../PatientForms/MaternityForms/index.tsx | 11 +- .../{PatientImaging.tsx => index.tsx} | 45 +- .../{PatientMedication.tsx => index.tsx} | 186 +++--- .../{PatientPMSI.tsx => index.tsx} | 68 +- .../Patient/PatientPreview/PatientPreview.tsx | 2 +- .../FilterTimelineDialog.tsx | 6 +- .../PatientTimeline/PatientTimeline.tsx | 20 +- .../Routes/AppNavigation/config.tsx | 4 +- src/components/ScopeTree/ScopeTreeTable.tsx | 261 ++++---- src/components/ScopeTree/SelectedCodes.tsx | 59 -- src/components/ScopeTree/index.tsx | 139 ++-- src/components/ScopeTree/styles.ts | 35 -- src/components/SearchValueSet/References.tsx | 59 ++ .../SearchValueSet/ValueSetField.tsx | 74 +++ .../SearchValueSet/ValueSetTable.tsx | 227 +++++++ src/components/SearchValueSet/index.tsx | 128 ++++ src/components/ui/Chip/Chips.tsx | 35 -- src/components/ui/Chip/index.tsx | 2 +- src/components/ui/Displayer/styles.ts | 9 + .../ui/Inputs/AsyncAutocomplete/index.tsx | 106 ---- .../ui/Inputs/ExecutiveUnit/index.tsx | 109 ---- .../ui/Inputs/ExecutiveUnit/styles.tsx | 12 - .../ui/Inputs/ExecutiveUnits/index.tsx | 123 ++++ src/components/ui/Layout/index.tsx | 7 - src/components/ui/List/index.tsx | 2 +- src/components/ui/Pagination/index.tsx | 29 +- src/components/ui/Pagination/styles.ts | 24 +- src/components/ui/Panel/index.tsx | 70 +++ src/components/ui/Searchbar/SearchInput.tsx | 3 + src/components/ui/Tabs/index.tsx | 39 +- src/components/ui/Tabs/styles.ts | 42 +- src/config.tsx | 2 +- src/constants.js | 2 - src/data/valueSets.ts | 99 +++ src/hooks/hierarchy/useHierarchy.ts | 221 ++++--- src/hooks/scopeTree/useScopeTree.ts | 114 ++++ src/hooks/{ => search}/useSearchParameters.ts | 43 +- src/hooks/useDebounceAction.ts | 28 + src/hooks/useFetch.ts | 45 -- src/hooks/useIsOverflow.ts | 33 + src/hooks/valueSet/useSearchValueSet.ts | 188 ++++++ src/mappers/filters.ts | 172 ++--- src/mappers/pmsi.ts | 21 +- src/reducers/searchCriteriasReducer.ts | 6 +- src/services/aphp/callApi.ts | 176 +----- .../aphp/cohortCreation/fetchObservation.ts | 84 --- src/services/aphp/serviceBiology.ts | 24 - src/services/aphp/serviceCohortCreation.ts | 443 +------------ src/services/aphp/serviceCohorts.ts | 21 +- src/services/aphp/servicePatients.ts | 24 +- src/services/aphp/servicePerimeters.ts | 42 +- src/services/aphp/servicePmsi.ts | 34 - src/services/aphp/serviceValueSets.ts | 275 ++++++++ src/state/biology.ts | 150 ----- src/state/cohortCreation.ts | 4 +- src/state/medication.ts | 168 ----- src/state/message.ts | 47 -- src/state/patient.ts | 21 +- src/state/pmsi.ts | 286 --------- src/state/scope.ts | 52 +- src/state/store.ts | 8 - src/state/syncHierarchyTable.ts | 84 --- src/state/valueSets.ts | 27 +- src/types.ts | 17 +- src/types/hierarchy.ts | 57 +- src/types/patient.ts | 2 +- src/types/requestCriterias.ts | 21 +- src/types/scope.ts | 4 + src/types/searchCriterias.ts | 43 +- src/types/valueSet.ts | 44 ++ src/utils/alphabeticalSort.ts | 46 -- src/utils/arrays.ts | 6 +- src/utils/cohortCreation.ts | 104 +-- src/utils/displayValueSetSystem.ts | 12 - src/utils/filters.ts | 17 +- src/utils/hierarchy.ts | 277 ++++++-- src/utils/map.ts | 5 + src/utils/mappers.ts | 72 ++- src/utils/pmsi.ts | 564 ----------------- src/utils/requestCriterias.tsx | 37 +- src/utils/string.ts | 4 + src/utils/tabsUtils.ts | 4 +- src/utils/valueSets.ts | 39 ++ .../Dashboard/{Dashboard.tsx => index.tsx} | 4 +- src/views/Patient/{Patient.tsx => index.tsx} | 10 +- src/views/Scope/CareSiteView.tsx | 39 +- src/views/Scope/styles.js | 3 +- src/views/Welcome/Welcome.tsx | 6 - 169 files changed, 5207 insertions(+), 8352 deletions(-) create mode 100644 src/__tests__/utilsFunction/hierarchy.test.ts delete mode 100644 src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/AdvancedInputs/AdvancedInputs.tsx create mode 100644 src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/AdvancedInputs/index.tsx delete mode 100644 src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/BiologyForm/components/BiologySearch/BiologySearch.tsx delete mode 100644 src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/BiologyForm/components/BiologySearch/styles.ts delete mode 100644 src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/BiologyForm/components/Form/BiologyForm.tsx delete mode 100644 src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/BiologyForm/components/Form/styles.ts delete mode 100644 src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/BiologyForm/components/Hierarchy/BiologyHierarchy.tsx delete mode 100644 src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/BiologyForm/components/Hierarchy/styles.ts delete mode 100644 src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/BiologyForm/styles.ts delete mode 100644 src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/CCAM/components/Form/styles.ts delete mode 100644 src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/CCAM/components/Hierarchy/CCAMHierarchy.tsx delete mode 100644 src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/CCAM/components/Hierarchy/styles.ts delete mode 100644 src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/CCAM/index.tsx delete mode 100644 src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/CCAM/styles.ts rename src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/{CCAM/components/Form/CCAMForm.tsx => CcamForm/index.tsx} (56%) delete mode 100644 src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/Cim10Form/components/Form/Cim10Form.tsx delete mode 100644 src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/Cim10Form/components/Form/styles.ts delete mode 100644 src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/Cim10Form/components/Hierarchy/Cim10Hierarchy.tsx delete mode 100644 src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/Cim10Form/components/Hierarchy/styles.ts delete mode 100644 src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/Cim10Form/styles.ts rename src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/DocumentsForm/{DocumentsForm.tsx => index.tsx} (96%) delete mode 100644 src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/GHM/components/Hierarchy/GhmHierarchy.tsx delete mode 100644 src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/GHM/components/Hierarchy/styles.ts delete mode 100644 src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/GHM/index.tsx delete mode 100644 src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/GHM/styles.ts rename src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/{GHM/components/Form/GhmForm.tsx => GHMForm/index.tsx} (53%) delete mode 100644 src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/MedicationForm/components/Form/MedicationForm.tsx delete mode 100644 src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/MedicationForm/components/Form/styles.ts delete mode 100644 src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/MedicationForm/components/Hierarchy/MedicationHierarchy.tsx delete mode 100644 src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/MedicationForm/components/Hierarchy/styles.ts delete mode 100644 src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/MedicationForm/styles.ts rename src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/{GHM/components/Form/styles.ts => formStyles.ts} (94%) rename src/components/CreationCohort/DiagramView/components/LogicalOperator/{LogicalOperator.tsx => index.tsx} (100%) delete mode 100644 src/components/CreationCohort/DiagramView/components/PopulationCard/components/PopulationRightPanel.tsx rename src/components/CreationCohort/DiagramView/{DiagramView.tsx => index.tsx} (85%) rename src/components/Dashboard/Documents/{Documents.tsx => index.tsx} (95%) rename src/components/Dashboard/PatientList/{PatientList.tsx => index.tsx} (96%) delete mode 100644 src/components/Filters/AnabioFilter/index.tsx delete mode 100644 src/components/Filters/LoincFilter/index.tsx create mode 100644 src/components/Hierarchy/CodesWithSystems.tsx create mode 100644 src/components/Hierarchy/SelectedCodes.tsx create mode 100644 src/components/Hierarchy/styles.ts rename src/components/Patient/PatientBiology/{PatientBiology.tsx => index.tsx} (86%) rename src/components/Patient/PatientDocs/{PatientDocs.tsx => index.tsx} (94%) rename src/components/Patient/PatientImaging/{PatientImaging.tsx => index.tsx} (92%) rename src/components/Patient/PatientMedication/{PatientMedication.tsx => index.tsx} (78%) rename src/components/Patient/PatientPMSI/{PatientPMSI.tsx => index.tsx} (91%) delete mode 100644 src/components/ScopeTree/SelectedCodes.tsx delete mode 100644 src/components/ScopeTree/styles.ts create mode 100644 src/components/SearchValueSet/References.tsx create mode 100644 src/components/SearchValueSet/ValueSetField.tsx create mode 100644 src/components/SearchValueSet/ValueSetTable.tsx create mode 100644 src/components/SearchValueSet/index.tsx delete mode 100644 src/components/ui/Chip/Chips.tsx create mode 100644 src/components/ui/Displayer/styles.ts delete mode 100644 src/components/ui/Inputs/AsyncAutocomplete/index.tsx delete mode 100644 src/components/ui/Inputs/ExecutiveUnit/index.tsx delete mode 100644 src/components/ui/Inputs/ExecutiveUnit/styles.tsx create mode 100644 src/components/ui/Inputs/ExecutiveUnits/index.tsx create mode 100644 src/components/ui/Panel/index.tsx create mode 100644 src/data/valueSets.ts create mode 100644 src/hooks/scopeTree/useScopeTree.ts rename src/hooks/{ => search}/useSearchParameters.ts (50%) create mode 100644 src/hooks/useDebounceAction.ts delete mode 100644 src/hooks/useFetch.ts create mode 100644 src/hooks/useIsOverflow.ts create mode 100644 src/hooks/valueSet/useSearchValueSet.ts delete mode 100644 src/services/aphp/cohortCreation/fetchObservation.ts delete mode 100644 src/services/aphp/serviceBiology.ts delete mode 100644 src/services/aphp/servicePmsi.ts create mode 100644 src/services/aphp/serviceValueSets.ts delete mode 100644 src/state/biology.ts delete mode 100644 src/state/medication.ts delete mode 100644 src/state/pmsi.ts delete mode 100644 src/state/syncHierarchyTable.ts create mode 100644 src/types/valueSet.ts delete mode 100644 src/utils/alphabeticalSort.ts delete mode 100644 src/utils/displayValueSetSystem.ts create mode 100644 src/utils/map.ts delete mode 100644 src/utils/pmsi.ts create mode 100644 src/utils/string.ts create mode 100644 src/utils/valueSets.ts rename src/views/Dashboard/{Dashboard.tsx => index.tsx} (98%) rename src/views/Patient/{Patient.tsx => index.tsx} (95%) 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 4eb77ac53..0ecaa51a4 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__/cohortCreation/cohortCreation.test.ts b/src/__tests__/cohortCreation/cohortCreation.test.ts index 5e3908d16..80472e364 100644 --- a/src/__tests__/cohortCreation/cohortCreation.test.ts +++ b/src/__tests__/cohortCreation/cohortCreation.test.ts @@ -48,9 +48,7 @@ import { buildClaimFilter, buildMedicationFilter, buildObservationFilter, - buildImagingFilter, - buildPregnancyFilter, - buildHospitFilter + buildImagingFilter } from 'utils/cohortCreation' import { completeDocumentCriteria, defaultDocumentCriteria } from '__tests__/data/cohortCreation/documentCriteria' import { completeConditionCriteria, defaultConditionCriteria } from '__tests__/data/cohortCreation/conditionCriteria' @@ -301,7 +299,7 @@ describe('test of buildConditionFilter', () => { const selectedCriteria: Cim10DataType = completeConditionCriteria const result = [ 'subject.active=true', - 'code=I841,I842', + 'code=https://smt.esante.gouv.fr/terminologie-cim-10/|I841,https://smt.esante.gouv.fr/terminologie-cim-10/|I842', 'orbis-status=fp,f', '_source=AREM', 'encounter.encounter-care-site=8312016825', @@ -325,7 +323,7 @@ describe('test of buildProcedureFilter', () => { const selectedCriteria: CcamDataType = completeProcedureCriteria const result = [ 'subject.active=true', - 'code=000126,000127', + 'code=https://www.atih.sante.fr/plateformes-de-transmiss…ls/logiciels-espace-de-telechargement/id_lot/3550|000126,https://www.atih.sante.fr/plateformes-de-transmiss…ls/logiciels-espace-de-telechargement/id_lot/3550|000127', 'encounter.encounter-care-site=8312016825', 'encounter.status=entered-in-error', 'date=ge2024-09-06T00:00:00Z', @@ -348,7 +346,7 @@ describe('test of buildClaimFilter', () => { const selectedCriteria: GhmDataType = completeClaimCriteria const result = [ 'patient.active=true', - 'diagnosis=05C021,05C022,05C023,05C024', + 'diagnosis=https://terminology.eds.aphp.fr/aphp-orbis-ghm|05C021,https://terminology.eds.aphp.fr/aphp-orbis-ghm|05C022,https://terminology.eds.aphp.fr/aphp-orbis-ghm|05C023,https://terminology.eds.aphp.fr/aphp-orbis-ghm|05C024', 'encounter.encounter-care-site=8312016825', 'encounter.status=cancelled', 'created=ge2024-09-03T00:00:00Z', @@ -414,7 +412,7 @@ describe('test of buildObservationFilter', () => { const selectedCriteria: ObservationDataType = completeObservationCriteria const result = [ 'subject.active=true&status=Val', - 'code=I3356', + 'code=https://terminology.eds.aphp.fr/aphp-itm-anabio|I3356', 'encounter.encounter-care-site=8312016825', 'encounter.status=cancelled', 'date=ge2024-09-03T00:00:00Z', diff --git a/src/__tests__/data/cohortCreation/claimCriteria.ts b/src/__tests__/data/cohortCreation/claimCriteria.ts index 10eee8303..ff2fe99b4 100644 --- a/src/__tests__/data/cohortCreation/claimCriteria.ts +++ b/src/__tests__/data/cohortCreation/claimCriteria.ts @@ -33,25 +33,33 @@ export const completeClaimCriteria: GhmDataType = { { id: '05C021', label: '05C021 - Chirurgie De Remplacement Valvulaire Avec…thétérisme Cardiaque Ou Coronarographie, Niveau 1', - system: 'https://terminology.eds.aphp.fr/aphp-orbis-ghm' + system: 'https://terminology.eds.aphp.fr/aphp-orbis-ghm', + above_levels_ids: '*', + inferior_levels_ids: '' }, { id: '05C022', label: '05C022 - Chirurgie De Remplacement Valvulaire Avec…thétérisme Cardiaque Ou Coronarographie, Niveau 2', - system: 'https://terminology.eds.aphp.fr/aphp-orbis-ghm' + system: 'https://terminology.eds.aphp.fr/aphp-orbis-ghm', + above_levels_ids: '*', + inferior_levels_ids: '' }, { id: '05C023', label: '05C023 - Chirurgie De Remplacement Valvulaire Avec…thétérisme Cardiaque Ou Coronarographie, Niveau 3', - system: 'https://terminology.eds.aphp.fr/aphp-orbis-ghm' + system: 'https://terminology.eds.aphp.fr/aphp-orbis-ghm', + above_levels_ids: '*', + inferior_levels_ids: '' }, { id: '05C024', label: '05C024 - Chirurgie De Remplacement Valvulaire Avec…thétérisme Cardiaque Ou Coronarographie, Niveau 4', - system: 'https://terminology.eds.aphp.fr/aphp-orbis-ghm' + system: 'https://terminology.eds.aphp.fr/aphp-orbis-ghm', + above_levels_ids: '*', + inferior_levels_ids: '' } ], encounterService: [ @@ -62,12 +70,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..8ac5908d3 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 @@ -86,12 +85,16 @@ export const procedurePeudonimizedCriteria: SelectedCriteriaType[] = [ { id: '000212', label: "000212 - Actes Diagnostiques Sur L'oreille", + above_levels_ids: '*', + inferior_levels_ids: '', system: 'https://www.atih.sante.fr/plateformes-de-transmission-et-logiciels/logiciels-espace-de-telechargement/id_lot/3550' }, { id: '000489', label: '000489 - Actes Thérapeutiques Sur Les Vaisseaux Intracrâniens', + above_levels_ids: '*', + inferior_levels_ids: '', system: 'https://www.atih.sante.fr/plateformes-de-transmission-et-logiciels/logiciels-espace-de-telechargement/id_lot/3550' } diff --git a/src/__tests__/data/cohortCreation/conditionCriteria.ts b/src/__tests__/data/cohortCreation/conditionCriteria.ts index 56f6cb7e8..4596b8cad 100644 --- a/src/__tests__/data/cohortCreation/conditionCriteria.ts +++ b/src/__tests__/data/cohortCreation/conditionCriteria.ts @@ -1,4 +1,5 @@ import { Cim10DataType, Comparators, CriteriaType } from 'types/requestCriterias' +import { System } from 'types/scope' export const defaultConditionCriteria: Cim10DataType = { id: 1, @@ -34,12 +35,16 @@ export const completeConditionCriteria: Cim10DataType = { { id: 'I841', label: 'I841 - *** Su14 *** Hémorroïdes Internes Avec Autres Complications', - system: 'https://smt.esante.gouv.fr/terminologie-cim-10/' + system: 'https://smt.esante.gouv.fr/terminologie-cim-10/', + above_levels_ids: '*', + inferior_levels_ids: '' }, { id: 'I842', label: 'I842 - *** Su15 *** Hémorroïdes Internes Sans Autres Complications', - system: 'https://smt.esante.gouv.fr/terminologie-cim-10/' + system: 'https://smt.esante.gouv.fr/terminologie-cim-10/', + above_levels_ids: '*', + inferior_levels_ids: '' } ], source: 'AREM', @@ -57,6 +62,7 @@ export const completeConditionCriteria: Cim10DataType = { ], encounterService: [ { + label: 'GH RCP', above_levels_ids: '8312002244', cohort_id: '6935', cohort_size: '23', @@ -68,7 +74,8 @@ export const completeConditionCriteria: Cim10DataType = { source_value: 'H01', status: undefined, subItems: undefined, - type: 'Groupe hospitalier (GH)' + type: 'Groupe hospitalier (GH)', + system: System.ScopeTree } ] } diff --git a/src/__tests__/data/cohortCreation/documentCriteria.ts b/src/__tests__/data/cohortCreation/documentCriteria.ts index e5b80a2ae..74fe3683c 100644 --- a/src/__tests__/data/cohortCreation/documentCriteria.ts +++ b/src/__tests__/data/cohortCreation/documentCriteria.ts @@ -1,4 +1,5 @@ import { Comparators, CriteriaType, DocumentDataType } from 'types/requestCriterias' +import { System } from 'types/scope' import { SearchByTypes } from 'types/searchCriterias' export const defaultDocumentCriteria: DocumentDataType = { @@ -42,6 +43,7 @@ export const completeDocumentCriteria: DocumentDataType = { searchBy: SearchByTypes.TEXT, encounterService: [ { + label: 'GH RCP', above_levels_ids: '8312002244', cohort_id: '6935', cohort_size: '23', @@ -53,7 +55,8 @@ export const completeDocumentCriteria: DocumentDataType = { source_value: 'H01', status: undefined, subItems: undefined, - type: 'Groupe hospitalier (GH)' + type: 'Groupe hospitalier (GH)', + system: System.ScopeTree } ] } diff --git a/src/__tests__/data/cohortCreation/encounterCriteria.ts b/src/__tests__/data/cohortCreation/encounterCriteria.ts index f8b12c30a..28e133cbe 100644 --- a/src/__tests__/data/cohortCreation/encounterCriteria.ts +++ b/src/__tests__/data/cohortCreation/encounterCriteria.ts @@ -1,4 +1,5 @@ import { Comparators, CriteriaType, EncounterDataType } from 'types/requestCriterias' +import { System } from 'types/scope' export const defaultEncounterCriteira: EncounterDataType = { id: 1, @@ -142,6 +143,7 @@ export const completeEncounterCriteria: EncounterDataType = { ], encounterService: [ { + label: 'ASSISTANCE PUBLIQUE AP-HP', above_levels_ids: '', cohort_id: '118', cohort_size: '19215', @@ -154,7 +156,8 @@ export const completeEncounterCriteria: EncounterDataType = { source_value: 'APHP', status: undefined, subItems: undefined, - type: 'AP-HP' + type: 'AP-HP', + system: System.ScopeTree } ] } diff --git a/src/__tests__/data/cohortCreation/medicationCriteria.ts b/src/__tests__/data/cohortCreation/medicationCriteria.ts index d5c64e833..92c62d118 100644 --- a/src/__tests__/data/cohortCreation/medicationCriteria.ts +++ b/src/__tests__/data/cohortCreation/medicationCriteria.ts @@ -1,4 +1,5 @@ import { MedicationDataType, Comparators, CriteriaType } from 'types/requestCriterias' +import { System } from 'types/scope' export const defaultMedicationCriteria: MedicationDataType = { id: 1, @@ -30,12 +31,26 @@ export const completeMedicationAdministrationCriteria: MedicationDataType = { includeEncounterEndDateNull: false, encounterStatus: [{ id: 'cancelled', label: 'Cancelled', system: 'http://hl7.org/fhir/CodeSystem/encounter-status' }], code: [ - { id: 'D01AA01', label: 'D01AA01 - Nystatin; Topical', system: 'https://terminology.eds.aphp.fr/atc' }, - { id: 'D01AA02', label: 'D01AA02 - Natamycin; Topical', system: 'https://terminology.eds.aphp.fr/atc' }, + { + id: 'D01AA01', + label: 'D01AA01 - Nystatin; Topical', + system: 'https://terminology.eds.aphp.fr/atc', + above_levels_ids: '*', + inferior_levels_ids: '' + }, + { + id: 'D01AA02', + label: 'D01AA02 - Natamycin; Topical', + system: 'https://terminology.eds.aphp.fr/atc', + above_levels_ids: '*', + inferior_levels_ids: '' + }, { id: '3400890000055', label: '3400890000055 - Sawis 2mg Cpr', - system: 'https://terminology.eds.aphp.fr/smt-medicament-ucd' + system: 'https://terminology.eds.aphp.fr/smt-medicament-ucd', + above_levels_ids: '*', + inferior_levels_ids: '' } ], administration: [ @@ -47,6 +62,7 @@ export const completeMedicationAdministrationCriteria: MedicationDataType = { ], encounterService: [ { + label: 'GH RCP', above_levels_ids: '8312002244', cohort_id: '6935', cohort_size: '23', @@ -58,7 +74,8 @@ export const completeMedicationAdministrationCriteria: MedicationDataType = { source_value: 'H01', status: undefined, subItems: undefined, - type: 'Groupe hospitalier (GH)' + type: 'Groupe hospitalier (GH)', + system: System.ScopeTree } ] } @@ -75,12 +92,26 @@ export const completeMedicationPrescriptionCriteria: MedicationDataType = { includeEncounterEndDateNull: false, encounterStatus: [{ id: 'cancelled', label: 'Cancelled', system: 'http://hl7.org/fhir/CodeSystem/encounter-status' }], code: [ - { id: 'D01AA01', label: 'D01AA01 - Nystatin; Topical', system: 'https://terminology.eds.aphp.fr/atc' }, - { id: 'D01AA02', label: 'D01AA02 - Natamycin; Topical', system: 'https://terminology.eds.aphp.fr/atc' }, + { + id: 'D01AA01', + label: 'D01AA01 - Nystatin; Topical', + system: 'https://terminology.eds.aphp.fr/atc', + above_levels_ids: '*', + inferior_levels_ids: '' + }, + { + id: 'D01AA02', + label: 'D01AA02 - Natamycin; Topical', + system: 'https://terminology.eds.aphp.fr/atc', + above_levels_ids: '*', + inferior_levels_ids: '' + }, { id: '3400890000055', label: '3400890000055 - Sawis 2mg Cpr', - system: 'https://terminology.eds.aphp.fr/smt-medicament-ucd' + system: 'https://terminology.eds.aphp.fr/smt-medicament-ucd', + above_levels_ids: '*', + inferior_levels_ids: '' } ], prescriptionType: [ @@ -92,6 +123,7 @@ export const completeMedicationPrescriptionCriteria: MedicationDataType = { ], encounterService: [ { + label: 'GH RCP', above_levels_ids: '8312002244', cohort_id: '6935', cohort_size: '23', @@ -103,7 +135,8 @@ export const completeMedicationPrescriptionCriteria: MedicationDataType = { source_value: 'H01', status: undefined, subItems: undefined, - type: 'Groupe hospitalier (GH)' + type: 'Groupe hospitalier (GH)', + system: System.ScopeTree } ] } diff --git a/src/__tests__/data/cohortCreation/observationCriteria.ts b/src/__tests__/data/cohortCreation/observationCriteria.ts index 6c1683b56..1eafc2bd7 100644 --- a/src/__tests__/data/cohortCreation/observationCriteria.ts +++ b/src/__tests__/data/cohortCreation/observationCriteria.ts @@ -1,4 +1,5 @@ import { ObservationDataType, Comparators, CriteriaType } from 'types/requestCriterias' +import { System } from 'types/scope' export const defaultObservationCriteria: ObservationDataType = { id: 1, @@ -15,7 +16,6 @@ export const defaultObservationCriteria: ObservationDataType = { includeEncounterEndDateNull: false, encounterStatus: [], code: [], - isLeaf: false, searchByValue: [null, null], valueComparator: Comparators.EQUAL, encounterService: [] @@ -35,13 +35,15 @@ export const completeObservationCriteria: ObservationDataType = { { id: 'I3356', label: 'I3356 - Erythrocytes Foetaux /érythrocytes Adultes_sang_cytochimie_hf/10000 Ha', - system: 'https://terminology.eds.aphp.fr/aphp-itm-anabio' + system: 'https://terminology.eds.aphp.fr/aphp-itm-anabio', + above_levels_ids: '*', + inferior_levels_ids: '' } ], - isLeaf: true, searchByValue: [3, null], encounterService: [ { + label: 'GH RCP', above_levels_ids: '8312002244', cohort_id: '6935', cohort_size: '23', @@ -53,7 +55,8 @@ export const completeObservationCriteria: ObservationDataType = { source_value: 'H01', status: undefined, subItems: undefined, - type: 'Groupe hospitalier (GH)' + type: 'Groupe hospitalier (GH)', + system: System.ScopeTree } ] } diff --git a/src/__tests__/data/cohortCreation/procedureCriteria.ts b/src/__tests__/data/cohortCreation/procedureCriteria.ts index 9901739f8..1d6e87c92 100644 --- a/src/__tests__/data/cohortCreation/procedureCriteria.ts +++ b/src/__tests__/data/cohortCreation/procedureCriteria.ts @@ -1,4 +1,5 @@ import { CcamDataType, Comparators, CriteriaType } from 'types/requestCriterias' +import { System } from 'types/scope' export const defaultProcedureCriteria: CcamDataType = { id: 1, @@ -14,7 +15,6 @@ export const defaultProcedureCriteria: CcamDataType = { encounterEndDate: [null, null], includeEncounterEndDateNull: false, encounterStatus: [], - hierarchy: undefined, code: [], source: null, label: undefined, @@ -37,18 +37,23 @@ export const completeProcedureCriteria: CcamDataType = { { id: '000126', label: "000126 - Explorations Électrophysiologiques De L'oeil", - system: 'https://www.atih.sante.fr/plateformes-de-transmiss…ls/logiciels-espace-de-telechargement/id_lot/3550' + system: 'https://www.atih.sante.fr/plateformes-de-transmiss…ls/logiciels-espace-de-telechargement/id_lot/3550', + above_levels_ids: '*', + inferior_levels_ids: '' }, { id: '000127', label: "000127 - Échographie De L'oeil", - system: 'https://www.atih.sante.fr/plateformes-de-transmiss…ls/logiciels-espace-de-telechargement/id_lot/3550' + system: 'https://www.atih.sante.fr/plateformes-de-transmiss…ls/logiciels-espace-de-telechargement/id_lot/3550', + above_levels_ids: '*', + inferior_levels_ids: '' } ], source: 'ORBIS', encounterService: [ { + label: 'GH RCP', above_levels_ids: '8312002244', cohort_id: '6935', cohort_size: '23', @@ -60,7 +65,8 @@ export const completeProcedureCriteria: CcamDataType = { source_value: 'H01', status: undefined, subItems: undefined, - type: 'Groupe hospitalier (GH)' + type: 'Groupe hospitalier (GH)', + system: System.ScopeTree } ] } diff --git a/src/__tests__/utilsFunction/hierarchy.test.ts b/src/__tests__/utilsFunction/hierarchy.test.ts new file mode 100644 index 000000000..2322acda5 --- /dev/null +++ b/src/__tests__/utilsFunction/hierarchy.test.ts @@ -0,0 +1,594 @@ +import { HIERARCHY_ROOT } from 'services/aphp/serviceValueSets' +import { + mapCodesToCache, + mapCacheToCodes, + getHierarchyRootCodes, + cleanNode, + mapHierarchyToMap, + getMissingCodesWithSystems, + getMissingCodes, + buildMultipleTrees, + buildTree, + groupBySystem, + getDisplayFromTree, + getDisplayFromTrees, + updateBranchStatus, + getItemSelectedStatus, + getSelectedCodesFromTree, + getSelectedCodesFromTrees, + createHierarchyRoot +} from 'utils/hierarchy' // Remplacez par le nom de votre fichier +import { SelectedStatus } from 'types' +import { Codes, CodesCache, GroupedBySystem, Hierarchy, Mode } from 'types/hierarchy' +import { vi } from 'vitest' + +describe('Utility Functions', () => { + describe('mapCodesToCache', () => { + it('should map Codes to Cache correctly', () => { + const codes: Codes> = new Map([ + [ + 'system1', + new Map([ + ['code1', { id: 'code1', label: 'Label1', system: 'system1' } as Hierarchy], + ['code2', { id: 'code2', label: 'Label2', system: 'system1' } as Hierarchy] + ]) + ] + ]) + + const result = mapCodesToCache(codes) + expect(result).toEqual([ + { + id: 'system1', + options: { + code1: { id: 'code1', label: 'Label1', system: 'system1' }, + code2: { id: 'code2', label: 'Label2', system: 'system1' } + } + } + ]) + }) + }) + + describe('mapCacheToCodes', () => { + it('should map Cache to Codes correctly', () => { + const cache: CodesCache[] = [ + { + id: 'system1', + options: { + code1: { id: 'code1', label: 'Label1', system: 'system1' }, + code2: { id: 'code2', label: 'Label2', system: 'system1' } + } + } + ] + const result = mapCacheToCodes(cache) + expect(result).toEqual( + new Map([ + [ + 'system1', + new Map([ + ['code1', { id: 'code1', label: 'Label1', system: 'system1' }], + ['code2', { id: 'code2', label: 'Label2', system: 'system1' }] + ]) + ] + ]) + ) + }) + }) + + describe('getHierarchyRootCodes', () => { + it('should extract root codes from hierarchy', () => { + const hierarchy: Hierarchy[] = [ + { + id: 'root1', + label: 'Root1', + system: 'system1', + subItems: [ + { id: 'child1', label: 'Child1', system: 'system1' }, + { id: 'child2', label: 'Child2', system: 'system1' } + ] + } + ] + + const result = getHierarchyRootCodes(hierarchy) + expect(result).toEqual( + new Map([ + ['child1', { id: 'child1', label: 'Child1', system: 'system1' }], + ['child2', { id: 'child2', label: 'Child2', system: 'system1' }] + ]) + ) + }) + }) + + describe('cleanNode', () => { + it('should remove subItems and status from node', () => { + const node: Hierarchy = { + id: 'node1', + label: 'Node1', + system: 'system1', + subItems: [], + status: SelectedStatus.SELECTED + } + + const result = cleanNode(node) + expect(result).toEqual({ id: 'node1', label: 'Node1', system: 'system1' }) + }) + }) + + describe('mapHierarchyToMap', () => { + it('should map hierarchy array to Map', () => { + const hierarchy: Hierarchy[] = [ + { id: 'code1', label: 'Label1', system: 'system1' }, + { id: 'code2', label: 'Label2', system: 'system1' } + ] + + const result = mapHierarchyToMap(hierarchy) + expect(result).toEqual( + new Map([ + ['code1', { id: 'code1', label: 'Label1', system: 'system1' }], + ['code2', { id: 'code2', label: 'Label2', system: 'system1' }] + ]) + ) + }) + }) + + describe('getMissingCodesWithSystems', () => { + it('should fetch missing codes across systems', async () => { + const trees = new Map([ + ['system1', [{ id: 'root1', label: 'Root1', system: 'system1' }]], + ['system2', [{ id: 'root1', label: 'Root1', system: 'system2', inferior_levels_ids: 'root3' }]] + ]) + const groupBySystem = [ + { + system: 'system2', + codes: [{ id: 'root4', label: 'Root4', system: 'system2', above_levels_ids: 'root1,root3' }] + } + ] + const codes: Codes> = new Map([ + ['system1', new Map([['root1', { id: 'root1', label: 'Root1', system: 'system1' }]])], + [ + 'system2', + new Map([['root1', { id: 'root1', label: 'Root1', system: 'system2', inferior_levels_ids: 'root3' }]]) + ] + ]) + const fetchHandler = vi + .fn() + .mockResolvedValue([ + { id: 'root3', label: 'Root3', system: 'system2', above_levels_ids: 'root1', inferior_levels_ids: 'root4' } + ]) + const result = await getMissingCodesWithSystems(trees, groupBySystem, codes, fetchHandler) + expect(fetchHandler).toHaveBeenCalledWith('root3', 'system2') + expect(result.get('system1')).toEqual(new Map([['root1', { id: 'root1', label: 'Root1', system: 'system1' }]])) + expect(result.get('system2')).toEqual( + new Map([ + ['root1', { id: 'root1', label: 'Root1', system: 'system2', inferior_levels_ids: 'root3' }], + [ + 'root3', + { id: 'root3', label: 'Root3', system: 'system2', above_levels_ids: 'root1', inferior_levels_ids: 'root4' } + ], + ['root4', { id: 'root4', label: 'Root4', system: 'system2', above_levels_ids: 'root1,root3' }] + ]) + ) + }) + }) + + describe('getMissingCodes', () => { + it('should identify missing codes', async () => { + const node = { + id: HIERARCHY_ROOT, + label: 'Toute la hiérarchie', + system: 'system1', + inferior_levels_ids: 'subItem1' + } + const prevCodes = new Map() + const fetchHandler = vi + .fn() + .mockResolvedValue([ + { id: 'subItem1', label: 'SubItem1 ', system: 'system1', above_levels_ids: HIERARCHY_ROOT } + ]) + const result = await getMissingCodes([node], prevCodes, [node], 'system1', Mode.EXPAND, fetchHandler) + expect(fetchHandler).toHaveBeenCalledWith('subItem1', 'system1') + expect(result).toEqual( + new Map([ + [ + HIERARCHY_ROOT, + { + id: HIERARCHY_ROOT, + label: 'Toute la hiérarchie', + system: 'system1', + inferior_levels_ids: 'subItem1' + } + ], + ['subItem1', { id: 'subItem1', label: 'SubItem1 ', system: 'system1', above_levels_ids: HIERARCHY_ROOT }] + ]) + ) + }) + }) + + describe('buildTree', () => { + it('should build a tree from baseTree and endCodes', () => { + const baseTree: Hierarchy[] = [] + const endCodes: Hierarchy[] = [ + { id: 'code1', label: 'Label1', system: 'system1', above_levels_ids: '', inferior_levels_ids: '' } + ] + const codes = new Map([['code1', { id: 'code1', label: 'Label1', system: 'system1' }]]) + const selected = new Map() + const mode = Mode.INIT + + const result = buildTree(baseTree, 'system1', endCodes, codes, selected, mode) + + expect(result).toEqual([ + { id: 'code1', label: 'Label1', system: 'system1', subItems: undefined, status: undefined } + ]) + }) + }) + + describe('createHierarchyRoot', () => { + it('should create a hierarchy root node', () => { + const system = 'system1' + const status = SelectedStatus.SELECTED + + const result = createHierarchyRoot(system, status) + expect(result).toEqual({ + id: HIERARCHY_ROOT, + label: 'Toute la hiérarchie', + above_levels_ids: '', + inferior_levels_ids: '', + system, + status + }) + }) + }) + + describe('groupBySystem', () => { + it('should group codes by system correctly', () => { + const codes: Hierarchy[] = [ + { id: 'code1', label: 'Label1', system: 'system1' }, + { id: 'code2', label: 'Label2', system: 'system2' }, + { id: 'code3', label: 'Label3', system: 'system1' } + ] + + const result = groupBySystem(codes) + expect(result).toEqual([ + { system: 'system1', codes: [codes[0], codes[2]] }, + { system: 'system2', codes: [codes[1]] } + ]) + }) + }) + + describe('getDisplayFromTree', () => { + it('should generate a display extract from a tree', () => { + const tree: Hierarchy[] = [ + { + id: 'code1', + label: 'Label1', + system: 'system1', + inferior_levels_ids: 'code2,code3', + subItems: [ + { + id: 'code2', + label: 'Label2', + system: 'system1', + above_levels_ids: 'code1', + inferior_levels_ids: 'code4', + subItems: [{ id: 'code4', label: 'Label4', system: 'system1', above_levels_ids: 'code1,code2' }] + }, + { id: 'code3', label: 'Label3', system: 'system1', above_levels_ids: 'code1' } + ] + } + ] + + const toDisplay: Hierarchy[] = [ + { id: 'code1', label: 'Label1', system: 'system1', inferior_levels_ids: 'code2,code3' }, + { id: 'code4', label: 'Label4', system: 'system1', above_levels_ids: 'code1,code2' } + ] + const result = getDisplayFromTree(toDisplay, tree) + expect(result).toStrictEqual([ + { + id: 'code1', + label: 'Label1', + system: 'system1', + inferior_levels_ids: 'code2,code3', + subItems: [ + { + id: 'code2', + label: 'Label2', + system: 'system1', + above_levels_ids: 'code1', + inferior_levels_ids: 'code4', + subItems: [{ id: 'code4', label: 'Label4', system: 'system1', above_levels_ids: 'code1,code2' }] + }, + { id: 'code3', label: 'Label3', system: 'system1', above_levels_ids: 'code1' } + ] + }, + { id: 'code4', label: 'Label4', system: 'system1', above_levels_ids: 'code1,code2' } + ]) + }) + }) + + describe('getDisplayFromTrees', () => { + it('should generate different display extracts from multiple trees', () => { + const toDisplay: Hierarchy[] = [ + { id: 'code2', label: 'Label2', system: 'system1', above_levels_ids: 'code1' }, + { id: 'code2', label: 'Label2', system: 'system2' } + ] + const trees = new Map([ + [ + 'system1', + [ + { + id: 'code1', + label: 'Label1', + system: 'system1', + inferior_levels_ids: 'code2', + subItems: [{ id: 'code2', label: 'Label2', system: 'system1', above_levels_ids: 'code1' }] + } + ] + ], + ['system2', [{ id: 'code2', label: 'Label2', system: 'system2' }]] + ]) + const result = getDisplayFromTrees(toDisplay, trees) + expect(result).toStrictEqual([ + { + id: 'code2', + label: 'Label2', + system: 'system1', + above_levels_ids: 'code1' + }, + { id: 'code2', label: 'Label2', system: 'system2' } + ]) + }) + }) + + describe('updateBranchStatus', () => { + it('should update the status of a branch subitems according to the first node status', () => { + const branch: Hierarchy = { + id: HIERARCHY_ROOT, + label: 'Toute la hiérarchie', + system: 'system1', + status: SelectedStatus.SELECTED, + subItems: [{ id: 'child1', label: 'Child1', system: 'system1', status: SelectedStatus.NOT_SELECTED }] + } + + const node = updateBranchStatus(branch, branch.status) + expect(node.status).toBe(SelectedStatus.SELECTED) + expect(node.subItems![0].status).toBe(SelectedStatus.SELECTED) + }) + }) + + describe('getItemSelectedStatus', () => { + it('should return the correct status of an item according to its subitems', () => { + const notSelected: Hierarchy = { + id: HIERARCHY_ROOT, + label: 'Toute la hiérarchie', + system: 'system1', + status: SelectedStatus.SELECTED, + subItems: [{ id: '2', status: SelectedStatus.NOT_SELECTED }, { id: '3' }] + } + const selected: Hierarchy = { + id: HIERARCHY_ROOT, + label: 'Toute la hiérarchie', + system: 'system1', + status: SelectedStatus.SELECTED, + subItems: [ + { id: '2', status: SelectedStatus.SELECTED }, + { id: '3', status: SelectedStatus.SELECTED } + ] + } + const indeterminate: Hierarchy = { + id: HIERARCHY_ROOT, + label: 'Toute la hiérarchie', + system: 'system1', + status: SelectedStatus.SELECTED, + subItems: [{ id: '2', status: SelectedStatus.SELECTED }, { id: '3' }] + } + + const result1 = getItemSelectedStatus(notSelected) + expect(result1).toBe(SelectedStatus.NOT_SELECTED) + const result2 = getItemSelectedStatus(selected) + expect(result2).toBe(SelectedStatus.SELECTED) + const result3 = getItemSelectedStatus(indeterminate) + expect(result3).toBe(SelectedStatus.INDETERMINATE) + }) + }) + + describe('getSelectedCodesFromTree', () => { + it('should return selected codes from a tree', () => { + const tree: Hierarchy[] = [ + { id: 'code1', label: 'Label1', system: 'system1', status: SelectedStatus.SELECTED }, + { id: 'code2', label: 'Label2', system: 'system1', status: SelectedStatus.NOT_SELECTED } + ] + + const result = getSelectedCodesFromTree(tree) + expect(result).toEqual(new Map([['code1', tree[0]]])) + }) + }) + + describe('getSelectedCodesFromTrees', () => { + it('should return selected codes from multiple trees', () => { + const trees = new Map([ + [ + 'system1', + [{ id: HIERARCHY_ROOT, label: 'Toute la hiérarchie', system: 'system1', status: SelectedStatus.SELECTED }] + ], + [ + 'system2', + [ + { + id: HIERARCHY_ROOT, + label: 'Toute la hiérarchie', + system: 'system2', + inferior_levels_ids: 'code1,code2', + status: SelectedStatus.INDETERMINATE, + subItems: [ + { + id: 'code1', + above_levels_ids: `${HIERARCHY_ROOT}`, + status: SelectedStatus.SELECTED, + system: 'system2' + }, + { + id: 'code2', + above_levels_ids: `${HIERARCHY_ROOT}`, + status: SelectedStatus.NOT_SELECTED, + system: 'system2' + } + ] + } + ] + ] + ]) + const prevCodes: Codes = new Map() + const result = getSelectedCodesFromTrees(trees, prevCodes) + expect(result.get('system1')).toEqual( + new Map([ + [ + HIERARCHY_ROOT, + { id: HIERARCHY_ROOT, label: 'Toute la hiérarchie', system: 'system1', status: SelectedStatus.SELECTED } + ] + ]) + ) + expect(result.get('system2')).toEqual( + new Map([ + [ + 'code1', + { + id: 'code1', + above_levels_ids: `${HIERARCHY_ROOT}`, + status: SelectedStatus.SELECTED, + system: 'system2' + } + ] + ]) + ) + }) + }) + + describe('buildMultipleTrees', () => { + it('should build multiple trees according to different systems', () => { + const groupBySystem: GroupedBySystem[] = [ + { + system: 'system1', + codes: [ + { + id: HIERARCHY_ROOT, + label: 'Toute la hiérarchie', + system: 'system1' + } + ] + }, + { + system: 'system2', + codes: [ + { + id: 'code1', + above_levels_ids: HIERARCHY_ROOT, + system: 'system2' + }, + { + id: 'code2', + above_levels_ids: HIERARCHY_ROOT, + system: 'system2' + } + ] + } + ] + const baseTrees = new Map([ + [ + 'system1', + [ + { + id: HIERARCHY_ROOT, + label: 'Toute la hiérarchie', + system: 'system1' + } + ] + ], + [ + 'system2', + [ + { + id: HIERARCHY_ROOT, + label: 'Toute la hiérarchie', + system: 'system2', + inferior_levels_ids: 'code1,code2' + } + ] + ] + ]) + const codes = new Map>>([ + [ + 'system1', + new Map([ + [ + HIERARCHY_ROOT, + { + id: HIERARCHY_ROOT, + label: 'Toute la hiérarchie', + system: 'system1' + } + ] + ]) + ], + [ + 'system2', + new Map([ + [ + HIERARCHY_ROOT, + { + id: HIERARCHY_ROOT, + label: 'Toute la hiérarchie', + inferior_levels_ids: 'code1,code2', + system: 'system2' + } + ], + [ + 'code1', + { + id: 'code1', + above_levels_ids: HIERARCHY_ROOT, + system: 'system2' + } + ], + [ + 'code2', + { + id: 'code2', + above_levels_ids: HIERARCHY_ROOT, + system: 'system2' + } + ] + ]) + ] + ]) + const mode = Mode.INIT + const result = buildMultipleTrees(baseTrees, groupBySystem, codes, new Map(), mode) + expect(result.get('system1')).toEqual([ + { + id: HIERARCHY_ROOT, + label: 'Toute la hiérarchie', + system: 'system1' + } + ]) + expect(result.get('system2')).toEqual([ + { + id: HIERARCHY_ROOT, + label: 'Toute la hiérarchie', + system: 'system2', + inferior_levels_ids: 'code1,code2', + status: SelectedStatus.NOT_SELECTED, + subItems: [ + { + id: 'code1', + above_levels_ids: HIERARCHY_ROOT, + system: 'system2' + }, + { + id: 'code2', + above_levels_ids: HIERARCHY_ROOT, + system: 'system2' + } + ] + } + ]) + }) + }) +}) diff --git a/src/components/CreationCohort/ControlPanel/ControlPanel.tsx b/src/components/CreationCohort/ControlPanel/ControlPanel.tsx index 86cb80f4a..630e72f28 100644 --- a/src/components/CreationCohort/ControlPanel/ControlPanel.tsx +++ b/src/components/CreationCohort/ControlPanel/ControlPanel.tsx @@ -100,7 +100,7 @@ const ControlPanel: React.FC<{ count_outdated, snapshotsHistory } = useAppSelector((state) => state.cohortCreation.request || {}) - const { uuid, includePatient, status, jobFailMsg } = count + const { includePatient, status, jobFailMsg } = count const [requestShare, setRequestShare] = useState({ currentSnapshot, requestId, diff --git a/src/components/CreationCohort/DataList_Criteria.tsx b/src/components/CreationCohort/DataList_Criteria.tsx index e65ae7acf..abd59777f 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 DocumentsForm from './DiagramView/components/LogicalOperator/components/CriteriaRightPanel/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,28 @@ 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 { HIERARCHY_ROOT, getChildrenFromCodes, getCodeList } from 'services/aphp/serviceValueSets' + +import docTypes from 'assets/docTypes.json' +import { birthStatusData, booleanFieldsData, booleanOpenChoiceFieldsData, vmeData } from 'data/questionnaire_data' +import { VitalStatusLabel } from 'types/searchCriterias' +import { FhirItem } from 'types/valueSet' +import { createHierarchyRoot } from 'utils/hierarchy' + +const async = (fetch: () => Promise>) => async () => (await fetch()).results + +const getCodesForValueSet = async (code: string, systems: string[]) => { + if (code === HIERARCHY_ROOT && systems.length) return [createHierarchyRoot(systems[0])] + for (const system of systems) { + try { + return (await getChildrenFromCodes(system, [code])).results + } catch { + console.error("Ce n'est pas une erreur.") + } + } +} const criteriaList: () => CriteriaItemType[] = () => { const ODD_QUESTIONNAIRE = getConfig().features.questionnaires.enabled @@ -47,7 +65,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, @@ -56,17 +88,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)) } }, { @@ -77,8 +109,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)) } }, { @@ -95,10 +127,10 @@ 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: (code: string, system: string) => + getCodesForValueSet(code, [system ?? getConfig().features.condition.valueSets.conditionHierarchy.url]), + encounterStatus: async(() => getCodeList(getConfig().core.valueSets.encounterStatus.url)) } }, { @@ -108,8 +140,9 @@ const criteriaList: () => CriteriaItemType[] = () => { fontWeight: 'normal', components: CCAMForm, fetch: { - ccamData: services.cohortCreation.fetchCcamData, - encounterStatus: services.cohortCreation.fetchEncounterStatus + ccamData: (code: string, system: string) => + getCodesForValueSet(code, [system ?? getConfig().features.procedure.valueSets.procedureHierarchy.url]), + encounterStatus: async(() => getCodeList(getConfig().core.valueSets.encounterStatus.url)) } }, { @@ -119,8 +152,9 @@ const criteriaList: () => CriteriaItemType[] = () => { fontWeight: 'normal', components: GhmForm, fetch: { - ghmData: services.cohortCreation.fetchGhmData, - encounterStatus: services.cohortCreation.fetchEncounterStatus + ghmData: (code: string, system: string) => + getCodesForValueSet(code, [system ?? getConfig().features.claim.valueSets.claimHierarchy.url]), + encounterStatus: async(() => getCodeList(getConfig().core.valueSets.encounterStatus.url)) } } ] @@ -133,10 +167,22 @@ 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: (code: string, system: string) => { + const valueSets = system + ? [system] + : [ + getConfig().features.medication.valueSets.medicationAtc.url, + getConfig().features.medication.valueSets.medicationUcd.url + ] + return getCodesForValueSet(code, valueSets) + }, + 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)) } }, { @@ -154,8 +200,16 @@ const criteriaList: () => CriteriaItemType[] = () => { components: BiologyForm, disabled: !ODD_BIOLOGY, fetch: { - biologyData: services.cohortCreation.fetchBiologyData, - encounterStatus: services.cohortCreation.fetchEncounterStatus + biologyData: (code: string, system: string) => { + const valueSets = system + ? [system] + : [ + getConfig().features.observation.valueSets.biologyHierarchyAnabio.url, + getConfig().features.observation.valueSets.biologyHierarchyLoinc.url + ] + return getCodesForValueSet(code, valueSets) + }, + encounterStatus: async(() => getCodeList(getConfig().core.valueSets.encounterStatus.url)) } }, { @@ -190,14 +244,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)) } }, { @@ -208,36 +270,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)) } } ] @@ -252,8 +348,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..0508d05b6 --- /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..ef0ec0ed1 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,46 @@ -import React, { useState, useEffect, useContext } from 'react' -import { Tabs, Tab } 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 { Comparators, ObservationDataType, CriteriaType } from 'types/requestCriterias' +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 '../formStyles' +import { Comparators, ObservationDataType, CriteriaType, CriteriaDataKey } 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' +import { HIERARCHY_ROOT } from 'services/aphp/serviceValueSets' +import { mappingCriteria, mappingHierarchyCriteria } from 'utils/mappers' + +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 +52,280 @@ 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 [searchByValueInput, setSearchByValueInput] = useState([ + currentCriteria.searchByValue[0]?.toString() ?? '', + currentCriteria.searchByValue[1]?.toString() ?? '' + ]) 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) + const [multiFields, setMultiFields] = useState(localStorage.getItem('multiple_fields')) + const [error, setError] = useState(Error.NO_ERROR) + const [isLeaf, setIsLeaf] = useState(false) + + const handleSearchValues = (newValue: string, index: number) => { + const invalidCharRegex = /[^0-9.-]/ + if (invalidCharRegex.exec(newValue)) return + const rawValues = searchByValueInput + rawValues[index] = newValue + setSearchByValueInput(rawValues) + const parsedValue = isNaN(parseFloat(newValue)) ? null : parseFloat(newValue) + const values: [number | null, number | null] = + index === 0 ? [parsedValue, currentCriteria.searchByValue[1]] : [currentCriteria.searchByValue[0], parsedValue] + checkSearchValues(values, currentCriteria.valueComparator) } - // 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 checkSearchValues = (values: [null | number, null | number], comparator: Comparators) => { + setError(Error.NO_ERROR) + let val1 = values[1] + if (comparator !== Comparators.BETWEEN) { + val1 = null + setSearchByValueInput([searchByValueInput[0], '']) + } + if (values[0] !== null && val1 !== null && values[0] > val1) { + setError(Error.INCOHERENT_VALUE_ERROR) + } else if (comparator === Comparators.BETWEEN && (values[0] === null || val1 === null)) { + setError(Error.MISSING_VALUE_ERROR) + } + setCurrentCriteria({ + ...currentCriteria, + searchByValue: [values[0], val1], + valueComparator: comparator + }) } + + useEffect(() => { + setError(Error.NO_ERROR) + if ( + currentCriteria.code.length === 1 && + !currentCriteria.code?.[0]?.inferior_levels_ids && + currentCriteria.code?.[0]?.id !== HIERARCHY_ROOT + ) + setIsLeaf(true) + else { + setIsLeaf(false) + setCurrentCriteria({ + ...currentCriteria, + searchByValue: [null, null] + }) + } + }, [currentCriteria.code]) + + const biologyReferences = useMemo(() => { + return getValueSetsFromSystems([ + getConfig().features.observation.valueSets.biologyHierarchyAnabio.url, + getConfig().features.observation.valueSets.biologyHierarchyLoinc.url + ]) + }, []) + useEffect(() => { - _initSyncHierarchyTableEffect() + const encounterStatus = + mappingCriteria(currentCriteria?.encounterStatus, CriteriaDataKey.ENCOUNTER_STATUS, criteriaData) || [] + const code = mappingHierarchyCriteria(currentCriteria?.code, CriteriaDataKey.BIOLOGY_DATA, criteriaData) || [] + setCurrentCriteria({ ...currentCriteria, code, encounterStatus }) }, []) + 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" + /> + + + + + + + + handleSearchValues(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 && ( + handleSearchValues(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 56% 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 c5a79a2a4..b6180869f 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,76 @@ -import React, { useState } from 'react' - +import React, { useEffect, 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, CriteriaDataKey, 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' +import { mappingCriteria, mappingHierarchyCriteria } from 'utils/mappers' 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 ccamReferences = useMemo(() => { + return getValueSetsFromSystems([getConfig().features.procedure.valueSets.procedureHierarchy.url]) + }, []) + + useEffect(() => { + const code = mappingHierarchyCriteria(currentCriteria?.code, CriteriaDataKey.CCAM_DATA, criteriaData) || [] + const encounterStatus = + mappingCriteria(currentCriteria?.encounterStatus, CriteriaDataKey.ENCOUNTER_STATUS, criteriaData) || [] + setCurrentCriteria({ ...currentCriteria, code, encounterStatus }) + }, []) - 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 ?? '?' - } - }) - : [] - - return isOpen ? ( + return ( {!isEdition ? ( @@ -123,13 +108,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 +122,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 +149,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,33 +175,30 @@ 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" + /> + 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 +210,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..abefeb937 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,39 @@ -import React, { useEffect, useState } from 'react' -import { Tab, Tabs } from '@mui/material' +import React, { useEffect, 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 { Cim10DataType, Comparators, CriteriaDataKey, 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 { getConfig } from 'config' +import ValueSetField from 'components/SearchValueSet/ValueSetField' +import { getValueSetsFromSystems } from 'utils/valueSets' +import { LabelObject } from 'types/searchCriterias' +import { mappingCriteria, mappingHierarchyCriteria } from 'utils/mappers' + +enum Error { + ADVANCED_INPUTS_ERROR, + NO_ERROR +} export const defaultCondition: Omit = { type: CriteriaType.CONDITION, @@ -29,83 +51,198 @@ 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( - (selectedCriteria as Cim10DataType) || defaultCondition + 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: LabelObject) => 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]) }, []) + useEffect(() => { + const code = mappingHierarchyCriteria(currentCriteria?.code, CriteriaDataKey.CIM_10_DIAGNOSTIC, criteriaData) || [] + const encounterStatus = + mappingCriteria(currentCriteria?.encounterStatus, CriteriaDataKey.ENCOUNTER_STATUS, criteriaData) || [] + setCurrentCriteria({ ...currentCriteria, code, encounterStatus }) + }, []) 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/DemographicForm/index.tsx b/src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/DemographicForm/index.tsx index 2b42ac562..571852591 100644 --- a/src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/DemographicForm/index.tsx +++ b/src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/DemographicForm/index.tsx @@ -24,29 +24,15 @@ import CalendarRange from 'components/ui/Inputs/CalendarRange' import DurationRange from 'components/ui/Inputs/DurationRange' import { CriteriaDataKey, DemographicDataType, CriteriaType } from 'types/requestCriterias' import { BlockWrapper } from 'components/ui/Layout' -import { CriteriaDrawerComponentProps, CriteriaItemDataCache } from 'types' +import { CriteriaDrawerComponentProps } from 'types' import { CriteriaLabel } from 'components/ui/CriteriaLabel' +import { mappingCriteria } from 'utils/mappers' enum Error { INCOHERENT_AGE_ERROR, NO_ERROR } -export const mappingCriteria = ( - criteriaToMap: LabelObject[] | null, - key: CriteriaDataKey, - mapping: CriteriaItemDataCache -) => { - if (criteriaToMap) { - return criteriaToMap.map((criteria) => { - const mappedCriteria = mapping.data?.[key]?.find((c: LabelObject) => c?.id === criteria?.id) - return mappedCriteria - }) - } else { - return [] - } -} - const DemographicForm = (props: CriteriaDrawerComponentProps) => { const { criteriaData, onChangeSelectedCriteria, goBack } = props const selectedCriteria: DemographicDataType | null = props.selectedCriteria as DemographicDataType diff --git a/src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/DocumentsForm/DocumentsForm.tsx b/src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/DocumentsForm/index.tsx similarity index 96% rename from src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/DocumentsForm/DocumentsForm.tsx rename to src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/DocumentsForm/index.tsx index c43918b47..a462eb327 100644 --- a/src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/DocumentsForm/DocumentsForm.tsx +++ b/src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/DocumentsForm/index.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' @@ -32,12 +32,13 @@ import { useDebounce } from 'utils/debounce' import { SearchByTypes, FilterByDocumentStatus } from 'types/searchCriterias' import { IndeterminateCheckBoxOutlined } from '@mui/icons-material' import { SearchInputError } from 'types/error' -import { Comparators, DocumentDataType, CriteriaType } from 'types/requestCriterias' +import { Comparators, DocumentDataType, CriteriaType, CriteriaDataKey } from 'types/requestCriterias' import Searchbar from 'components/ui/Searchbar' import SearchInput from 'components/ui/Searchbar/SearchInput' import { BlockWrapper } from 'components/ui/Layout' import OccurenceInput from 'components/ui/Inputs/Occurences' import { SourceType } from 'types/scope' +import { mappingCriteria } from 'utils/mappers' const defaultComposition: Omit = { type: CriteriaType.DOCUMENTS, @@ -72,13 +73,13 @@ const DocumentsForm: React.FC = (props) => { const [multiFields, setMultiFields] = useState(localStorage.getItem('multiple_fields')) const [searchCheckingLoading, setSearchCheckingLoading] = useState(false) const [searchInputError, setSearchInputError] = useState(undefined) - const [occurrence, setOccurrence] = useState(defaultValues.occurrence || 1) + const [occurrence, setOccurrence] = useState(defaultValues.occurrence ?? 1) const [occurrenceComparator, setOccurrenceComparator] = useState( - defaultValues.occurrenceComparator || Comparators.GREATER_OR_EQUAL + defaultValues.occurrenceComparator ?? Comparators.GREATER_OR_EQUAL ) const debouncedSearchItem = useDebounce(500, defaultValues.search) - const isEdition = selectedCriteria !== null ? true : false + const isEdition = selectedCriteria !== null const docStatuses: string[] = [FilterByDocumentStatus.VALIDATED, FilterByDocumentStatus.NOT_VALIDATED] @@ -290,7 +291,7 @@ const DocumentsForm: React.FC = (props) => { noOptionsText="Veuillez entrer un statut de visite associée" getOptionLabel={(option) => option.label} isOptionEqualToValue={(option, value) => option.id === value.id} - value={defaultValues.encounterStatus} + value={mappingCriteria(defaultValues?.encounterStatus, CriteriaDataKey.ENCOUNTER_STATUS, criteriaData)} onChange={(e, value) => _onChangeValue('encounterStatus', value)} renderInput={(params) => } /> 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 ba197c122..03af33d9f 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 @@ -27,11 +27,11 @@ import OccurenceInput from 'components/ui/Inputs/Occurences' import Collapse from 'components/ui/Collapse' import CalendarRange from 'components/ui/Inputs/CalendarRange' import DurationRange from 'components/ui/Inputs/DurationRange' -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' +import { mappingCriteria } from 'utils/mappers' enum Error { EMPTY_FORM, @@ -82,9 +82,7 @@ const EncounterForm = ({ const [admission, setAdmission] = useState( mappingCriteria(criteria?.admission, CriteriaDataKey.ADMISSION, criteriaData) || [] ) - const [encounterService, setEncounterService] = useState[]>( - criteria?.encounterService || [] - ) + const [encounterService, setEncounterService] = useState[]>(criteria?.encounterService || []) const [encounterStartDate, setEncounterStartDate] = useState( criteria?.encounterStartDate || [null, null] ) @@ -107,7 +105,7 @@ const EncounterForm = ({ ) const { classes } = useStyles() - const [multiFields, setMultiFields] = useState(localStorage.getItem('multiple_fields')) + const [multiFields] = useState(localStorage.getItem('multiple_fields')) const isEdition = selectedCriteria !== null || false const [error, setError] = useState(Error.NO_ERROR) const selectedPopulation = useAppSelector((state) => state.cohortCreation.request.selectedPopulation || []) 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 53% 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..290ba0ef9 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, { useEffect, useMemo, useState } from 'react' import { Alert, Autocomplete, @@ -14,70 +13,59 @@ import { Typography } from '@mui/material' +import useStyles from '../formStyles' +import { CriteriaDrawerComponentProps } from 'types' +import { Comparators, GhmDataType, CriteriaType, CriteriaDataKey } 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' +import { mappingCriteria, mappingHierarchyCriteria } from 'utils/mappers' 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 { classes } = useStyles() - const dispatch = useAppDispatch() - const initialState: HierarchyTree | null = useAppSelector((state) => state.syncHierarchyTable) - const currentState = { ...selectedCriteria, ...initialState } +const GhmForm = (props: CriteriaDrawerComponentProps) => { + const { criteriaData, selectedCriteria, onChangeSelectedCriteria, goBack } = props + const [currentCriteria, setCurrentCriteria] = useState((selectedCriteria as GhmDataType) ?? defaultClaim) + + 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 ghmReferences = useMemo(() => { + return getValueSetsFromSystems([getConfig().features.claim.valueSets.claimHierarchy.url]) + }, []) - 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 ? ( + useEffect(() => { + const code = mappingHierarchyCriteria(currentCriteria?.code, CriteriaDataKey.GHM_DATA, criteriaData) || [] + const encounterStatus = + mappingCriteria(currentCriteria?.encounterStatus, CriteriaDataKey.ENCOUNTER_STATUS, criteriaData) || [] + setCurrentCriteria({ ...currentCriteria, code, encounterStatus }) + }, []) + + return ( {!isEdition ? ( @@ -127,13 +115,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,52 +129,52 @@ 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" + /> + 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 +186,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..2eb571c27 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,38 @@ -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 { Comparators, MedicationDataType, CriteriaType, CriteriaDataKey } from 'types/requestCriterias' +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' +import { LabelObject } from 'types/searchCriterias' +import { mappingCriteria, mappingHierarchyCriteria } from 'utils/mappers' -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' +enum Error { + ADVANCED_INPUTS_ERROR, + NO_ERROR +} export const defaultMedication: Omit = { type: CriteriaType.MEDICATION_REQUEST, @@ -29,77 +49,223 @@ 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) + + useEffect(() => { + if (currentCriteria.type === CriteriaType.MEDICATION_ADMINISTRATION) { + setCurrentCriteria({ ...currentCriteria, endOccurrence: [null, null] }) + } + }, [currentCriteria.type]) + + const medicationReferences = useMemo(() => { + return getValueSetsFromSystems([ + getConfig().features.medication.valueSets.medicationAtc.url, + getConfig().features.medication.valueSets.medicationUcd.url + ]) + }, []) - 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) - - const _initSyncHierarchyTableEffect = async () => { - await initSyncHierarchyTableEffect( - medicationHierarchy, - selectedCriteria, - defaultCriteria && defaultCriteria.code ? defaultCriteria.code : [], - fetchMedication, - defaultMedication.type, - dispatch - ) - } useEffect(() => { - _initSyncHierarchyTableEffect() + const encounterStatus = + mappingCriteria(currentCriteria?.encounterStatus, CriteriaDataKey.ENCOUNTER_STATUS, criteriaData) || [] + const code = mappingHierarchyCriteria(currentCriteria?.code, CriteriaDataKey.MEDICATION_DATA, criteriaData) || [] + setCurrentCriteria({ ...currentCriteria, code, encounterStatus }) }, []) + if (criteriaData?.data?.prescriptionTypes === 'loading' || criteriaData?.data?.administrations === 'loading') { + return <> + } + + const selectedCriteriaPrescriptionType = + currentCriteria.type === CriteriaType.MEDICATION_REQUEST && currentCriteria.prescriptionType + ? currentCriteria.prescriptionType.map((prescriptionType) => { + const criteriaPrescriptionType = criteriaData.data.prescriptionTypes + ? criteriaData.data.prescriptionTypes.find((p: LabelObject) => 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: LabelObject) => p.id === administration.id) + : null + return { + id: administration.id, + label: administration.label ? administration.label : criteriaAdministration?.label ?? '?' + } + }) + : [] + return ( - <> - setSelectedTab(tab)} - > - - - - - { - - } - { - setSelectedTab('form')} - goBack={goBack} - /> - } - + + + {!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)} + /> + + + + {!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..b33c20e6e 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 @@ -18,12 +18,12 @@ import OccurenceInput from 'components/ui/Inputs/Occurences' import Collapse from 'components/ui/Collapse' import SearchbarWithCheck from 'components/ui/Inputs/SearchbarWithCheck' import CalendarRange from 'components/ui/Inputs/CalendarRange' -import { mappingCriteria } from '../DemographicForm' 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' +import { mappingCriteria } from 'utils/mappers' enum Error { EMPTY_FORM, @@ -91,9 +91,7 @@ const PregnantForm = ({ const [ultrasoundMonitoring, setUltrasoundMonitoring] = useState( mappingCriteria(criteria?.ultrasoundMonitoring, CriteriaDataKey.ULTRASOUND_MONITORING, criteriaData) || [] ) - const [encounterService, setEncounterService] = useState[]>( - criteria?.encounterService || [] - ) + const [encounterService, setEncounterService] = useState[]>(criteria?.encounterService || []) const [occurrence, setOccurrence] = useState(criteria?.occurrence || 1) const [occurrenceComparator, setOccurrenceComparator] = useState( criteria?.occurrenceComparator || Comparators.GREATER_OR_EQUAL 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..ab2ce75b2 100644 --- a/src/components/CreationCohort/DiagramView/components/PopulationCard/PopulationCard.tsx +++ b/src/components/CreationCohort/DiagramView/components/PopulationCard/PopulationCard.tsx @@ -14,7 +14,7 @@ export type PopulationCardPropsType = { label?: string loading: boolean onEditDisabled: boolean - population: Hierarchy[] + population: Hierarchy[] onEdit: () => void } @@ -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 85% rename from src/components/CreationCohort/DiagramView/DiagramView.tsx rename to src/components/CreationCohort/DiagramView/index.tsx index c60c46061..da18bf4b3 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() @@ -21,9 +22,9 @@ const DiagramView = () => { const maintenanceIsActive = useAppSelector((state) => state.me?.maintenance?.active || false) const [openDrawer, setOpenDrawer] = useState(false) const [rightsError, setRightsError] = useState(false) - const [selectedCodes, setSelectedCodes] = useState[]>([]) + const [selectedCodes, setSelectedCodes] = useState[]>([]) - const handleChangePopulation = (selectedPopulation: Hierarchy[]) => { + const handleChangePopulation = (selectedPopulation: Hierarchy[]) => { if ( selectedPopulation?.some((perimeter) => perimeter?.access === 'Pseudonymisé') && checkNominativeCriteria(requestState.selectedCriteria) @@ -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 ( @@ -70,7 +70,7 @@ const DiagramView = () => { )} - + {selectedPopulation && selectedPopulation.length > 0 && ( { {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 34a4947aa..66a9f6156 100644 --- a/src/components/Dashboard/BiologyList/index.tsx +++ b/src/components/Dashboard/BiologyList/index.tsx @@ -18,9 +18,8 @@ import SearchInput from 'components/ui/Searchbar/SearchInput' import TextInput from 'components/Filters/TextInput' import { ResourceType } from 'types/requestCriterias' -import { Hierarchy } from 'types/hierarchy' import { DTTB_ResultsType as ResultsType, LoadingStatus, CohortObservation } from 'types' -import { BiologyFilters, Direction, FilterKeys, Order } from 'types/searchCriterias' +import { BiologyFilters, Direction, FilterKeys, LabelObject, Order } from 'types/searchCriterias' import { CanceledError } from 'axios' import { useSavedFilters } from 'hooks/filters/useSavedFilters' import services from 'services/aphp' @@ -28,15 +27,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, cleanSearchParams, 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 = { deidentified?: boolean @@ -55,7 +52,7 @@ const BiologyList = ({ deidentified }: BiologyListProps) => { const [toggleSavedFiltersModal, setToggleSavedFiltersModal] = useState(false) const [toggleFilterInfoModal, setToggleFilterInfoModal] = useState(false) const [isReadonlyFilterInfoModal, setIsReadonlyFilterInfoModal] = useState(true) - const [encounterStatusList, setEncounterStatusList] = useState[]>([]) + const [encounterStatusList, setEncounterStatusList] = useState([]) const [page, setPage] = useState(getPageParam ? parseInt(getPageParam, 10) : 1) const { @@ -78,24 +75,23 @@ const BiologyList = ({ 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 +118,7 @@ const BiologyList = ({ 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,8 +166,8 @@ const BiologyList = ({ deidentified }: BiologyListProps) => { useEffect(() => { const fetch = async () => { try { - const encounterStatus = await services.cohortCreation.fetchEncounterStatus() - setEncounterStatusList(encounterStatus) + const encounterStatusList = (await getCodeList(getConfig().core.valueSets.encounterStatus.url)).results + setEncounterStatusList(encounterStatusList) } catch (e) { /* empty */ } @@ -193,8 +189,7 @@ const BiologyList = ({ deidentified }: BiologyListProps) => { validatedStatus, nda, ipp, - loinc, - anabio, + code, startDate, endDate, executiveUnits, @@ -215,6 +210,13 @@ const BiologyList = ({ deidentified }: BiologyListProps) => { } }, [loadingStatus]) + const references = useMemo(() => { + return getValueSetsFromSystems([ + getConfig().features.observation.valueSets.biologyHierarchyAnabio.url, + getConfig().features.observation.valueSets.biologyHierarchyLoinc.url + ]) + }, []) + return ( @@ -319,8 +321,7 @@ const BiologyList = ({ deidentified }: BiologyListProps) => { > {!deidentified && } {!deidentified && } - - + { searchInput, nda, ipp, - anabio, - loinc, + code, startDate, endDate, validatedStatus, @@ -391,8 +391,7 @@ const BiologyList = ({ deidentified }: BiologyListProps) => { filters: { nda, ipp, - anabio, - loinc, + code, startDate, endDate, validatedStatus, @@ -445,19 +444,11 @@ const BiologyList = ({ deidentified }: BiologyListProps) => { )} - - - - diff --git a/src/components/Dashboard/Documents/Documents.tsx b/src/components/Dashboard/Documents/index.tsx similarity index 95% rename from src/components/Dashboard/Documents/Documents.tsx rename to src/components/Dashboard/Documents/index.tsx index 49bd8fc98..776addafd 100644 --- a/src/components/Dashboard/Documents/Documents.tsx +++ b/src/components/Dashboard/Documents/index.tsx @@ -11,7 +11,8 @@ import { Order, searchByListDocuments, SearchByTypes, - FilterByDocumentStatus + FilterByDocumentStatus, + LabelObject } from 'types/searchCriterias' import allDocTypesList from 'assets/docTypes.json' import { SearchInputError } from 'types/error' @@ -41,11 +42,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, cleanSearchParams, handlePageError } from 'utils/paginationUtils' import { CanceledError } from 'axios' import { DocumentReference } from 'fhir/r4' +import { getCodeList } from 'services/aphp/serviceValueSets' +import { getConfig } from 'config' +import { v4 as uuidv4 } from 'uuid' type DocumentsProps = { deidentified: boolean @@ -83,7 +86,7 @@ const Documents: React.FC = ({ 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 +206,8 @@ const Documents: React.FC = ({ 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() @@ -300,7 +303,7 @@ const Documents: React.FC = ({ deidentified }) => { )} - + {!!allSavedFilters?.count && ( + + + + + + ) +} + +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..92d0f0f95 100644 --- a/src/components/ui/Tabs/index.tsx +++ b/src/components/ui/Tabs/index.tsx @@ -1,36 +1,37 @@ import React from 'react' -import { Box } from '@mui/material' import { TabType } from 'types' import { TabWrapper, TabsWrapper } from './styles' type TabsProps = { values: TabType[] active: TabType + disabled?: boolean onchange: (newValue: TabType) => void + variant?: 'pink' | 'blue' } -const Tabs = ({ values, active, onchange }: TabsProps) => { +const Tabs = ({ values, active, disabled = false, onchange, variant = 'blue' }: 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..efd18daae 100644 --- a/src/components/ui/Tabs/styles.ts +++ b/src/components/ui/Tabs/styles.ts @@ -1,8 +1,9 @@ import { Tab, Tabs } from '@mui/material' import { styled } from '@mui/material/styles' -type CustomProps = { +type TabCustomProps = { width: string + variant: 'pink' | 'blue' } export const TabsWrapper = styled(Tabs)(() => ({ @@ -14,20 +15,37 @@ export const TabsWrapper = styled(Tabs)(() => ({ } })) -export const TabWrapper = styled(Tab)(({ width }) => ({ - color: '#FFF', - backgroundColor: '#153D8A', +export const TabWrapper = styled(Tab)(({ width, variant }) => ({ padding: '0px 6px', - fontWeight: 600, fontSize: 13, width: width, - '&.Mui-selected': { - backgroundColor: '#0063AF', - color: '#FFF', - fontWeight: 900, - fontSize: 12 - }, '&.MuiButtonBase-root.MuiTab-root': { minHeight: 40 - } + }, + '&.Mui-selected': { + fontWeight: 900 + }, + ...(variant === 'pink' && { + color: '#00000099', + backgroundColor: 'transparent', + fontWeight: 700, + borderBottomWidth: 'thin', + borderColor: 'rgba(0,0,0,0.12)', + borderStyle: 'solid', + '&.Mui-selected': { + backgroundColor: 'transparent', + color: '#ED6D91', + fontSize: 13 + } + }), + ...(variant === 'blue' && { + color: '#FFF', + backgroundColor: '#153D8A', + fontWeight: 600, + '&.Mui-selected': { + backgroundColor: '#0063AF', + color: '#FFF', + fontSize: 12 + } + }) })) diff --git a/src/config.tsx b/src/config.tsx index ea53b8237..fa59f35ed 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 7a79f9ffc..05a9bdb8d 100644 --- a/src/constants.js +++ b/src/constants.js @@ -6,5 +6,3 @@ export const CONFIG_URL = !!`${ENV_CONFIG_URL}`.match('VITE_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..14b23a7df --- /dev/null +++ b/src/data/valueSets.ts @@ -0,0 +1,99 @@ +import { AppConfig } from 'config' +import { LabelObject } from 'types/searchCriterias' +import { 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: LabelObject) => /^[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: LabelObject) => + 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..830aede90 100644 --- a/src/hooks/hierarchy/useHierarchy.ts +++ b/src/hooks/hierarchy/useHierarchy.ts @@ -1,131 +1,176 @@ import { - buildHierarchy, - getHierarchyDisplay, - getItemSelectedStatus, + buildTree, + buildMultipleTrees, + getDisplayFromTree, + getDisplayFromTrees, getMissingCodes, - mapHierarchyToMap -} from './../../utils/hierarchy' + getMissingCodesWithSystems, + groupBySystem, + getHierarchyRootCodes, + mapHierarchyToMap, + getSelectedCodesFromTrees, + createHierarchyRoot, + DEFAULT_HIERARCHY_INFO +} from '../../utils/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 } 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 [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() + const 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 = getDisplayFromTree(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) + const search = async (fetchSearch: () => Promise>>) => { + setLoadingStatus({ ...loadingStatus, search: LoadingStatus.FETCHING }) + try { + 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) - setHierarchyRepresentation(newTree) - setHierarchyDisplay(newDisplay) + setTrees(newTrees) setLoadingStatus({ ...loadingStatus, search: LoadingStatus.SUCCESS }) + return { display: getDisplayFromTrees(endCodes, newTrees), count } + } catch (e) { + return { + display: [], + count: 0 + } } - init() - }, []) + } - 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) - setLoadingStatus({ ...loadingStatus, search: LoadingStatus.SUCCESS }) + 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: '' }) } - 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[], toAdd: boolean, mode: SearchMode.EXPLORATION | SearchMode.RESEARCH) => { + const bySystem = groupBySystem(nodes) + const newTrees = buildMultipleTrees(trees, bySystem, codes, selectedCodes, toAdd ? Mode.SELECT : Mode.UNSELECT) + let system = '' + if (mode === SearchMode.EXPLORATION) { + system = nodes?.[0].system + const current = hierarchies.get(system) || DEFAULT_HIERARCHY_INFO + const newHierarchy = getDisplayFromTrees(current.tree, newTrees) + setHierarchies(replaceInMap(system, { ...current, tree: newHierarchy }, hierarchies)) + } else { + const newSearch = getDisplayFromTrees(searchResults.tree, newTrees) + setSearchResults({ ...searchResults, tree: newSearch }) + } + setSelectedCodes(getSelectedCodesFromTrees(newTrees, selectedCodes, system)) + setTrees(newTrees) } - const selectAll = (toAdd: boolean) => { + const selectAll = (system: string, toAdd: boolean) => { + const nodes = trees.get(system) || [] + const currentHierarchy = hierarchies.get(system) || DEFAULT_HIERARCHY_INFO + const bySystem = groupBySystem(nodes) 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 newTrees = buildMultipleTrees(trees, bySystem, codes, selectedCodes, mode) + const newSearch = getDisplayFromTrees(searchResults.tree, newTrees) + const newHierarchy = getDisplayFromTrees(currentHierarchy.tree, newTrees) + const root = new Map() + if (toAdd) root.set(HIERARCHY_ROOT, createHierarchyRoot(system)) + setSelectedCodes(replaceInMap(system, root, selectedCodes)) + setTrees(newTrees) + setSearchResults({ ...searchResults, tree: newSearch }) + setHierarchies(replaceInMap(system, { ...currentHierarchy, tree: newHierarchy }, hierarchies)) } - 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 newHierarchy = getDisplayFromTree(currentHierarchy.tree, newTree) + setCodes(replaceInMap(hierarchyId, newCodes, codes)) + setTrees(replaceInMap(hierarchyId, newTree, trees)) + setHierarchies(replaceInMap(hierarchyId, { ...currentHierarchy, tree: newHierarchy }, hierarchies)) setLoadingStatus({ ...loadingStatus, search: LoadingStatus.SUCCESS }) } return { - hierarchy: hierarchyDisplay, + trees, + 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..1d8f1b7b7 --- /dev/null +++ b/src/hooks/scopeTree/useScopeTree.ts @@ -0,0 +1,114 @@ +import { useCallback, useEffect, useMemo, useState } from 'react' +import { Codes, Hierarchy, Mode, 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 { DEFAULT_HIERARCHY_INFO, getItemSelectedStatus, mapCodesToCache } from 'utils/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 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 { trees, hierarchies, searchResults, selectedCodes, loadingStatus, initTrees, fetchMore, expand, select } = + useHierarchy(selectedNodes, codes, handleSaveCodes, fetchChildren) + + useEffect(() => { + initTrees([ + { system: System.ScopeTree, fetchBaseTree: async () => ({ results: baseTree, count: baseTree.length }) } + ]) + }, [baseTree]) + + const current = useMemo(() => { + const current = + mode === SearchMode.EXPLORATION ? hierarchies.get(System.ScopeTree) || DEFAULT_HIERARCHY_INFO : searchResults + return current + }, [mode, hierarchies, searchResults]) + + const selectAllStatus = useMemo(() => { + const subItems = mode === SearchMode.RESEARCH ? searchResults.tree : trees.get(System.ScopeTree) + const node = { + id: 'parent', + subItems + } as Hierarchy + return getItemSelectedStatus(node) + }, [trees, mode]) + + 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) + } + } + + const handleSelect = (type: Mode.SELECT | Mode.SELECT_ALL, toAdd: boolean, codes: Hierarchy[] = []) => { + if (type === Mode.SELECT_ALL) codes = trees.get(System.ScopeTree) || [] + select(codes, toAdd, mode) + } + + return { + hierarchyData: { + hierarchy: current, + loadingStatus, + selectAllStatus, + selectedCodes: selected + }, + hierarchyActions: { + expand, + select: handleSelect + }, + 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..b5a0b8c93 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/useIsOverflow.ts b/src/hooks/useIsOverflow.ts new file mode 100644 index 000000000..beb8dd350 --- /dev/null +++ b/src/hooks/useIsOverflow.ts @@ -0,0 +1,33 @@ +import React, { MutableRefObject, useLayoutEffect, useState } from 'react' + +type UseIsOverflowArgs = { + ref: MutableRefObject + // eslint-disable-next-line @typescript-eslint/no-explicit-any + additionalDependencies?: Record +} + +export const useIsOverflow = ({ ref, additionalDependencies }: UseIsOverflowArgs) => { + const [isOverflow, setIsOverflow] = useState(undefined) + + useLayoutEffect(() => { + const { current } = ref + + if (!current) return + + const checkOverflow = () => { + const hasOverflow = current.scrollHeight > current.clientHeight + setIsOverflow(hasOverflow) + } + checkOverflow() + + // this is used to monitor size changes + const resizeObserver = new ResizeObserver(() => checkOverflow()) + resizeObserver.observe(current) + + return () => { + resizeObserver.disconnect() + } + }, [ref, additionalDependencies]) + + return isOverflow +} diff --git a/src/hooks/valueSet/useSearchValueSet.ts b/src/hooks/valueSet/useSearchValueSet.ts new file mode 100644 index 000000000..bd7ae880f --- /dev/null +++ b/src/hooks/valueSet/useSearchValueSet.ts @@ -0,0 +1,188 @@ +import { useCallback, useEffect, useMemo, useRef, useState } from 'react' +import { FhirItem, 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, Hierarchy, SearchMode } from 'types/hierarchy' +import { saveValueSets, selectValueSetCodes } from 'state/valueSets' +import { useAppDispatch, useAppSelector } from 'state' +import { DEFAULT_HIERARCHY_INFO, getItemSelectedStatus, mapCodesToCache } from 'utils/hierarchy' +import { LoadingStatus } from 'types' +import { cancelPendingRequest } from 'utils/abortController' + +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 controllerRef = useRef(null) + + 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, selectAll } = + useHierarchy(selectedNodes, codes, handleSaveCodes, fetchChildren) + + const currentHierarchy = useMemo(() => { + const ref = explorationParameters.options.references.find((ref) => ref.checked) + return hierarchies.get(ref?.url ?? '') ?? DEFAULT_HIERARCHY_INFO + }, [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) + 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) { + if (loadingStatus.search === LoadingStatus.FETCHING) + controllerRef.current = cancelPendingRequest(controllerRef.current) + return await searchInValueSets( + references, + searchInput, + page * LIMIT_PER_PAGE, + LIMIT_PER_PAGE, + controllerRef.current?.signal + ) + } 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 + if (isRoot) selectAll(code.system, false) + else select([code], false, SearchMode.EXPLORATION) + } + + 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, + selectAll + } + } +} diff --git a/src/mappers/filters.ts b/src/mappers/filters.ts index 291cf546e..7450cca95 100644 --- a/src/mappers/filters.ts +++ b/src/mappers/filters.ts @@ -40,12 +40,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' const getGenericKeyFromResourceType = ( type: ResourceType, @@ -72,13 +70,38 @@ const getGenericKeyFromResourceType = ( return '' } +const getValueSetCodes = async (parameters: URLSearchParams, key: string) => { + const codeIds = decodeURIComponent(parameters.get(key) ?? '') + const urlMap: Map = new Map() + 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')) const startDate = dates.find((e) => e.includes('ge'))?.split('ge')?.[1] ?? null const endDate = dates.find((e) => e.includes('le'))?.split('le')?.[1] ?? null const executiveUnitsParams = parameters.get(getGenericKeyFromResourceType(type, 'EXECUTIVE_UNITS')) - let executiveUnits: Hierarchy[] = [] + let executiveUnits: Hierarchy[] = [] if (executiveUnitsParams) { const fetchedData = await servicesPerimeters.getPerimeters({ ids: executiveUnitsParams }) executiveUnits = fetchedData.results @@ -88,11 +111,11 @@ 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], - label: allEncounterStatus.find((encounterStatus) => encounterStatus.id === elem.split('|')?.[1])?.label || '' + label: allEncounterStatus.find((encounterStatus) => encounterStatus.id === elem.split('|')?.[1])?.label ?? '' } }) } @@ -146,22 +169,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) ?? '' @@ -173,15 +188,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, @@ -191,15 +198,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 @@ -211,16 +210,17 @@ 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 || '' } + return { id: elem.split('|')?.[1], label: types.find((type) => type.id === elem.split('|')?.[1])?.label ?? '' } }) } const { nda, startDate, endDate, executiveUnits, encounterStatus } = await mapGenericFromRequestParams( 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) => { @@ -229,44 +229,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 || '' } + 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) => { @@ -274,11 +257,12 @@ 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], - label: allModalities.find((allModality) => allModality.id === elem.split('|')?.[1])?.label || '' + label: allModalities.find((allModality) => allModality.id === elem.split('|')?.[1])?.label ?? '' } }) } @@ -394,11 +378,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( @@ -410,11 +392,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) @@ -425,11 +405,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( @@ -439,13 +417,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 }, @@ -456,13 +438,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 }, @@ -473,34 +459,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/mappers/pmsi.ts b/src/mappers/pmsi.ts index c83d7b786..02bcba019 100644 --- a/src/mappers/pmsi.ts +++ b/src/mappers/pmsi.ts @@ -1,4 +1,3 @@ -import { getConfig } from 'config' import { Claim, Condition, Procedure } from 'fhir/r4' import { Medication, Pmsi } from 'state/patient' import { CohortPMSI } from 'types' @@ -7,6 +6,16 @@ import { PMSIResourceTypes, ResourceType } from 'types/requestCriterias' import { SourceType } from 'types/scope' import { Order } from 'types/searchCriterias' +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 +} + export function mapToAttribute( type: ResourceType.MEDICATION_ADMINISTRATION | ResourceType.MEDICATION_REQUEST ): keyof Medication @@ -55,16 +64,6 @@ export const mapToLabelSingular = (tabId: PMSIResourceTypes) => { return mapToLabel[tabId] } -export const mapToUrlCode = (tabId: PMSIResourceTypes) => { - const mapToUrlCode = { - [ResourceType.CONDITION]: getConfig().features.condition.valueSets.conditionHierarchy.url, - [ResourceType.PROCEDURE]: getConfig().features.procedure.valueSets.procedureHierarchy.url, - [ResourceType.CLAIM]: getConfig().features.claim.valueSets.claimHierarchy.url - } - - return mapToUrlCode[tabId] -} - export const mapToSourceType = (tabId: PMSIResourceTypes) => { const tabIdMapper = { [ResourceType.CONDITION]: SourceType.CIM10, diff --git a/src/reducers/searchCriteriasReducer.ts b/src/reducers/searchCriteriasReducer.ts index 1e09667aa..b2eaba2d9 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 3e65cb5ca..ded5b37ad 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' @@ -24,7 +23,6 @@ import { DiagnosticReport, DocumentReference, Encounter, - Extension, ImagingStudy, Location, MedicationAdministration, @@ -36,12 +34,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, mapDocumentStatusesToRequestParam, @@ -63,8 +57,6 @@ import { QuestionnaireResponseParamsKeys } from 'types/requestCriterias' 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 => @@ -74,7 +66,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 @@ -496,7 +488,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}`] @@ -568,7 +560,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}`] @@ -635,7 +627,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 @@ -669,8 +661,7 @@ type fetchObservationProps = { sortDirection?: Direction _text?: string encounter?: string - loinc?: string - anabio?: string + code?: string subject?: string minDate?: string maxDate?: string @@ -691,8 +682,7 @@ export const fetchObservation = async (args: fetchObservationProps): FHIR_Bundle sortDirection, _text, encounter, - loinc, - anabio, + code, subject, minDate, maxDate, @@ -714,13 +704,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.CODE}=${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 @@ -754,6 +740,8 @@ type fetchMedicationRequestProps = { _text?: string encounter?: string ipp?: string + patientIds?: string + code?: string subject?: string type?: string[] minDate: string | null @@ -769,6 +757,7 @@ export const fetchMedicationRequest = async ( ): FHIR_Bundle_Promise_Response => { const { id, + code, size, offset, _sort, @@ -799,12 +788,13 @@ export const fetchMedicationRequest = async ( if (subject) options = [...options, `subject=${subject}`] if (ipp) options = [...options, `${PrescriptionParamsKeys.IPP}=${ipp}`] 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) 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) @@ -831,6 +821,7 @@ type fetchMedicationAdministrationProps = { size?: number offset?: number _sort?: string + code?: string sortDirection?: Direction _text?: string encounter?: string @@ -850,6 +841,7 @@ export const fetchMedicationAdministration = async ( ): FHIR_Bundle_Promise_Response => { const { id, + code, size, offset, _sort, @@ -880,12 +872,13 @@ export const fetchMedicationAdministration = async ( if (subject) options = [...options, `subject=${subject}`] if (ipp) options = [...options, `${AdministrationParamsKeys.IPP}=${ipp}`] 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) @@ -953,7 +946,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, `${ImagingParamsKeys.IPP}=${ipp}`] if (minDate) options = [...options, `${ImagingParamsKeys.DATE}=ge${minDate}`] @@ -1129,135 +1122,6 @@ export const fetchDiagnosticReport = async (args: fetchDiagnosticReportProps) => 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/serviceBiology.ts b/src/services/aphp/serviceBiology.ts deleted file mode 100644 index af78dea88..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.biologyHierarchyAnabio.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 5dc85f19e..15cae5c17 100644 --- a/src/services/aphp/serviceCohorts.ts +++ b/src/services/aphp/serviceCohorts.ts @@ -77,7 +77,7 @@ import { getResourceInfos } from 'utils/fillElement' import { substructAgeString } from 'utils/age' import { getExtension } from 'utils/fhir' import { PMSIResourceTypes, ResourceType } from 'types/requestCriterias' -import { mapToOrderByCode, mapToUrlCode } from 'mappers/pmsi' +import { mapToOrderByCode } from 'mappers/pmsi' import { mapMedicationToOrderByCode } from 'mappers/medication' import { linkToDiagnosticReport } from './serviceImaging' @@ -528,7 +528,7 @@ const servicesCohorts: IServiceCohorts = { const filtersMapper = { [ResourceType.CONDITION]: () => ({ ...commonFilters(), - code: code.map((e) => encodeURIComponent(`${mapToUrlCode(selectedTab)}|`) + e.id).join(','), + code: code.map((e) => encodeURIComponent(`${e.system}|${e.id}`)).join(','), source: source, type: diagnosticTypes?.map((type) => type.id), 'min-recorded-date': startDate ?? '', @@ -537,7 +537,7 @@ const servicesCohorts: IServiceCohorts = { }), [ResourceType.PROCEDURE]: () => ({ ...commonFilters(), - code: code.map((e) => encodeURIComponent(`${mapToUrlCode(selectedTab)}|`) + e.id).join(','), + code: code.map((e) => encodeURIComponent(`${e.system}|${e.id}`)).join(','), source: source, minDate: startDate ?? '', maxDate: endDate ?? '', @@ -545,7 +545,7 @@ const servicesCohorts: IServiceCohorts = { }), [ResourceType.CLAIM]: () => ({ ...commonFilters(), - diagnosis: code.map((e) => encodeURIComponent(`${mapToUrlCode(selectedTab)}|`) + e.id).join(','), + diagnosis: code.map((e) => encodeURIComponent(`${e.system}|${e.id}`)).join(','), minCreated: startDate ?? '', maxCreated: endDate ?? '', uniqueFacet: ['patient'] @@ -614,6 +614,7 @@ const servicesCohorts: IServiceCohorts = { orderBy, searchInput, filters: { + code, nda, ipp, startDate, @@ -644,6 +645,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'] }) @@ -670,7 +672,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), @@ -723,7 +726,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 = @@ -734,8 +737,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({ @@ -753,8 +755,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 3acf28284..b5489565f 100644 --- a/src/services/aphp/servicePatients.ts +++ b/src/services/aphp/servicePatients.ts @@ -34,7 +34,6 @@ import { Condition, DocumentReference, Encounter, - Group, Identifier, ImagingStudy, MedicationAdministration, @@ -178,6 +177,7 @@ export interface IServicePatients { sortDirection: Direction, searchInput: string, nda: string, + code: string, selectedPrescriptionTypeIds: string[], selectedAdministrationRouteIds: string[], startDate: string | null, @@ -220,8 +220,7 @@ export interface IServicePatients { rowStatus: boolean, searchInput: string, nda: string, - loinc: string, - anabio: string, + code: string, startDate?: string | null, endDate?: string | null, groupId?: string, @@ -457,7 +456,7 @@ const servicesPatients: IServicePatients = { _sort: sortBy === Order.CODE ? Order.CODE : Order.RECORDED_DATE, sortDirection: sortDirection, 'encounter-identifier': nda, - code: code, + code, source: source, type: diagnosticTypes, 'min-recorded-date': startDate ?? '', @@ -477,7 +476,7 @@ const servicesPatients: IServicePatients = { _sort: sortBy === Order.CODE ? Order.CODE : Order.DATE, sortDirection: sortDirection, 'encounter-identifier': nda, - code: code, + code, source: source, minDate: startDate ?? '', maxDate: endDate ?? '', @@ -556,6 +555,7 @@ const servicesPatients: IServicePatients = { sortDirection, searchInput, nda, + code, selectedPrescriptionTypeIds, selectedAdministrationRouteIds, startDate, @@ -579,6 +579,7 @@ const servicesPatients: IServicePatients = { _sort: sortBy, sortDirection, type: selectedPrescriptionTypeIds, + code, minDate: startDate, maxDate: endDate, signal, @@ -601,7 +602,8 @@ const servicesPatients: IServicePatients = { maxDate: endDate, signal, executiveUnits, - encounterStatus + encounterStatus, + code }) break default: @@ -626,8 +628,7 @@ const servicesPatients: IServicePatients = { rowStatus: boolean, searchInput: string, nda: string, - loinc: string, - anabio: string, + code: string, startDate?: string | null, endDate?: string | null, groupId?: string, @@ -644,8 +645,7 @@ const servicesPatients: IServicePatients = { offset: page ? (page - 1) * 20 : 0, _text: searchInput, encounter: nda, - loinc: loinc, - anabio: anabio, + code, minDate: startDate ?? '', maxDate: endDate ?? '', rowStatus, @@ -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 fc5c9736d..dec2a1f19 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' @@ -37,7 +37,7 @@ export interface IServicePerimeters { * Retour: * - booléen */ - allowSearchIpp: (selectedPopulation: Hierarchy[]) => Promise + allowSearchIpp: (selectedPopulation: Hierarchy[]) => Promise /** * Cette fonction retourne les informations lié à un (ou plusieurs) périmètre(s) @@ -60,7 +60,7 @@ export interface IServicePerimeters { * Retour: * - ScopeTreeTableRow | undefined */ - fetchPopulationForRequeteur: (perimeterId: string[]) => Promise[]> + fetchPopulationForRequeteur: (perimeterId: string[]) => Promise[]> /** * Cette fonction retourne l'ensemble des perimetres auquels un practitioner a le droit @@ -72,13 +72,16 @@ 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 @@ -185,7 +188,7 @@ const servicesPerimeters: IServicePerimeters = { }, fetchPopulationForRequeteur: async (cohortIds) => { - let population: Hierarchy[] = [] + let population: Hierarchy[] = [] const ids = cohortIds.join(',') if (ids) { const response = (await servicesPerimeters.getRights({ limit: -1, cohortIds: ids, sourceType: SourceType.ALL })) @@ -195,6 +198,7 @@ const servicesPerimeters: IServicePerimeters = { { id: Rights.EXPIRED, name: '', + label: '', source_value: '', above_levels_ids: '', inferior_levels_ids: '', @@ -202,7 +206,8 @@ const servicesPerimeters: IServicePerimeters = { type: '', cohort_id: '', cohort_size: '', - full_path: '' + full_path: '', + system: System.ScopeTree } ] else population = response @@ -210,7 +215,7 @@ const servicesPerimeters: IServicePerimeters = { return population }, - mapRightsToScopeElement: (item: ReadRightPerimeter): ScopeElement => { + mapRightsToScopeElement: (item: ReadRightPerimeter): Hierarchy => { const { perimeter, read_role, @@ -230,12 +235,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, @@ -252,6 +259,8 @@ const servicesPerimeters: IServicePerimeters = { return { id: id.toString(), name, + system: System.ScopeTree, + label: `${source_value} - ${name}`, source_value, type, parent_id, @@ -263,8 +272,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 = [] @@ -293,8 +305,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..8a5883f61 --- /dev/null +++ b/src/services/aphp/serviceValueSets.ts @@ -0,0 +1,275 @@ +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 { Hierarchy } 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' +import { FhirItem } from 'types/valueSet' +import axios from 'axios' + +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 +} + +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 + ) +} + +/** + * 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 fhirItem + * @returns + */ +const mapFhirHierarchyToHierarchyWithLabelAndSystem = (fhirItem: FhirItem): Hierarchy => { + return { + id: fhirItem.id, + label: fhirItem.label, + system: fhirItem.system, + above_levels_ids: fhirItem.parentIds?.join(',') ?? '', + inferior_levels_ids: fhirItem.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 + try { + 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 + } catch (error) { + if (axios.isCancel(error)) throw 'Cancelled request.' + return { + count: 0, + results: [] + } + } +} + +/** + * 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: Hierarchy) => boolean = () => true, + filterOut: (code: Hierarchy) => boolean = (value: Hierarchy) => 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 + })) as Hierarchy[] + ).filter((code) => !filterOut(code)) + + const chapters = codeList + .filter((code) => filterRoots(code)) + .map((e) => mapFhirHierarchyToHierarchyWithLabelAndSystem(e)) + const toBeAdoptedCodes = codeList.filter((code) => !filterRoots(code)) + const childrenIds = chapters.map((code) => code.id) + let subItems: Hierarchy[] | undefined = undefined + if (toBeAdoptedCodes.length) { + const unknownChaptersIds = toBeAdoptedCodes.map((code) => code.id) + const unknownChapters = (await getChildrenFromCodes(codeSystem, unknownChaptersIds)).results + const unknownChapter: Hierarchy = { + id: UNKOWN_HIERARCHY_CHAPTER, + label: `${UNKOWN_HIERARCHY_CHAPTER}`, + system: codeSystem, + above_levels_ids: HIERARCHY_ROOT, + inferior_levels_ids: unknownChapters.map((code) => code.id).join(','), + subItems: unknownChapters + } + const chaptersEntities = (await getChildrenFromCodes(codeSystem, childrenIds)).results + childrenIds.push(UNKOWN_HIERARCHY_CHAPTER) + subItems = [...chaptersEntities, unknownChapter] + } + const 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/cohortCreation.ts b/src/state/cohortCreation.ts index 7b9e9b6e8..02d66e1d3 100644 --- a/src/state/cohortCreation.ts +++ b/src/state/cohortCreation.ts @@ -35,8 +35,8 @@ export type CohortCreationState = { navHistory: CurrentSnapshot[] snapshotsHistory: QuerySnapshotInfo[] count: CohortCreationCounterType - selectedPopulation: Hierarchy[] | null - executiveUnits: (Hierarchy | undefined)[] | null + selectedPopulation: Hierarchy[] | null + executiveUnits: (Hierarchy | undefined)[] | null allowSearchIpp: boolean selectedCriteria: SelectedCriteriaType[] isCriteriaNominative: boolean 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 ee4c02207..f1aff71b2 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 | { @@ -97,30 +94,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' @@ -177,26 +150,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/patient.ts b/src/state/patient.ts index 9efbf64f5..1cc209758 100644 --- a/src/state/patient.ts +++ b/src/state/patient.ts @@ -52,7 +52,7 @@ import { } from 'types/searchCriterias' import { isCustomError } from 'utils/perimeters' import { PMSIResourceTypes, ResourceType } from 'types/requestCriterias' -import { mapToAttribute, mapToUrlCode } from 'mappers/pmsi' +import { mapToAttribute } from 'mappers/pmsi' import { getExtension } from 'utils/fhir' import { getConfig } from 'config' import { linkToDiagnosticReport } from 'services/aphp/serviceImaging' @@ -129,8 +129,7 @@ const fetchPmsi = createAsyncThunk encodeURIComponent(`${codeUrl}|`) + e.id).join(',') + const code = options.searchCriterias.filters.code.map((e) => encodeURIComponent(`${e.system}|${e.id}`)).join(',') const source = options.searchCriterias.filters.source ?? '' const diagnosticTypes = options.searchCriterias.filters.diagnosticTypes?.map((type) => type.id) ?? [] const nda = options.searchCriterias.filters.nda @@ -229,16 +228,7 @@ const fetchBiology = createAsyncThunk encodeURIComponent(`${getConfig().features.observation.valueSets.biologyHierarchyLoinc.url}|`) + e.id - ) - .join(',') - const anabio = searchCriterias.filters.anabio - .map( - (e) => encodeURIComponent(`${getConfig().features.observation.valueSets.biologyHierarchyAnabio.url}|`) + e.id - ) - .join(',') + const code = options.searchCriterias.filters.code.map((e) => encodeURIComponent(`${e.system}|${e.id}`)).join(',') const startDate = searchCriterias.filters.startDate const endDate = searchCriterias.filters.endDate const executiveUnits = searchCriterias.filters.executiveUnits.map((unit) => unit.id) @@ -253,8 +243,7 @@ const fetchBiology = createAsyncThunk unit.id) const encounterStatus = searchCriterias.filters.encounterStatus?.map(({ id }) => id) + const code = options.searchCriterias.filters.code.map((e) => encodeURIComponent(`${e.system}|${e.id}`)).join(',') const medicationResponse = await services.patients.fetchMedication( page, @@ -344,6 +334,7 @@ const fetchMedication = createAsyncThunk< sortDirection, searchInput, nda, + code, prescriptionTypes, administrationRoutes, startDate, 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..095878595 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' export type ScopeState = { - rights: Hierarchy[] + 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..079301e3c 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 } from 'types/hierarchy' import { logout } from './me' import { LabelObject } from 'types/searchCriterias' +import { RootState } from 'state' +import { mapCacheToCodes } from 'utils/hierarchy' +import { FhirItem } from 'types/valueSet' -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 51de0a8f3..04926e37c 100644 --- a/src/types.ts +++ b/src/types.ts @@ -33,7 +33,6 @@ import { SelectedCriteriaType } from 'types/requestCriterias' import { ExportTableType } from 'components/Dashboard/ExportModal/export_table' -import { Hierarchy } from 'types/hierarchy' import { SearchByTypes } from 'types/searchCriterias' import { PMSILabel } from 'types/patient' @@ -379,13 +378,11 @@ export type CriteriaItemType = { fontWeight?: string components: React.FC | null disabled?: boolean - fetch?: { [key in CriteriaDataKey]?: FetchFunctionVariant } + fetch?: { [key in CriteriaDataKey]?: any } subItems?: CriteriaItemType[] } -type FetchFunctionVariant = - | (() => Promise) - | ((searchValue?: string, noStar?: boolean, signal?: AbortSignal) => Promise[]>) +export type ResearchType = string | boolean | AbortSignal | undefined export type ValueSet = { code: string @@ -708,9 +705,10 @@ export type IPatientImaging = { page: number } -export type TabType = { +export type TabType = { label: TL id: T + active?: boolean icon?: ReactElement wrapped?: boolean } @@ -740,13 +738,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..14064f576 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,55 @@ 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 Hierarchy = AbstractTree< + S, + T & { + label: string + above_levels_ids: string + inferior_levels_ids: string + system: string + status?: SelectedStatus + } +> + 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/patient.ts b/src/types/patient.ts index 99bf05f26..ef323e54f 100644 --- a/src/types/patient.ts +++ b/src/types/patient.ts @@ -1,6 +1,6 @@ export enum PatientTableLabels { GENDER = 'Sexe', - FIRSTNAME = 'Prénom', + NAME = 'Prénom', LASTNAME = 'Nom', BIRTHDATE = 'Date de naissance', AGE = 'Âge', diff --git a/src/types/requestCriterias.ts b/src/types/requestCriterias.ts index 9a732f9a8..3056c400c 100644 --- a/src/types/requestCriterias.ts +++ b/src/types/requestCriterias.ts @@ -1,6 +1,7 @@ import { ScopeElement, SimpleCodeType } from 'types' import { Hierarchy } from './hierarchy' import { DocumentAttachmentMethod, DurationRangeType, LabelObject, SearchByTypes } from './searchCriterias' +import { FhirItem } from './valueSet' export enum QuestionnaireResponseParamsKeys { NAME = 'questionnaire.name', @@ -124,19 +125,20 @@ export enum AdministrationParamsKeys { IPP = 'subject.identifier', ADMINISTRATION_ROUTES = 'dosage-route', DATE = 'effective-time', + CODE = 'code', EXECUTIVE_UNITS = 'context.encounter-care-site', ENCOUNTER_STATUS = 'context.status' } export enum ObservationParamsKeys { NDA = 'encounter.identifier', - ANABIO_LOINC = 'code', VALIDATED_STATUS = 'status', DATE = 'date', VALUE = 'value-quantity', EXECUTIVE_UNITS = 'encounter.encounter-care-site', ENCOUNTER_STATUS = 'encounter.status', - IPP = 'subject.identifier' + IPP = 'subject.identifier', + CODE = 'code' } export enum ImagingParamsKeys { @@ -221,7 +223,7 @@ export type CommonCriteriaDataType = { id: number error?: boolean type: CriteriaType - encounterService?: Hierarchy[] + encounterService?: Hierarchy[] isInclusive?: boolean title: string } @@ -288,7 +290,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', @@ -334,8 +335,7 @@ export type CcamDataType = CommonCriteriaDataType & WithEncounterDateDataType & WithEncounterStatusDataType & { type: CriteriaType.PROCEDURE - hierarchy: undefined - code: LabelObject[] | null + code: Hierarchy[] source: string | null label: undefined } @@ -345,7 +345,7 @@ export type Cim10DataType = CommonCriteriaDataType & WithEncounterDateDataType & WithEncounterStatusDataType & { type: CriteriaType.CONDITION - code: LabelObject[] | null + code: Hierarchy[] source: string | null diagnosticType: LabelObject[] | null label: undefined @@ -381,7 +381,7 @@ export type GhmDataType = CommonCriteriaDataType & WithEncounterDateDataType & WithEncounterStatusDataType & { type: CriteriaType.CLAIM - code: LabelObject[] | null + code: Hierarchy[] label: undefined } @@ -496,7 +496,7 @@ export type MedicationDataType = CommonCriteriaDataType & WithOccurenceCriteriaDataType & WithEncounterDateDataType & WithEncounterStatusDataType & { - code: LabelObject[] | null + code: Hierarchy[] administration: LabelObject[] | null } & ( | { @@ -511,8 +511,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..5effcd091 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' +} diff --git a/src/types/searchCriterias.ts b/src/types/searchCriterias.ts index 4973aa40c..28189943e 100644 --- a/src/types/searchCriterias.ts +++ b/src/types/searchCriterias.ts @@ -3,6 +3,7 @@ import { PatientTableLabels } from './patient' import { CohortsType } from './cohorts' import { ResourceType } from './requestCriterias' import { Hierarchy } from './hierarchy' +import { FhirItem } from './valueSet' export enum FormNames { PREGNANCY = 'APHPEDSQuestionnaireFicheGrossesse', @@ -100,25 +101,28 @@ export enum DirectionLabel { DESC = 'Décroissant' } export enum Order { - ADMINISTRATION_MODE = 'route', AGE_MONTH = 'age-month', - ANABIO = 'code-anabio', AUTHORED = 'authored', - BIRTHDATE = 'birthdate', + LABEL = 'label', CODE = 'code', + RESULT_SIZE = 'result_size', + RECORDED_DATE = 'recorded-date', + FAMILY = 'family', + DATE = 'date', CREATED = 'created', + DIAGNOSIS = 'diagnosis', + ANABIO = 'code-anabio', + TYPE = 'type-name', + LASTNAME = 'lastname', + BIRTHDATE = 'birthdate', CREATED_AT = 'created_at', - DATE = 'date', DESCRIPTION = 'description', - DIAGNOSIS = 'diagnosis', EFFECTIVE_TIME = 'effective-time', ENCOUNTER = 'encounter', - FAMILY = 'family', FAVORITE = 'favorite', GENDER = 'gender', ID = 'id', IPP = 'identifier', - LASTNAME = 'lastname', LOINC = 'code-loinc', MEDICATION_ATC = 'medication-atc', MEDICATION_UCD = 'medication-ucd', @@ -128,12 +132,11 @@ export enum Order { PERIOD_START = 'Period-start', PRESCRIPTION_TYPES = 'category-name', PROCEDURE = 'procedureCode', - RECORDED_DATE = 'recorded-date', - RESULT_SIZE = 'result_size', STUDY_DATE = 'started', SUBJECT_IDENTIFIER = 'subject-identifier', - TYPE = 'type-name', - UPDATED = 'updated_at' + UPDATED = 'updated_at', + ADMINISTRATION_MODE = 'route', + DISPLAY = 'display' } export enum SearchByTypes { TEXT = '_text', @@ -187,8 +190,6 @@ export enum FilterKeys { PRESCRIPTION_TYPES = 'prescriptionTypes', CODE = 'code', NDA = 'nda', - ANABIO = 'anabio', - LOINC = 'loinc', DIAGNOSTIC_TYPES = 'diagnosticTypes', START_DATE = 'startDate', END_DATE = 'endDate', @@ -246,8 +247,8 @@ export type FilterValue = | GenderStatus[] | VitalStatus | VitalStatus[] - | Hierarchy - | Hierarchy[] + | Hierarchy + | Hierarchy[] | SimpleCodeType | SimpleCodeType[] | null @@ -266,7 +267,7 @@ export type GenericFilter = { nda: string startDate: string | null endDate: string | null - executiveUnits: Hierarchy[] + executiveUnits: Hierarchy[] encounterStatus: LabelObject[] } @@ -278,7 +279,7 @@ export interface PatientsFilters { export type PMSIFilters = GenericFilter & { diagnosticTypes?: LabelObject[] - code: LabelObject[] + code: Hierarchy[] source?: string ipp?: string } @@ -287,11 +288,11 @@ export type MedicationFilters = GenericFilter & { prescriptionTypes?: LabelObject[] administrationRoutes?: LabelObject[] ipp?: string + code: Hierarchy[] } export type BiologyFilters = GenericFilter & { - loinc: LabelObject[] - anabio: LabelObject[] + code: Hierarchy[] validatedStatus: boolean ipp?: string } @@ -306,7 +307,7 @@ export type MaternityFormFilters = { startDate: string | null endDate: string | null encounterStatus: LabelObject[] - executiveUnits: Hierarchy[] + executiveUnits: Hierarchy[] ipp?: string } @@ -393,7 +394,7 @@ export const orderByListPatients = [ }, { id: Order.NAME, - label: PatientTableLabels.FIRSTNAME + label: PatientTableLabels.NAME }, { id: Order.FAMILY, diff --git a/src/types/valueSet.ts b/src/types/valueSet.ts new file mode 100644 index 000000000..f622db839 --- /dev/null +++ b/src/types/valueSet.ts @@ -0,0 +1,44 @@ +import { Hierarchy } 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: Hierarchy) => boolean +} + +export type FhirItem = { + id: string + label: string + parentIds?: string[] + childrenIds?: string[] + system: string +} 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.ts b/src/utils/cohortCreation.ts index b95549737..1f7134352 100644 --- a/src/utils/cohortCreation.ts +++ b/src/utils/cohortCreation.ts @@ -85,8 +85,9 @@ 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 { getConfig } from 'config' +import { Hierarchy } from 'types/hierarchy' +import { FhirItem } from 'types/valueSet' const REQUETEUR_VERSION = 'v1.6.0' @@ -261,7 +262,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 })) } @@ -371,10 +372,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)), @@ -394,10 +392,7 @@ export const buildConditionFilter = (criterion: Cim10DataType): string[] => { export const buildProcedureFilter = (criterion: CcamDataType): string[] => { return [ 'subject.active=true', - filtersBuilders( - ProcedureParamsKeys.CODE, - buildLabelObjectFilter(criterion.code, getConfig().features.procedure.valueSets.procedureHierarchy.url) - ), + filtersBuilders(ProcedureParamsKeys.CODE, buildLabelObjectFilter(criterion.code, true)), filtersBuilders(ProcedureParamsKeys.EXECUTIVE_UNITS, buildEncounterServiceFilter(criterion.encounterService)), filtersBuilders(ProcedureParamsKeys.ENCOUNTER_STATUS, buildLabelObjectFilter(criterion.encounterStatus)), filtersBuilders(ProcedureParamsKeys.DATE, buildDateFilter(criterion.startOccurrence[0], 'ge')), @@ -416,10 +411,7 @@ export const buildProcedureFilter = (criterion: CcamDataType): string[] => { export const buildClaimFilter = (criterion: GhmDataType): string[] => { return [ 'patient.active=true', - filtersBuilders( - ClaimParamsKeys.CODE, - buildLabelObjectFilter(criterion.code, getConfig().features.claim.valueSets.claimHierarchy.url) - ), + filtersBuilders(ClaimParamsKeys.CODE, buildLabelObjectFilter(criterion.code, true)), filtersBuilders(ClaimParamsKeys.EXECUTIVE_UNITS, buildEncounterServiceFilter(criterion.encounterService)), filtersBuilders(ClaimParamsKeys.ENCOUNTER_STATUS, buildLabelObjectFilter(criterion.encounterStatus)), filtersBuilders(ClaimParamsKeys.DATE, buildDateFilter(criterion.startOccurrence[0], 'ge')), @@ -449,10 +441,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 @@ -485,10 +474,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.CODE, 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')), @@ -774,7 +760,7 @@ const mapCriteriaToResource = (criteriaType: CriteriaType): ResourceType => { } export function buildRequest( - selectedPopulation: (Hierarchy | undefined)[] | null, + selectedPopulation: (Hierarchy | undefined)[] | null, selectedCriteria: SelectedCriteriaType[], criteriaGroup: CriteriaGroup[], temporalConstraints: TemporalConstraintsType[] @@ -1174,7 +1160,6 @@ const unbuildProcedureCriteria = async (element: RequeteurCriteriaType): Promise startOccurrence: [null, null], source: null, label: undefined, - hierarchy: undefined, encounterService: [], occurrenceComparator: null, encounterStatus: [], @@ -1277,10 +1262,7 @@ const unbuildMedicationCriteria = async (element: RequeteurCriteriaType): Promis } return await unbuildCriteria(element, currentCriterion, { - [PrescriptionParamsKeys.CODE]: (c, v) => { - const codeIds = v?.replace(/https:\/\/.*?\|/g, '') - unbuildLabelObjectFilter(c, 'code', codeIds) - }, + [PrescriptionParamsKeys.CODE]: (c, v) => unbuildLabelObjectFilter(c, 'code', v), [PrescriptionParamsKeys.PRESCRIPTION_TYPES]: (c, v) => unbuildLabelObjectFilter(c, 'prescriptionType', v), [PrescriptionParamsKeys.PRESCRIPTION_ROUTES || AdministrationParamsKeys.ADMINISTRATION_ROUTES]: (c, v) => unbuildLabelObjectFilter(c, 'administration', v), @@ -1329,7 +1311,6 @@ const unbuildObservationCriteria = async (element: RequeteurCriteriaType): Promi type: CriteriaType.OBSERVATION, title: element.name ?? 'Critère de biologie', code: [], - isLeaf: false, occurrence: null, startOccurrence: [null, null], encounterService: [], @@ -1341,21 +1322,8 @@ const unbuildObservationCriteria = async (element: RequeteurCriteriaType): Promi } return await unbuildCriteria(element, currentCriterion, { - [ObservationParamsKeys.ANABIO_LOINC]: async (c, v) => { + [ObservationParamsKeys.CODE]: async (c, v) => { unbuildLabelObjectFilter(c, 'code', v) - - // TODO: pas propre vvvv - if (currentCriterion.code && currentCriterion.code.length === 1) { - try { - const checkChildrenResp = await services.cohortCreation.fetchBiologyHierarchy(currentCriterion.code?.[0].id) - - if (checkChildrenResp.length === 0) { - currentCriterion.isLeaf = true - } - } catch (error) { - console.error('Erreur lors du check des enfants du code de biologie sélectionné', error) - } - } }, [ObservationParamsKeys.EXECUTIVE_UNITS]: async (c, v) => await unbuildEncounterServiceCriterias(c, 'encounterService', v), @@ -2111,7 +2079,6 @@ export const getDataFromFetch = async ( (criterion.type === CriteriaType.MEDICATION_REQUEST || criterion.type === CriteriaType.MEDICATION_ADMINISTRATION)) ) - if (currentSelectedCriteria) { for (const currentcriterion of currentSelectedCriteria) { if ( @@ -2129,10 +2096,12 @@ export const getDataFromFetch = async ( currentcriterion.code.length > 0 ) { for (const code of currentcriterion.code) { - const prevData = prevDataCache[dataKey]?.find((data: any) => data.id === code?.id) - const codeData = prevData ? [prevData] : await _criterion.fetch[dataKey]?.(code?.id, true) + const prevData = prevDataCache[dataKey]?.find( + (data: any) => data.id === code?.id && data.system === code?.system + ) + const codeData = prevData ? [prevData] : await _criterion.fetch[dataKey]?.(code?.id, code?.system) const existingCodes = criteriaDataCache.data[dataKey] || [] - criteriaDataCache.data[dataKey] = [...existingCodes, ...(codeData || [])] + criteriaDataCache.data[dataKey] = [...existingCodes, ...((codeData as Hierarchy[]) || [])] } } } @@ -2224,44 +2193,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 f12c027ff..af8418827 100644 --- a/src/utils/filters.ts +++ b/src/utils/filters.ts @@ -17,6 +17,7 @@ import { getDurationRangeLabel } from './age' import { CohortsType, CohortsTypeLabel } from 'types/cohorts' import { Hierarchy } from 'types/hierarchy' import labels from 'labels.json' +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 index d65eab444..47bea2551 100644 --- a/src/utils/hierarchy.ts +++ b/src/utils/hierarchy.ts @@ -1,9 +1,53 @@ import { SelectedStatus } from 'types' -import { Hierarchy, InfiniteMap, Mode } from 'types/hierarchy' +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 cleanNodes = (nodes: Hierarchy[]) => { - return nodes.map((item) => ({ ...item, subItems: undefined, status: undefined })) +export const DEFAULT_HIERARCHY_INFO = { + tree: [], + count: 0, + page: 1, + system: '' +} + +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[]) => { @@ -27,12 +71,29 @@ const addAllFetchedIds = (codes: Map>, results: return new Map([...codes, ...resultsMap]) } +export const getMissingCodesWithSystems = async ( + trees: Map[]>, + groupBySystem: GroupedBySystem[], + codes: Codes>, + fetchHandler: (ids: string, system: string) => Promise[]> +) => { + const 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) => Promise[]> + fetchHandler: (ids: string, system: string) => Promise[]> ) => { const newCodesMap = mapHierarchyToMap(newCodes) let allCodes = new Map([...prevCodes, ...newCodesMap]) @@ -45,127 +106,187 @@ export const getMissingCodes = async ( } if (missingIds.length) { const ids = missingIds.join(',') - const fetched = await fetchHandler(ids) + const fetched = await fetchHandler(ids, system) allCodes = addAllFetchedIds(allCodes, fetched) } if (mode !== Mode.EXPAND) { - const children = getInferiorLevels(above.map((id) => allCodes.get(id)!)) + 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) + const childrenResponse = await fetchHandler(ids, system) allCodes = addAllFetchedIds(allCodes, childrenResponse) } } return allCodes } -const getMissingNode = (key: string, node: Hierarchy, codes: Map>) => { - const code = codes.get(key) ?? null +const getMissingNode = (id: string, node: Hierarchy, codes: Map>) => { + const code = codes.get(id) ?? 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) + 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 buildHierarchy = ( +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: Hierarchy[], + selected: Map>, mode: Mode ) => { const buildBranch = ( node: Hierarchy, + system: string, path: [string, InfiniteMap], codes: Map>, selected: Map>, mode: Mode ) => { - const [key, nextPath] = path - node = getMissingNode(key, node, codes) + 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) { 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) + 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 || mode === Mode.SELECT_ALL) node.status = SelectedStatus.SELECTED - if (mode === Mode.UNSELECT || mode === Mode.UNSELECT_ALL) node.status = SelectedStatus.NOT_SELECTED + 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(key)) { + 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) + const paths = getPaths(baseTree, endCodes) + let uniquePaths = getUniquePath(paths) + if (mode === Mode.SELECT || mode === Mode.UNSELECT) uniquePaths = getPathsForSelection(uniquePaths, endCodes) if (mode === Mode.INIT) baseTree = [] + if (mode === Mode.SELECT_ALL) mode = Mode.SELECT + if (mode === Mode.UNSELECT_ALL) mode = Mode.UNSELECT 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) + const branch = buildBranch(baseTree[index] || null, system, [key, value], codes, selected, mode) if (branch && index > -1) baseTree[index] = branch - else baseTree.push(branch) + else if (index === -1) baseTree.push(branch) } return [...baseTree] } -export const getHierarchyDisplay = (defaultLevels: Hierarchy[], tree: Hierarchy[]) => { +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 getDisplayFromTree = (toDisplay: 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' } + if (toDisplay.length && tree.length) + branches = toDisplay.map((item) => { + let path = item.above_levels_ids ? [...getAboveLevelsWithRights(item, tree), ...[item.id]] : [item.id] + path = updateUnknownChapter(tree, path) + return findBranch(path, tree) }) return branches } +export const getDisplayFromTrees = ( + toDisplay: Hierarchy[], + trees: Map[]> +) => { + const branches: Hierarchy[] = [] + toDisplay.forEach((node) => { + const currentTree = trees.get(node.system) + if (currentTree) { + const foundNode = getDisplayFromTree([node], currentTree)[0] + branches.push(foundNode) + } + }) + return branches +} + const findBranch = (path: string[], tree: Hierarchy[]): Hierarchy => { - let branch: Hierarchy = { id: 'empty' } as Hierarchy + let branch: Hierarchy = { id: `${path}-empty`, label: `ERROR | ${path}---------------` } as Hierarchy< + T, + string + > 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) - } + } else if (next && next.subItems) branch = findBranch(path.slice(1), next.subItems) return branch } -const getPaths = (baseTree: Hierarchy[], endCodes: Hierarchy[], selectAll: boolean) => { +const updateUnknownChapter = (baseTree: Hierarchy[], path: string[]) => { + const unknownChapterFound = baseTree[0]?.subItems?.find((chapter) => chapter.id === UNKOWN_HIERARCHY_CHAPTER) + const childrenIds = unknownChapterFound?.inferior_levels_ids.split(',') || [] + if (path?.[1] !== UNKOWN_HIERARCHY_CHAPTER && childrenIds.includes(path?.[1])) + path.splice(1, 0, UNKOWN_HIERARCHY_CHAPTER) + return path +} + +const getPaths = (baseTree: Hierarchy[], endCodes: Hierarchy[]) => { 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 - }) - } - } + paths = paths.map((path) => updateUnknownChapter(baseTree, path)) return paths } @@ -183,7 +304,16 @@ const getUniquePath = (paths: string[][]): InfiniteMap => { return tree } -const updateBranchStatus = (node: Hierarchy, status: SelectedStatus | undefined) => { +const getPathsForSelection = (paths: InfiniteMap, endCodes: Hierarchy[]): InfiniteMap => { + const codesMap = mapHierarchyToMap(endCodes) + for (const [key, values] of paths) { + if (codesMap.get(key)) paths.set(key, new Map()) + else getPathsForSelection(values, endCodes) + } + return paths +} + +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 @@ -200,14 +330,28 @@ export const getItemSelectedStatus = (item: Hierarchy): SelectedStat 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) +export const getSelectedCodesFromTree = (tree: Hierarchy[]) => { + const get = (node: Hierarchy, selectedCodes: Hierarchy[]) => { + if (node.status === SelectedStatus.INDETERMINATE || node.id === UNKOWN_HIERARCHY_CHAPTER) + node.subItems?.forEach((subItem) => get(subItem, selectedCodes)) + if (node.status === SelectedStatus.SELECTED && node.id !== UNKOWN_HIERARCHY_CHAPTER) selectedCodes.push(node) return selectedCodes } - const selectedCodes = list.flatMap((hierarchy) => get(hierarchy, [])) + const selectedCodes = mapHierarchyToMap(tree.flatMap((hierarchy) => get(hierarchy, []))) + return selectedCodes +} + +export const getSelectedCodesFromTrees = ( + trees: Map[]>, + prevCodes: Codes, + system?: string +) => { + const selectedCodes: Codes> = new Map() + trees.forEach((tree, key) => { + const isFound = prevCodes.get(key) + if (system !== key && isFound && isFound.get(HIERARCHY_ROOT)) selectedCodes.set(key, isFound) + else selectedCodes.set(key, getSelectedCodesFromTree(tree)) + }) return selectedCodes } @@ -221,13 +365,26 @@ const getAboveLevelsWithRights = (item: Hierarchy, baseTree: Hiera } 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) => - [...getAboveLevelsWithRights(item, baseTree)].filter((item) => item && item !== 'null' && item !== 'undefined') + (item?.inferior_levels_ids || '').split(',').filter((level) => level && level !== 'null' && level !== 'undefined') ) } -const getInferiorLevels = (hierarchy: Hierarchy[]) => { - return hierarchy - .flatMap((item) => (item.inferior_levels_ids || '').split(',')) - .filter((item) => item && item !== 'null' && item !== '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..369e129cc --- /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 +} diff --git a/src/utils/mappers.ts b/src/utils/mappers.ts index 9dbed36be..59219ceaa 100644 --- a/src/utils/mappers.ts +++ b/src/utils/mappers.ts @@ -1,4 +1,4 @@ -import { ScopeElement, SimpleCodeType } from 'types' +import { CriteriaItemDataCache, ScopeElement, SimpleCodeType } from 'types' import { DocumentAttachmentMethod, DurationRangeType, @@ -24,12 +24,23 @@ import { SelectedCriteriaTypesWithAdvancedInputs, EncounterDataType, EncounterParamsKeys, - ObservationParamsKeys + ObservationParamsKeys, + CriteriaDataKey } from 'types/requestCriterias' import { comparatorToFilter, parseOccurence } from './valueComparator' import services from 'services/aphp' import extractFilterParams, { FhirFilterValue } from './fhirFilterParser' -import { Hierarchy } from 'types/hierarchy' +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 +} const searchReducer = (accumulator: string, currentValue: string): string => accumulator || !!accumulator === false ? `${accumulator},${currentValue}` : currentValue || accumulator @@ -39,18 +50,10 @@ 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)).join(',')}` return filter } return '' @@ -58,13 +61,15 @@ 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('|') ? { system: value.split('|')?.[0], id: value.split('|')?.[1] } : { id: value } + ) if (newArray) { currentCriterion[filterName] = currentCriterion ? [...currentCriterion[filterName], ...newArray] : newArray } } -export const buildEncounterServiceFilter = (criterion?: Hierarchy[]) => { +export const buildEncounterServiceFilter = (criterion?: LabelObject[]) => { return criterion && criterion.length > 0 ? `${criterion.map((item) => item.id).reduce(searchReducer)}` : '' } @@ -123,8 +128,8 @@ export const unbuildSearchFilter = (value: string | null) => { export const buildObservationValueFilter = (criterion: ObservationDataType, fhirKey: string) => { const valueComparatorFilter = comparatorToFilter(criterion.valueComparator) + const filter = `${fhirKey}=le0,ge0` if ( - criterion.isLeaf && criterion.code && criterion.code.length === 1 && criterion.valueComparator && @@ -136,7 +141,7 @@ export const buildObservationValueFilter = (criterion: ObservationDataType, fhir return `${fhirKey}=${valueComparatorFilter}${criterion.searchByValue[0]}` } } - return `${fhirKey}=le0,ge0` + return filter } export const unbuildObservationValueFilter = (filters: string[][], currentCriterion: ObservationDataType) => { @@ -382,3 +387,36 @@ export const unbuildQuestionnaireFilters = ( export const filtersBuilders = (fhirKey: string, value?: string) => { return value ? `${fhirKey}=${value}` : '' } + +export const mappingCriteria = ( + criteriaToMap: LabelObject[] | null, + key: CriteriaDataKey, + mapping: CriteriaItemDataCache +) => { + if (criteriaToMap) { + return criteriaToMap.map((criteria) => { + const mappedCriteria = mapping.data?.[key]?.find((c: LabelObject) => c?.id === criteria?.id) + return mappedCriteria + }) + } else { + return [] + } +} + +export const mappingHierarchyCriteria = ( + criteriaToMap: LabelObject[] | null, + key: CriteriaDataKey, + mapping: CriteriaItemDataCache +) => { + if (criteriaToMap) { + return criteriaToMap.map((criteria) => { + const mappedCriteria = mapping.data?.[key]?.find((c: LabelObject) => { + if (criteria.system) return c?.id === criteria?.id && c?.system === criteria?.system + else return c?.id === criteria?.id + }) + return mappedCriteria + }) + } else { + return [] + } +} 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 092f600ef..04d363ea7 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,17 +58,28 @@ const getLabelFromCriteriaObject = ( ) => { const criterionData = criteriaState.cache.find((criteriaCache) => criteriaCache.criteriaType === resourceType)?.data if (criterionData === null || values === null) return '' - - const criterion = criterionData?.[name] || [] - if (criterion !== 'loading') { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const removeDuplicates = (array: any[], key: string) => { - return array.filter((obj, index, self) => index === self.findIndex((el) => el[key] === obj[key])) - } - const labels = removeDuplicates(criterion, 'id') - .filter((obj) => values.map((value) => value.id).includes(obj.id)) - .map((obj: LabelObject) => `${displaySystem(obj.system)} ${obj.label}`) - + const cache = criterionData?.[name] || [] + if (cache !== 'loading') { + const labels = values + .map((code) => + cache.find((cacheCode: any) => { + if (cacheCode.id === code.id) { + if ( + (name === CriteriaDataKey.MEDICATION_DATA || + name === CriteriaDataKey.BIOLOGY_DATA || + name === CriteriaDataKey.CIM_10_DIAGNOSTIC || + name === CriteriaDataKey.GHM_DATA || + name === CriteriaDataKey.CCAM_DATA) && + code.system + ) + return cacheCode.system === code.system + else return true + } + return false + }) + ) + .filter((e) => e) + .map((found: LabelObject) => getFullLabelFromCode(found)) const tooltipTitle = labels.join(' - ') return ( @@ -80,7 +91,7 @@ const getLabelFromCriteriaObject = ( } } -const getLabelFromName = (values: Hierarchy[]) => { +const getLabelFromName = (values: Hierarchy[]) => { const labels = values.map((value) => `${value.source_value} - ${value.name}`).join(' - ') return labels } 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/tabsUtils.ts b/src/utils/tabsUtils.ts index 13eb15db3..4b24bf907 100644 --- a/src/utils/tabsUtils.ts +++ b/src/utils/tabsUtils.ts @@ -1,5 +1,5 @@ -import { medicationTabs } from 'components/Patient/PatientMedication/PatientMedication' -import { PMSITabs } from 'components/Patient/PatientPMSI/PatientPMSI' +import { medicationTabs } from 'components/Patient/PatientMedication' +import { PMSITabs } from 'components/Patient/PatientPMSI' import { PMSILabel } from 'types/patient' import { MedicationLabel, ResourceType } from 'types/requestCriterias' diff --git a/src/utils/valueSets.ts b/src/utils/valueSets.ts new file mode 100644 index 000000000..679e6ba9d --- /dev/null +++ b/src/utils/valueSets.ts @@ -0,0 +1,39 @@ +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] + return isFound?.joinDisplayWithCode +} + +export const isDisplayedWithSystem = (system: string) => { + const isFound = getValueSetsFromSystems([system])?.[0] + return isFound?.joinDisplayWithSystem +} + +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 6d2af6f45..d91765ecb 100644 --- a/src/views/Dashboard/Dashboard.tsx +++ b/src/views/Dashboard/index.tsx @@ -3,8 +3,8 @@ import { Link, useLocation, useParams, useSearchParams } 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 95% rename from src/views/Patient/Patient.tsx rename to src/views/Patient/index.tsx index 65de76973..a1a49ad40 100644 --- a/src/views/Patient/Patient.tsx +++ b/src/views/Patient/index.tsx @@ -3,15 +3,15 @@ import { Link, useLocation, useParams, useSearchParams } from 'react-router-dom' import { IconButton, Grid, Tabs, Tab, CircularProgress } from '@mui/material' import ChevronLeftIcon from '@mui/icons-material/ChevronLeft' import PatientNotExist from 'components/ErrorView/PatientNotExist' -import PatientDocs from 'components/Patient/PatientDocs/PatientDocs' +import PatientDocs from 'components/Patient/PatientDocs' 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 PatientBiology from 'components/Patient/PatientBiology/PatientBiology' -import PatientImaging from 'components/Patient/PatientImaging/PatientImaging' -import PatientPMSI from 'components/Patient/PatientPMSI/PatientPMSI' +import PatientMedication from 'components/Patient/PatientMedication' +import PatientBiology from 'components/Patient/PatientBiology' +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/Scope/CareSiteView.tsx b/src/views/Scope/CareSiteView.tsx index 984c2d1e8..beefa71f7 100644 --- a/src/views/Scope/CareSiteView.tsx +++ b/src/views/Scope/CareSiteView.tsx @@ -1,11 +1,10 @@ import React, { useEffect, useState } from 'react' -import { Button, Grid } from '@mui/material' +import { Button, Grid, Typography } from '@mui/material' import useStyles from './styles' import { useNavigate } from 'react-router-dom' import { useAppDispatch, useAppSelector } from 'state' import { closeAllOpenedPopulation } from 'state/scope' import { ScopeElement } from 'types' -import Typography from '@mui/material/Typography' import ScopeTree from 'components/ScopeTree' import { Hierarchy } from 'types/hierarchy' import { SourceType } from 'types/scope' @@ -15,7 +14,7 @@ const CareSiteView = () => { const navigate = useNavigate() const dispatch = useAppDispatch() const population = useAppSelector((state) => state.scope.rights) - const [selectedCodes, setSelectedCodes] = useState[]>([]) + const [selectedCodes, setSelectedCodes] = useState[]>([]) const open = useAppSelector((state) => state.drawer) const handleNavigation = () => { @@ -31,34 +30,28 @@ const CareSiteView = () => { return ( - - - - Explorer un périmètre - - setSelectedCodes(items)} - sourceType={SourceType.ALL} - /> - + + + Explorer un périmètre + + setSelectedCodes(items)} + sourceType={SourceType.ALL} + sx={{ backgroundColor: '#E6F1FD' }} + /> - +