diff --git a/src/__tests__/utilsFunction/hierarchy.test.ts b/src/__tests__/utilsFunction/hierarchy.test.ts index e6b76d2a0..9751ecc5c 100644 --- a/src/__tests__/utilsFunction/hierarchy.test.ts +++ b/src/__tests__/utilsFunction/hierarchy.test.ts @@ -5,7 +5,7 @@ import { getHierarchyRootCodes, cleanNode, mapHierarchyToMap, - getMissingCodesWithSystems, + getMissingCodesWithValueSets, getMissingCodes, buildMultipleTrees, buildTree, @@ -136,9 +136,9 @@ describe('Utility Functions', () => { ['system1', [{ id: 'root1', label: 'Root1', system: 'system1' }]], ['system2', [{ id: 'root1', label: 'Root1', system: 'system2', inferior_levels_ids: 'root3' }]] ]) - const groupBySystem = [ + const groupByValueSet = [ { - system: 'system2', + valueSetUrl: 'system2', codes: [{ id: 'root4', label: 'Root4', system: 'system2', above_levels_ids: 'root1,root3' }] } ] @@ -154,7 +154,7 @@ describe('Utility Functions', () => { .mockResolvedValue([ { id: 'root3', label: 'Root3', system: 'system2', above_levels_ids: 'root1', inferior_levels_ids: 'root4' } ]) - const result = await getMissingCodesWithSystems(trees, groupBySystem, codes, fetchHandler) + const result = await getMissingCodesWithValueSets(trees, groupByValueSet, 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( @@ -233,6 +233,7 @@ describe('Utility Functions', () => { above_levels_ids: '', inferior_levels_ids: '', system, + valueSetUrl: system, status }) }) @@ -464,9 +465,9 @@ describe('Utility Functions', () => { describe('buildMultipleTrees', () => { it('should build multiple trees according to different systems', () => { - const groupBySystem: GroupedBySystem[] = [ + const groupByValueSet: Array<{ valueSetUrl: string; codes: any[] }> = [ { - system: 'system1', + valueSetUrl: 'system1', codes: [ { id: HIERARCHY_ROOT, @@ -476,7 +477,7 @@ describe('Utility Functions', () => { ] }, { - system: 'system2', + valueSetUrl: 'system2', codes: [ { id: 'code1', @@ -560,7 +561,7 @@ describe('Utility Functions', () => { ] ]) const mode = Mode.INIT - const result = buildMultipleTrees(baseTrees, groupBySystem, codes, new Map(), mode) + const result = buildMultipleTrees(baseTrees, groupByValueSet, codes, new Map(), mode) expect(result.get('system1')).toEqual([ { id: HIERARCHY_ROOT, diff --git a/src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/CriteriaForm/mappers/chipDisplayMapper.tsx b/src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/CriteriaForm/mappers/chipDisplayMapper.tsx index ca576d815..cd8a2eddd 100644 --- a/src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/CriteriaForm/mappers/chipDisplayMapper.tsx +++ b/src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/CriteriaForm/mappers/chipDisplayMapper.tsx @@ -23,6 +23,7 @@ import allDocTypes from 'assets/docTypes.json' import moment from 'moment' import { getDurationRangeLabel } from 'utils/age' import { getConfig } from 'config' +import { getValueSetFromCodeSystem } from 'utils/valueSets' /************************************************************************************/ /* Criteria Form Item Chip Display */ @@ -137,9 +138,22 @@ const getLabelsForCodeSearchItem = ( ): LabelObject[] => { return val .map((value) => { + let cacheKey: string | undefined + + if (value.system) { + // value.system is a CodeSystem URL, we need to find the corresponding ValueSet URL + const valueSetUrl = getValueSetFromCodeSystem(value.system) + if (valueSetUrl) { + cacheKey = valueSetUrl + } else { + // Fallback: try to find in any of the configured valueSets + cacheKey = item.valueSetsInfo.find((valueset) => valueSets.cache[valueset.url])?.url + } + } + return ( - (value.system - ? valueSets.cache[value.system] + (cacheKey + ? valueSets.cache[cacheKey] : item.valueSetsInfo.flatMap((valueset) => valueSets.cache[valueset.url])) || [] ).find((code) => code && code.id === value.id) as LabelObject }) @@ -194,7 +208,12 @@ const chipFromCodeSearch = ( // TODO refacto this to be more generic using config const displaySystem = (system?: string) => { - switch (system) { + if (!system) return '' + + // system might be a CodeSystem URL, so we need to find the corresponding ValueSet URL first + const valueSetUrl = getValueSetFromCodeSystem(system) || system + + switch (valueSetUrl) { case getConfig().features.medication.valueSets.medicationAtc.url: return `${getConfig().features.medication.valueSets.medicationAtc.title}: ` case getConfig().features.medication.valueSets.medicationUcd.url: diff --git a/src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/forms/BiologyForm.ts b/src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/forms/BiologyForm.ts index 615229049..92753679b 100644 --- a/src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/forms/BiologyForm.ts +++ b/src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/forms/BiologyForm.ts @@ -10,7 +10,7 @@ import { import { SourceType } from 'types/scope' import { getConfig } from 'config' import { BiologyStatus } from 'types' -import { getValueSetsFromSystems } from 'utils/valueSets' +import { getValueSetsByUrls } from 'utils/valueSets' import { FhirItem } from 'types/valueSet' import { Hierarchy } from 'types/hierarchy' @@ -80,7 +80,7 @@ export const form: () => CriteriaForm = () => ({ type: 'codeSearch', label: 'Sélectionner les codes', checkIsLeaf: true, - valueSetsInfo: getValueSetsFromSystems([ + valueSetsInfo: getValueSetsByUrls([ getConfig().features.observation.valueSets.biologyHierarchyAnabio.url, getConfig().features.observation.valueSets.biologyHierarchyLoinc.url ]), @@ -88,7 +88,10 @@ export const form: () => CriteriaForm = () => ({ buildInfo: { fhirKey: ObservationParamsKeys.CODE, buildMethodExtraArgs: [ - { type: 'string', value: getConfig().features.observation.valueSets.biologyHierarchyAnabio.url }, + { + type: 'string', + value: getConfig().features.observation.valueSets.biologyHierarchyAnabio.codeSystemUrls?.at(0) || '' + }, { type: 'boolean', value: true } ] } diff --git a/src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/forms/CCAMForm.ts b/src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/forms/CCAMForm.ts index 4e18fbfe0..6e36bcc43 100644 --- a/src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/forms/CCAMForm.ts +++ b/src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/forms/CCAMForm.ts @@ -8,7 +8,7 @@ import { } from '../CriteriaForm/types' import { SourceType } from 'types/scope' import { getConfig } from 'config' -import { getValueSetsFromSystems } from 'utils/valueSets' +import { getValueSetsByUrls } from 'utils/valueSets' import { Hierarchy } from 'types/hierarchy' import { FhirItem } from 'types/valueSet' @@ -89,12 +89,15 @@ export const form: () => CriteriaForm = () => ({ valueKey: 'code', type: 'codeSearch', label: "Sélectionner les codes d'actes CCAM", - valueSetsInfo: getValueSetsFromSystems([getConfig().features.procedure.valueSets.procedureHierarchy.url]), + valueSetsInfo: getValueSetsByUrls([getConfig().features.procedure.valueSets.procedureHierarchy.url]), noOptionsText: 'Veuillez entrer un code ou un acte CCAM', buildInfo: { fhirKey: ProcedureParamsKeys.CODE, buildMethodExtraArgs: [ - { type: 'string', value: getConfig().features.procedure.valueSets.procedureHierarchy.url } + { + type: 'string', + value: getConfig().features.procedure.valueSets.procedureHierarchy.codeSystemUrls?.at(0) || '' + } ], chipDisplayMethodExtraArgs: [ { type: 'string', value: '' }, diff --git a/src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/forms/Cim10Form.ts b/src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/forms/Cim10Form.ts index 278baf513..de5266021 100644 --- a/src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/forms/Cim10Form.ts +++ b/src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/forms/Cim10Form.ts @@ -8,7 +8,7 @@ import { } from '../CriteriaForm/types' import { SourceType } from 'types/scope' import { getConfig } from 'config' -import { getValueSetsFromSystems } from 'utils/valueSets' +import { getValueSetsByUrls } from 'utils/valueSets' import { FhirItem } from 'types/valueSet' import { Hierarchy } from 'types/hierarchy' @@ -90,13 +90,16 @@ export const form: () => CriteriaForm = () => ({ { valueKey: 'code', type: 'codeSearch', - valueSetsInfo: getValueSetsFromSystems([getConfig().features.condition.valueSets.conditionHierarchy.url]), + valueSetsInfo: getValueSetsByUrls([getConfig().features.condition.valueSets.conditionHierarchy.url]), noOptionsText: 'Veuillez entrer un code ou un diagnostic CIM10', label: 'Sélectionner les codes CIM10', buildInfo: { fhirKey: ConditionParamsKeys.CODE, buildMethodExtraArgs: [ - { type: 'string', value: getConfig().features.condition.valueSets.conditionHierarchy.url } + { + type: 'string', + value: getConfig().features.condition.valueSets.conditionHierarchy.codeSystemUrls?.at(0) || '' + } ], chipDisplayMethodExtraArgs: [ { type: 'string', value: '' }, diff --git a/src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/forms/GHMForm.tsx b/src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/forms/GHMForm.tsx index f8ecf6ea0..94917dd3b 100644 --- a/src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/forms/GHMForm.tsx +++ b/src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/forms/GHMForm.tsx @@ -10,7 +10,7 @@ import { import { Link } from '@mui/material' import { SourceType } from 'types/scope' import { getConfig } from 'config' -import { getValueSetsFromSystems } from 'utils/valueSets' +import { getValueSetsByUrls } from 'utils/valueSets' import { Hierarchy } from 'types/hierarchy' import { FhirItem } from 'types/valueSet' @@ -74,12 +74,14 @@ export const form: () => CriteriaForm = () => ({ { valueKey: 'code', type: 'codeSearch', - valueSetsInfo: getValueSetsFromSystems([getConfig().features.claim.valueSets.claimHierarchy.url]), + valueSetsInfo: getValueSetsByUrls([getConfig().features.claim.valueSets.claimHierarchy.url]), noOptionsText: 'Aucun GHM trouvé', label: 'Sélectionner les codes GHM', buildInfo: { fhirKey: ClaimParamsKeys.CODE, - buildMethodExtraArgs: [{ type: 'string', value: getConfig().features.claim.valueSets.claimHierarchy.url }], + buildMethodExtraArgs: [ + { type: 'string', value: getConfig().features.claim.valueSets.claimHierarchy.codeSystemUrls?.at(0) || '' } + ], chipDisplayMethodExtraArgs: [ { type: 'string', value: '' }, { type: 'boolean', value: true } diff --git a/src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/forms/MedicationForm.ts b/src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/forms/MedicationForm.ts index a60fab5b1..1a2af9757 100644 --- a/src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/forms/MedicationForm.ts +++ b/src/components/CreationCohort/DiagramView/components/LogicalOperator/components/CriteriaRightPanel/forms/MedicationForm.ts @@ -15,7 +15,7 @@ import { } from '../CriteriaForm/types' import { SourceType } from 'types/scope' import { getConfig } from 'config' -import { getValueSetsFromSystems } from 'utils/valueSets' +import { getValueSetsByUrls } from 'utils/valueSets' import { Hierarchy } from 'types/hierarchy' import { FhirItem } from 'types/valueSet' import { hasSearchParam } from 'services/aphp/serviceFhirConfig' @@ -94,14 +94,17 @@ export const form: () => CriteriaForm = () => ({ type: 'codeSearch', label: 'Sélectionner les codes', noOptionsText: 'Veuillez entrer un code de médicament', - valueSetsInfo: getValueSetsFromSystems([ + valueSetsInfo: getValueSetsByUrls([ getConfig().features.medication.valueSets.medicationAtc.url, getConfig().features.medication.valueSets.medicationUcd.url ]), buildInfo: { fhirKey: PrescriptionParamsKeys.CODE, buildMethodExtraArgs: [ - { type: 'string', value: getConfig().features.medication.valueSets.medicationAtc.url }, + { + type: 'string', + value: getConfig().features.medication.valueSets.medicationAtc.codeSystemUrls?.at(0) || '' + }, { type: 'boolean', value: true } ], chipDisplayMethodExtraArgs: [ diff --git a/src/components/ExplorationBoard/config/biology.ts b/src/components/ExplorationBoard/config/biology.ts index 1922233d8..f13807d13 100644 --- a/src/components/ExplorationBoard/config/biology.ts +++ b/src/components/ExplorationBoard/config/biology.ts @@ -27,7 +27,7 @@ import { narrowSearchCriterias, resolveAdditionalInfos } from 'utils/exploration' -import { getValueSetsFromSystems } from 'utils/valueSets' +import { getValueSetsByUrls } from 'utils/valueSets' const fetchAdditionalInfos = async (additionalInfo: AdditionalInfo): Promise => { const fetchersMap: Record Promise> = { @@ -37,7 +37,7 @@ const fetchAdditionalInfos = async (additionalInfo: AdditionalInfo): Promise => { const config = getConfig().features @@ -48,7 +48,7 @@ const fetchAdditionalInfos = async (additionalInfo: AdditionalInfo): Promise => { const fetchersMap: Record Promise> = { @@ -152,7 +152,7 @@ export const conditionConfig = ( narrowSearchCriterias(deidentified, searchCriterias, !!patient, [], ['searchBy']), fetchAdditionalInfos: async (infos) => { const _infos = await fetchAdditionalInfos(infos) - const references: Reference[] = getValueSetsFromSystems([ + const references: Reference[] = getValueSetsByUrls([ getConfig().features.condition.valueSets.conditionHierarchy.url ]) const sourceType = SourceType.CIM10 @@ -183,7 +183,7 @@ export const procedureConfig = ( narrowSearchCriterias(deidentified, searchCriterias, !!patient, ['diagnosticTypes'], ['searchBy']), fetchAdditionalInfos: async (infos) => { const _infos = await fetchAdditionalInfos(infos) - const references: Reference[] = getValueSetsFromSystems([ + const references: Reference[] = getValueSetsByUrls([ getConfig().features.procedure.valueSets.procedureHierarchy.url ]) const sourceType = SourceType.CCAM @@ -213,7 +213,7 @@ export const claimConfig = ( narrowSearchCriterias(deidentified, searchCriterias, !!patient, ['diagnosticTypes', 'source'], ['searchBy']), fetchAdditionalInfos: async (infos) => { const _infos = await fetchAdditionalInfos(infos) - const references: Reference[] = getValueSetsFromSystems([getConfig().features.claim.valueSets.claimHierarchy.url]) + const references: Reference[] = getValueSetsByUrls([getConfig().features.claim.valueSets.claimHierarchy.url]) const sourceType = SourceType.GHM return { ..._infos, references, sourceType } }, diff --git a/src/config.tsx b/src/config.tsx index 287506448..b9273a969 100644 --- a/src/config.tsx +++ b/src/config.tsx @@ -7,7 +7,8 @@ import { birthStatusData, booleanFieldsData, booleanOpenChoiceFieldsData, vmeDat import { DeepPartial } from 'redux' type ValueSetConfig = { - url: string + url: string // ValueSet URL (for searching/listing valuesets) + codeSystemUrls?: string[] // Array of CodeSystem URLs (for individual codes within valuesets) title?: string data?: LabelObject[] } diff --git a/src/data/valueSets.ts b/src/data/valueSets.ts index 14b23a7df..f55b8ea10 100644 --- a/src/data/valueSets.ts +++ b/src/data/valueSets.ts @@ -9,6 +9,7 @@ export const getReferences = (config: Readonly) => [ label: ReferencesLabel.ATC, standard: true, url: config.features.medication.valueSets.medicationAtc.url, + codeSystemUrls: config.features.medication.valueSets.medicationAtc.codeSystemUrls, checked: true, isHierarchy: true, joinDisplayWithCode: true, @@ -21,6 +22,7 @@ export const getReferences = (config: Readonly) => [ label: ReferencesLabel.UCD, standard: true, url: config.features.medication.valueSets.medicationUcd.url, + codeSystemUrls: config.features.medication.valueSets.medicationUcd.codeSystemUrls, checked: true, joinDisplayWithCode: true, joinDisplayWithSystem: true, @@ -33,6 +35,7 @@ export const getReferences = (config: Readonly) => [ label: ReferencesLabel.ANABIO, standard: true, url: config.features.observation.valueSets.biologyHierarchyAnabio.url, + codeSystemUrls: config.features.observation.valueSets.biologyHierarchyAnabio.codeSystemUrls, checked: true, isHierarchy: true, joinDisplayWithCode: false, @@ -54,6 +57,7 @@ export const getReferences = (config: Readonly) => [ label: ReferencesLabel.LOINC, standard: true, url: config.features.observation.valueSets.biologyHierarchyLoinc.url, + codeSystemUrls: config.features.observation.valueSets.biologyHierarchyLoinc.codeSystemUrls, checked: true, isHierarchy: false, joinDisplayWithCode: true, @@ -66,6 +70,7 @@ export const getReferences = (config: Readonly) => [ label: ReferencesLabel.CCAM, standard: true, url: config.features.procedure.valueSets.procedureHierarchy.url, + codeSystemUrls: config.features.procedure.valueSets.procedureHierarchy.codeSystemUrls, checked: true, isHierarchy: true, joinDisplayWithCode: true, @@ -78,6 +83,7 @@ export const getReferences = (config: Readonly) => [ label: ReferencesLabel.CIM10, standard: true, url: config.features.condition.valueSets.conditionHierarchy.url, + codeSystemUrls: config.features.condition.valueSets.conditionHierarchy.codeSystemUrls, checked: true, isHierarchy: true, joinDisplayWithSystem: false, @@ -90,6 +96,7 @@ export const getReferences = (config: Readonly) => [ label: ReferencesLabel.GHM, standard: true, url: config.features.claim.valueSets.claimHierarchy.url, + codeSystemUrls: config.features.claim.valueSets.claimHierarchy.codeSystemUrls, checked: true, isHierarchy: true, joinDisplayWithCode: true, diff --git a/src/hooks/hierarchy/useHierarchy.ts b/src/hooks/hierarchy/useHierarchy.ts index 98f7cb67f..2d5071f2e 100644 --- a/src/hooks/hierarchy/useHierarchy.ts +++ b/src/hooks/hierarchy/useHierarchy.ts @@ -4,8 +4,8 @@ import { getDisplayFromTree, getDisplayFromTrees, getMissingCodes, - getMissingCodesWithSystems, - groupBySystem, + getMissingCodesWithValueSets, + groupByValueSet, getHierarchyRootCodes, mapHierarchyToMap, getSelectedCodesFromTrees, @@ -21,19 +21,19 @@ 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. + * @param {(ids: string, valueSetUrl: string) => Promise[]>} fetchHandler - A callback function that returns fetched hierarchies. */ export const useHierarchy = ( selectedNodes: Hierarchy[], fetchedCodes: Codes>, onCache: (codes: Codes>) => void, - fetchHandler: (ids: string, system: string) => Promise[]> + fetchHandler: (ids: string, valueSetUrl: string) => Promise[]> ) => { 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)])) + new Map(groupByValueSet(selectedNodes).map((item) => [item.valueSetUrl, mapHierarchyToMap(item.codes)])) ) const [codes, setCodes] = useState>>(fetchedCodes) @@ -55,7 +55,7 @@ export const useHierarchy = ( const initTrees = async ( initHandlers: { - system: string + valueSetUrl: string fetchBaseTree: () => Promise>> }[] ) => { @@ -64,16 +64,23 @@ export const useHierarchy = ( 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 currentSelected = selectedCodes.get(handler.valueSetUrl) || 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 currentCodes = codes.get(handler.valueSetUrl) || new Map() + const newCodes = await getMissingCodes( + baseTree, + currentCodes, + toFind, + handler.valueSetUrl, + Mode.INIT, + fetchHandler + ) + const newTree = buildTree(baseTree, handler.valueSetUrl, 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)])) + newTrees.set(handler.valueSetUrl, newTree) + newHierarchies.set(handler.valueSetUrl, { tree: newHierarchy, count, page: 1, system: handler.valueSetUrl }) + allCodes.set(handler.valueSetUrl, new Map([...newCodes, ...getHierarchyRootCodes(newTree)])) } setTrees(newTrees) setHierarchies(newHierarchies) @@ -85,9 +92,9 @@ export const useHierarchy = ( 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) + const byValueSet = groupByValueSet(endCodes) + const newCodes = await getMissingCodesWithValueSets(trees, byValueSet, codes, fetchHandler) + const newTrees = buildMultipleTrees(trees, byValueSet, newCodes, selectedCodes, Mode.SEARCH) setCodes(newCodes) setTrees(newTrees) setLoadingStatus({ ...loadingStatus, search: LoadingStatus.SUCCESS }) @@ -118,47 +125,48 @@ export const useHierarchy = ( } 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 = '' + const byValueSet = groupByValueSet(nodes) + const newTrees = buildMultipleTrees(trees, byValueSet, codes, selectedCodes, toAdd ? Mode.SELECT : Mode.UNSELECT) + let valueSetUrl = '' if (mode === SearchMode.EXPLORATION) { - system = nodes?.[0].system - const current = hierarchies.get(system) || DEFAULT_HIERARCHY_INFO + valueSetUrl = nodes?.[0].valueSetUrl || nodes?.[0].system || '' + const current = hierarchies.get(valueSetUrl) || DEFAULT_HIERARCHY_INFO const newHierarchy = getDisplayFromTrees(current.tree, newTrees) - setHierarchies(replaceInMap(system, { ...current, tree: newHierarchy }, hierarchies)) + setHierarchies(replaceInMap(valueSetUrl, { ...current, tree: newHierarchy }, hierarchies)) } else { const newSearch = getDisplayFromTrees(searchResults.tree, newTrees) setSearchResults({ ...searchResults, tree: newSearch }) } - setSelectedCodes(getSelectedCodesFromTrees(newTrees, selectedCodes, system)) + setSelectedCodes(getSelectedCodesFromTrees(newTrees, selectedCodes, valueSetUrl)) setTrees(newTrees) } - const selectAll = (system: string, toAdd: boolean) => { - const nodes = trees.get(system) || [] - const currentHierarchy = hierarchies.get(system) || DEFAULT_HIERARCHY_INFO - const bySystem = groupBySystem(nodes) + const selectAll = (valueSetUrl: string, toAdd: boolean) => { + const nodes = trees.get(valueSetUrl) || [] + const currentHierarchy = hierarchies.get(valueSetUrl) || DEFAULT_HIERARCHY_INFO + const byValueSet = groupByValueSet(nodes) const mode = toAdd ? Mode.SELECT_ALL : Mode.UNSELECT_ALL - const newTrees = buildMultipleTrees(trees, bySystem, codes, selectedCodes, mode) + const newTrees = buildMultipleTrees(trees, byValueSet, 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)) + if (toAdd) root.set(HIERARCHY_ROOT, createHierarchyRoot(valueSetUrl)) + setSelectedCodes(replaceInMap(valueSetUrl, root, selectedCodes)) setTrees(newTrees) setSearchResults({ ...searchResults, tree: newSearch }) - setHierarchies(replaceInMap(system, { ...currentHierarchy, tree: newHierarchy }, hierarchies)) + setHierarchies(replaceInMap(valueSetUrl, { ...currentHierarchy, tree: newHierarchy }, hierarchies)) } const expand = async (node: Hierarchy) => { setLoadingStatus({ ...loadingStatus, expand: LoadingStatus.FETCHING }) - const hierarchyId = node.system + const hierarchyId = node.valueSetUrl || 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 newTree = buildTree(currentTree, hierarchyId, [node], newCodes, currentSelected, Mode.EXPAND) const newHierarchy = getDisplayFromTree(currentHierarchy.tree, newTree) setCodes(replaceInMap(hierarchyId, newCodes, codes)) setTrees(replaceInMap(hierarchyId, newTree, trees)) diff --git a/src/hooks/scopeTree/useScopeTree.ts b/src/hooks/scopeTree/useScopeTree.ts index 19136294f..d39f9248c 100644 --- a/src/hooks/scopeTree/useScopeTree.ts +++ b/src/hooks/scopeTree/useScopeTree.ts @@ -49,7 +49,7 @@ export const useScopeTree = ( useEffect(() => { initTrees([ - { system: System.ScopeTree, fetchBaseTree: async () => ({ results: baseTree, count: baseTree.length }) } + { valueSetUrl: System.ScopeTree, fetchBaseTree: async () => ({ results: baseTree, count: baseTree.length }) } ]) }, [baseTree]) diff --git a/src/hooks/valueSet/useSearchValueSet.ts b/src/hooks/valueSet/useSearchValueSet.ts index 5092c9f4b..f13862f1e 100644 --- a/src/hooks/valueSet/useSearchValueSet.ts +++ b/src/hooks/valueSet/useSearchValueSet.ts @@ -27,7 +27,7 @@ export const useSearchValueSet = (references: Reference[], selectedNodes: Hierar const controllerRef = useRef(null) const fetchChildren = useCallback( - async (ids: string, system: string) => (await getChildrenFromCodes(system, ids.split(','))).results, + async (ids: string, valueSetUrl: string) => (await getChildrenFromCodes(valueSetUrl, ids.split(','))).results, [] ) @@ -70,7 +70,8 @@ export const useSearchValueSet = (references: Reference[], selectedNodes: Hierar const isSelectionDisabled = useCallback( (node: Hierarchy) => { - const isAll = selectedCodes.get(node.system)?.get(HIERARCHY_ROOT) + const nodeKey = node.valueSetUrl || node.system + const isAll = selectedCodes.get(nodeKey)?.get(HIERARCHY_ROOT) if (mode === SearchMode.RESEARCH && isAll) return true else { const ref = explorationParameters.options.references.find((ref) => ref.checked) @@ -114,7 +115,7 @@ export const useSearchValueSet = (references: Reference[], selectedNodes: Hierar const initExploration = (references: Reference[]) => { const hierachyReferences = references.map((ref, index) => ({ ...ref, checked: index === 0 })) const initHandlers = references.map((ref) => ({ - system: ref.url, + valueSetUrl: ref.url, fetchBaseTree: () => fetchBaseTree(ref) })) explorationParameters.onChangeReferences(hierachyReferences) @@ -136,7 +137,8 @@ export const useSearchValueSet = (references: Reference[], selectedNodes: Hierar const handleDeleteSelectedCodes = (code: Hierarchy) => { const isRoot = code.id === HIERARCHY_ROOT - if (isRoot) selectAll(code.system, false) + const codeKey = code.valueSetUrl || code.system + if (isRoot) selectAll(codeKey, false) else select([code], false, SearchMode.EXPLORATION) } diff --git a/src/services/aphp/serviceValueSets.ts b/src/services/aphp/serviceValueSets.ts index 8f3f8f596..5209a768e 100644 --- a/src/services/aphp/serviceValueSets.ts +++ b/src/services/aphp/serviceValueSets.ts @@ -48,21 +48,24 @@ const mapAbandonedChildren = (children: Hierarchy[]) => { * @returns */ const mapFhirHierarchyToHierarchyWithLabelAndSystem = (fhirItem: FhirItem): Hierarchy => { - return { + const result = { id: fhirItem.id, label: fhirItem.label, system: fhirItem.system, + valueSetUrl: fhirItem.valueSetUrl, // Preserve ValueSet URL above_levels_ids: fhirItem.parentIds?.join(',') ?? '', inferior_levels_ids: fhirItem.childrenIds?.join(',') ?? '', statTotal: fhirItem.statTotal, statTotalUnique: fhirItem.statTotalUnique } + return result } const mapCodesToFhirItems = ( codes: ValueSetComposeIncludeConcept[], codeSystem: string, - codeInLabel: boolean + codeInLabel: boolean, + valueSetUrl?: string ): FhirItem[] => { return sortArray( codes.map((code) => ({ @@ -70,7 +73,8 @@ const mapCodesToFhirItems = ( label: codeInLabel ? `${code.code} - ${capitalizeFirstLetter(code.display)}` : capitalizeFirstLetter(code.display), - system: codeSystem + system: codeSystem, + valueSetUrl // Populate ValueSet URL for proper grouping })), 'label' ) @@ -100,7 +104,10 @@ const extractStats = (codeExtensions?: Extension[]) => { } } -const formatValuesetExpansion = (valueSetExpansion?: ValueSetExpansion): Back_API_Response> => { +const formatValuesetExpansion = ( + valueSetExpansion?: ValueSetExpansion, + valueSetUrl?: string +): Back_API_Response> => { const codeList: Array = valueSetExpansion?.contains?.map((code) => { const stats = code?.extension @@ -112,6 +119,7 @@ const formatValuesetExpansion = (valueSetExpansion?: ValueSetExpansion): Back_AP 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), + valueSetUrl, // Populate ValueSet URL for proper grouping statTotal: stats?.statTotal, statTotalUnique: stats?.statTotalUnique } @@ -152,13 +160,13 @@ export const fetchCodeSystem = async (codeSystem: string, signal?: AbortSignal): * You must then call getFhirCode on subItems to expand the hierarchy * For large code arrays (> config.core.maxParallelCodeSearchExpandCount), calls are batched * to prevent API limits and improve performance - * @param codeSystem the code system from which belongs the code + * @param valueSetUrl the ValueSet URL to expand codes from * @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, + valueSetUrl: string, codes: string[], signal?: AbortSignal ): Promise>> => { @@ -172,7 +180,7 @@ export const getChildrenFromCodes = async ( const maxBatchSize = getConfig().core.maxParallelCodeSearchExpandCount if (codes.length <= maxBatchSize) { - return await getChildrenFromCodesBatch(codeSystem, codes, signal) + return await getChildrenFromCodesBatch(valueSetUrl, codes, signal) } const batches: string[][] = [] @@ -180,7 +188,7 @@ export const getChildrenFromCodes = async ( batches.push(codes.slice(i, i + maxBatchSize)) } - const batchPromises = batches.map((batch) => getChildrenFromCodesBatch(codeSystem, batch, signal)) + const batchPromises = batches.map((batch) => getChildrenFromCodesBatch(valueSetUrl, batch, signal)) const batchResults = await Promise.all(batchPromises) @@ -198,13 +206,13 @@ export const getChildrenFromCodes = async ( /** * Internal helper function to process a batch of codes for hierarchy expansion - * @param codeSystem the code system from which belongs the code + * @param valueSetUrl the value set URL from which belongs the code * @param codes the codes to get the partial hierarchy from (should be <= maxParallelCodeSearchExpandCount) * @param signal the abort signal to cancel the request * @returns the partial hierarchy from the code batch */ const getChildrenFromCodesBatch = async ( - codeSystem: string, + valueSetUrl: string, codes: string[], signal?: AbortSignal ): Promise>> => { @@ -219,7 +227,7 @@ const getChildrenFromCodesBatch = async ( name: 'valueSet', resource: { resourceType: 'ValueSet', - url: codeSystem, + url: valueSetUrl, compose: { include: [ { @@ -241,14 +249,14 @@ const getChildrenFromCodesBatch = async ( const res = await apiFhir.post>(`/ValueSet/$expand`, JSON.stringify(json), { signal: signal }) - return formatValuesetExpansion(getApiResponseResourceOrThrow(res).expansion) + return formatValuesetExpansion(getApiResponseResourceOrThrow(res).expansion, valueSetUrl) } /** * 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 valueSetUrls the valueset urls to search in * @param search the search string * @param offset the offset * @param count the size of the result @@ -257,7 +265,7 @@ const getChildrenFromCodesBatch = async ( * @returns the partial hierarchy for the nodes matching the search string */ export const searchInValueSets = async ( - codeSystems: string[], + valueSetUrls: string[], search: string, offset?: number, count?: number, @@ -278,12 +286,12 @@ export const searchInValueSets = async ( const searchValue = search || HIERARCHY_ROOT try { const res = await apiFhir.get>( - `/ValueSet/$expand?url=${codeSystems.join(',')}&filter=${encodeURIComponent( + `/ValueSet/$expand?url=${valueSetUrls.join(',')}&filter=${encodeURIComponent( searchValue )}&excludeNested=false&_tag=text-search-rank&_tag=${LOW_TOLERANCE_TAG}${options}`, { signal } ) - const response = formatValuesetExpansion(getApiResponseResourceOrThrow(res).expansion) + const response = formatValuesetExpansion(getApiResponseResourceOrThrow(res).expansion, valueSetUrls[0]) response.results = mapAbandonedChildren(response.results) return response } catch (error) { @@ -296,18 +304,18 @@ export const searchInValueSets = async ( } /** - * Get the complete list of a specific code system - * @param codeSystem the code system to search in + * Get the complete list of a specific valueset + * @param valueSetUrl the ValueSet URL to get codes from * @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 + * @returns the complete list of the valueset */ export const getCodeList = async ( - codeSystem: string, + valueSetUrl: string, codeInLabel = false, signal?: AbortSignal ): Promise> => { - const res = await apiFhir.get>(`/ValueSet?reference=${codeSystem}`, { + const res = await apiFhir.get>(`/ValueSet?url=${valueSetUrl}`, { signal: signal }) const valueSetBundle = getApiResponseResourcesOrThrow(res) @@ -322,25 +330,46 @@ export const getCodeList = async ( } } const codeList = formatCodesFromValueSetReponse(valueSetBundle)[0] - const fhirItems = mapCodesToFhirItems(codeList, codeSystem, codeInLabel) + // TODO: Extract actual CodeSystem URL from response instead of using placeholder + const codeSystemUrl = valueSetBundle[0]?.compose?.include?.[0]?.system || 'unknown-codesystem' + const fhirItems = mapCodesToFhirItems(codeList, codeSystemUrl, codeInLabel, valueSetUrl) return { results: fhirItems, count: codeList.length } } +// Reverse lookup utility functions +// Note: These are now implemented in utils/valueSets.ts +// Keep these exports for backward compatibility +export { getValueSetFromCodeSystem } from 'utils/valueSets' + +export const getCodeSystemFromValueSet = (valueSetUrl: string): string[] | undefined => { + // Import locally to avoid circular dependency + // eslint-disable-next-line @typescript-eslint/no-var-requires + const { getReferences } = require('data/valueSets') + // eslint-disable-next-line @typescript-eslint/no-var-requires + const { getConfig } = require('config') + const references = getReferences(getConfig()) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const reference = references.find((ref: any) => ref.url === valueSetUrl) + return reference?.codeSystemUrls +} + export const getHierarchyRoots = async ( - codeSystem: string, + valueSetUrl: 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`, + `/ValueSet?only-roots=true&url=${valueSetUrl}&_sort=code`, { signal } ) const valueSetBundle = getApiResponseResourcesOrThrow(res) + // TODO: Extract actual CodeSystem URL from response instead of using placeholder + const codeSystemUrl = valueSetBundle[0]?.compose?.include?.[0]?.system || 'unknown-codesystem' const codeList = ( formatCodesFromValueSetReponse(valueSetBundle) .filter((valueSetPerSystem) => !!valueSetPerSystem) @@ -348,7 +377,8 @@ export const getHierarchyRoots = async ( .map((code) => ({ id: code.code, label: capitalizeFirstLetter(code.display), - system: codeSystem + system: codeSystemUrl, + valueSetUrl // Populate ValueSet URL for proper grouping })) as Hierarchy[] ).filter((code) => !filterOut(code)) @@ -360,16 +390,17 @@ export const getHierarchyRoots = async ( let subItems: Hierarchy[] | undefined = undefined if (toBeAdoptedCodes.length) { const unknownChaptersIds = toBeAdoptedCodes.map((code) => code.id) - const unknownChapters = (await getChildrenFromCodes(codeSystem, unknownChaptersIds)).results + const unknownChapters = (await getChildrenFromCodes(valueSetUrl, unknownChaptersIds)).results const unknownChapter: Hierarchy = { id: UNKOWN_HIERARCHY_CHAPTER, label: `${UNKOWN_HIERARCHY_CHAPTER}`, - system: codeSystem, + system: codeSystemUrl, + valueSetUrl, // Populate ValueSet URL for proper grouping above_levels_ids: HIERARCHY_ROOT, inferior_levels_ids: unknownChapters.map((code) => code.id).join(','), subItems: unknownChapters } - const chaptersEntities = (await getChildrenFromCodes(codeSystem, childrenIds)).results + const chaptersEntities = (await getChildrenFromCodes(valueSetUrl, childrenIds)).results childrenIds.push(UNKOWN_HIERARCHY_CHAPTER) subItems = [...chaptersEntities, unknownChapter] } @@ -377,7 +408,8 @@ export const getHierarchyRoots = async ( { id: HIERARCHY_ROOT, label: valueSetTitle, - system: codeSystem, + system: codeSystemUrl, + valueSetUrl, // Populate ValueSet URL for proper grouping childrenIds, parentIds: [] } diff --git a/src/state/valueSets.ts b/src/state/valueSets.ts index ffe4ce8d4..1072718a7 100644 --- a/src/state/valueSets.ts +++ b/src/state/valueSets.ts @@ -18,7 +18,7 @@ import { getCodeList } from 'services/aphp/serviceValueSets' const valueSetsAdapter = createEntityAdapter>() -export type CodeCache = { [system: string]: Hierarchy[] } +export type CodeCache = { [valueSetUrl: string]: Hierarchy[] } export type ValueSetStore = { entities: Dictionary>; cache: CodeCache } @@ -84,7 +84,7 @@ const valueSetsSlice = createSlice({ }), reducers: { saveValueSets: (state, action) => valueSetsAdapter.setMany(state, action.payload), - updateCache: (state, action: PayloadAction<{ [system: string]: Hierarchy[] }>) => { + updateCache: (state, action: PayloadAction<{ [valueSetUrl: string]: Hierarchy[] }>) => { return { ...state, cache: action.payload diff --git a/src/types/hierarchy.ts b/src/types/hierarchy.ts index c3c9a2854..0f1ac8b9f 100644 --- a/src/types/hierarchy.ts +++ b/src/types/hierarchy.ts @@ -35,6 +35,7 @@ export type Hierarchy = AbstractTree< above_levels_ids: string inferior_levels_ids: string system: string + valueSetUrl?: string // ValueSet URL (for grouping and API calls) status?: SelectedStatus statTotal?: number statTotalUnique?: number diff --git a/src/types/valueSet.ts b/src/types/valueSet.ts index f2337d3dc..54243ac9f 100644 --- a/src/types/valueSet.ts +++ b/src/types/valueSet.ts @@ -27,7 +27,8 @@ export type Reference = { label: string title: string standard: boolean - url: string + url: string // ValueSet URL (for searching/listing valuesets) + codeSystemUrls?: string[] // Array of CodeSystem URLs (for individual codes within valuesets) checked: boolean isHierarchy: boolean joinDisplayWithCode: boolean @@ -47,7 +48,12 @@ export type FhirItem = { label: string parentIds?: string[] childrenIds?: string[] - system: string + system: string // CodeSystem URL (for individual code identification) + valueSetUrl?: string // ValueSet URL (for grouping and API calls) statTotal?: number statTotalUnique?: number } + +// Utility types for reverse lookup functionality +export type CodeSystemToValueSetMap = Record +export type ValueSetToCodeSystemMap = Record diff --git a/src/utils/cohortCreation.ts b/src/utils/cohortCreation.ts index 1d26b8b50..ae48994e4 100644 --- a/src/utils/cohortCreation.ts +++ b/src/utils/cohortCreation.ts @@ -43,6 +43,7 @@ import { getChildrenFromCodes, HIERARCHY_ROOT } from 'services/aphp/serviceValue import { createHierarchyRoot } from './hierarchy' import { FhirItem } from 'types/valueSet' import { ScopeElement } from 'types/scope' +import { getValueSetFromCodeSystem } from './valueSets' import { formatAge } from './age' /** Current version of the Requeteur format used for cohort requests */ @@ -754,20 +755,14 @@ export async function unbuildRequest(_json: string): Promise[] | undefined> => { - if (code === HIERARCHY_ROOT && systems.length) return [createHierarchyRoot(systems[0])] - for (const system of systems) { +const getCodesForValueSet = async ( + code: string, + valueSetUrls: string[] +): Promise[] | undefined> => { + if (code === HIERARCHY_ROOT && valueSetUrls.length) return [createHierarchyRoot(valueSetUrls[0])] + for (const valueSetUrl of valueSetUrls) { try { - return (await getChildrenFromCodes(system, [code])).results + return (await getChildrenFromCodes(valueSetUrl, [code])).results } catch { console.error("Ce n'est pas une erreur.") } @@ -822,21 +817,32 @@ export const fetchCriteriasCodes = async ( const labelValues = criterion[dataKey] as unknown as LabelObject[] if (labelValues && labelValues.length > 0) { for (const code of labelValues) { - const codeSystem = code.system ?? defaultValueSet - const valueSetCodeCache = [...(updatedCriteriaData[codeSystem] ?? [])] + // code.system is a CodeSystem URL, we need to find the corresponding ValueSet URL + let valueSetUrl = defaultValueSet + if (code.system) { + // Try to find the ValueSet URL from the CodeSystem URL + const foundValueSetUrl = getValueSetFromCodeSystem(code.system) + if (foundValueSetUrl) { + valueSetUrl = foundValueSetUrl + } else { + valueSetUrl = defaultValueSet + } + } + const valueSetCodeCache = [...(updatedCriteriaData[valueSetUrl] ?? [])] if (!valueSetCodeCache.find((data) => data.id === code.id)) { try { - const fetchedCode = await getCodesForValueSet(code.id, [codeSystem]) + const fetchedCode = await getCodesForValueSet(code.id, [valueSetUrl]) if (fetchedCode) { valueSetCodeCache.push(...fetchedCode) } else { - console.warn(`Code ${code.id} not found in system ${codeSystem}`) + console.warn(`Code ${code.id} not found in valueSet ${valueSetUrl}`) } } catch (e) { - console.error(`Error fetching code ${code.id} from system ${codeSystem}`, e) + // fail silently + console.error(`Error fetching code ${code.id} from valueSet ${valueSetUrl}`, e) } } - updatedCriteriaData[codeSystem] = valueSetCodeCache + updatedCriteriaData[valueSetUrl] = valueSetCodeCache } } } diff --git a/src/utils/hierarchy.ts b/src/utils/hierarchy.ts index 77af63440..e71732731 100644 --- a/src/utils/hierarchy.ts +++ b/src/utils/hierarchy.ts @@ -71,18 +71,25 @@ const addAllFetchedIds = (codes: Map>, results: return new Map([...codes, ...resultsMap]) } -export const getMissingCodesWithSystems = async ( +export const getMissingCodesWithValueSets = async ( trees: Map[]>, - groupBySystem: GroupedBySystem[], + groupByValueSet: Array<{ valueSetUrl: string; codes: Hierarchy[] }>, codes: Codes>, - fetchHandler: (ids: string, system: string) => Promise[]> + fetchHandler: (ids: string, valueSetUrl: 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) + for (const group of groupByValueSet) { + const tree = trees.get(group.valueSetUrl) || [] + const codesByValueSet = codes.get(group.valueSetUrl) || new Map() + const newCodes = await getMissingCodes( + tree, + codesByValueSet, + group.codes, + group.valueSetUrl, + Mode.SEARCH, + fetchHandler + ) + allCodes.set(group.valueSetUrl, newCodes) } return allCodes } @@ -91,9 +98,9 @@ export const getMissingCodes = async ( baseTree: Hierarchy[], prevCodes: Map>, newCodes: Hierarchy[], - system: string, + valueSetUrl: string, mode: Mode, - fetchHandler: (ids: string, system: string) => Promise[]> + fetchHandler: (ids: string, valueSetUrl: string) => Promise[]> ) => { const newCodesMap = mapHierarchyToMap(newCodes) let allCodes = new Map([...prevCodes, ...newCodesMap]) @@ -106,7 +113,8 @@ export const getMissingCodes = async ( } if (missingIds.length) { const ids = missingIds.join(',') - const fetched = await fetchHandler(ids, system) + console.log('debug: getMissingCodes calling fetchHandler with ids:', ids, 'valueSetUrl:', valueSetUrl) + const fetched = await fetchHandler(ids, valueSetUrl) allCodes = addAllFetchedIds(allCodes, fetched) } if (mode !== Mode.EXPAND) { @@ -114,7 +122,8 @@ export const getMissingCodes = async ( missingIds = getMissingIds(allCodes, arrayToMap(children, null)) if (missingIds.length) { const ids = missingIds.join(',') - const childrenResponse = await fetchHandler(ids, system) + console.log('debug: getMissingCodes (children) calling fetchHandler with ids:', ids, 'valueSetUrl:', valueSetUrl) + const childrenResponse = await fetchHandler(ids, valueSetUrl) allCodes = addAllFetchedIds(allCodes, childrenResponse) } } @@ -139,17 +148,17 @@ const getMissingSubItems = (node: Hierarchy, codes: Map( trees: Map[]>, - groupBySystem: GroupedBySystem[], + groupByValueSet: Array<{ valueSetUrl: string; codes: Hierarchy[] }>, 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) + for (const group of groupByValueSet) { + const tree = trees.get(group.valueSetUrl) || [] + const codesByValueSet = codes.get(group.valueSetUrl) || new Map() + const selectedByValueSet = selected.get(group.valueSetUrl) || new Map() + const newTree = buildTree([...tree], group.valueSetUrl, group.codes, codesByValueSet, selectedByValueSet, mode) + trees.set(group.valueSetUrl, newTree) } return new Map(trees) } @@ -165,7 +174,7 @@ export const buildMultipleTrees = ( */ export const buildTree = ( baseTree: Hierarchy[], - system: string, + valueSetUrl: string, endCodes: Hierarchy[], codes: Map>, selected: Map>, @@ -173,7 +182,7 @@ export const buildTree = ( ) => { const buildBranch = ( node: Hierarchy, - system: string, + valueSetUrl: string, path: [string, InfiniteMap], codes: Map>, selected: Map>, @@ -186,7 +195,7 @@ export const buildTree = ( for (const [nextKey, nextValue] of nextPath) { const index = node.subItems.findIndex((elem) => elem.id === nextKey) if (index > -1) { - const item = buildBranch(node.subItems[index], system, [nextKey, nextValue], codes, selected, mode) + const item = buildBranch(node.subItems[index], valueSetUrl, [nextKey, nextValue], codes, selected, mode) node.subItems[index] = item } node.status = getItemSelectedStatus(node) @@ -211,7 +220,7 @@ export const buildTree = ( 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, system, [key, value], codes, selected, mode) + const branch = buildBranch(baseTree[index] || null, valueSetUrl, [key, value], codes, selected, mode) if (branch && index > -1) baseTree[index] = branch else if (index === -1) baseTree.push(branch) } @@ -234,6 +243,22 @@ export const groupBySystem = (codes: Hierarchy[]) => { return groupedHierarchies } +export const groupByValueSet = (codes: Hierarchy[]) => { + const valueSetMap = new Map[]>() + for (const hierarchy of codes) { + const valueSetUrl = hierarchy.valueSetUrl || 'unknown-valueset' + if (!valueSetMap.has(valueSetUrl)) { + valueSetMap.set(valueSetUrl, []) + } + valueSetMap.get(valueSetUrl)!.push(hierarchy) + } + const groupedHierarchies: Array<{ valueSetUrl: string; codes: Hierarchy[] }> = [] + valueSetMap.forEach((codes, valueSetUrl) => { + groupedHierarchies.push({ valueSetUrl, codes }) + }) + return groupedHierarchies +} + export const getDisplayFromTree = (toDisplay: Hierarchy[], tree: Hierarchy[]) => { let branches: Hierarchy[] = [] if (toDisplay.length && tree.length) @@ -251,7 +276,8 @@ export const getDisplayFromTrees = ( ) => { const branches: Hierarchy[] = [] toDisplay.forEach((node) => { - const currentTree = trees.get(node.system) + const treeKey = node.valueSetUrl || node.system + const currentTree = trees.get(treeKey) if (currentTree) { const foundNode = getDisplayFromTree([node], currentTree)[0] branches.push(foundNode) @@ -378,13 +404,14 @@ const getInferiorLevels = (hierarchy: Hierarchy[]) => { ) } -export const createHierarchyRoot = (system: string, status?: SelectedStatus) => { +export const createHierarchyRoot = (valueSetUrl: string, status?: SelectedStatus) => { return { id: HIERARCHY_ROOT, label: 'Toute la hiérarchie', above_levels_ids: '', inferior_levels_ids: '', - system, + system: valueSetUrl, + valueSetUrl, status } } diff --git a/src/utils/valueSets.ts b/src/utils/valueSets.ts index a64693418..82e540c89 100644 --- a/src/utils/valueSets.ts +++ b/src/utils/valueSets.ts @@ -10,34 +10,25 @@ import { Codes, Hierarchy } from 'types/hierarchy' import { LabelObject } from 'types/searchCriterias' import { FhirItem } from 'types/valueSet' -/** - * Gets value set references that match the provided systems - * - * @param systems - Array of system URLs to filter by - * @returns Array of matching value set references - * - * @example - * ```typescript - * const valueSets = getValueSetsFromSystems(['http://loinc.org', 'http://snomed.info/sct']) - * ``` - */ -export const getValueSetsFromSystems = (systems: string[]) => { - return getReferences(getConfig()).filter((reference) => systems.includes(reference.url)) +export const getValueSetsByUrls = (urls: string[]) => { + return getReferences(getConfig()).filter((reference) => urls.includes(reference.url)) +} + +// Reverse lookup function: given a CodeSystem URL, find the corresponding ValueSet URL +export const getValueSetFromCodeSystem = (codeSystemUrl: string): string | undefined => { + const references = getReferences(getConfig()) + const reference = references.find((ref) => ref.codeSystemUrls?.includes(codeSystemUrl)) + return reference?.url +} + +// Helper function to get ValueSet Reference from CodeSystem URL +export const getValueSetReferenceFromCodeSystem = (codeSystemUrl: string) => { + const references = getReferences(getConfig()) + return references.find((ref) => ref.codeSystemUrls?.includes(codeSystemUrl)) } -/** - * Checks if a system should display codes alongside labels - * - * @param system - The system URL to check - * @returns True if codes should be displayed with labels, false otherwise - * - * @example - * ```typescript - * isDisplayedWithCode('http://loinc.org') // returns true/false based on configuration - * ``` - */ export const isDisplayedWithCode = (system: string) => { - const isFound = getValueSetsFromSystems([system])?.[0] + const isFound = getValueSetReferenceFromCodeSystem(system) return isFound?.joinDisplayWithCode } @@ -53,7 +44,7 @@ export const isDisplayedWithCode = (system: string) => { * ``` */ export const isDisplayedWithSystem = (system: string) => { - const isFound = getValueSetsFromSystems([system])?.[0] + const isFound = getValueSetReferenceFromCodeSystem(system) return isFound?.joinDisplayWithSystem } @@ -108,7 +99,7 @@ export const getFullLabelFromCode = (code: LabelObject) => { * ``` */ export const getLabelFromSystem = (system: string) => { - const isFound = getValueSetsFromSystems([system])?.[0] + const isFound = getValueSetReferenceFromCodeSystem(system) return isFound?.label || '' } @@ -130,8 +121,14 @@ export const checkIsLeaf = async (codes: Hierarchy[], cache: Codes 1) return false if (!children[0]) return true const code = codes[0] - const found = cache.get(code.system)?.get(children[0]) + const cacheKey = code.valueSetUrl || code.system + const found = cache.get(cacheKey)?.get(children[0]) let childCode: Hierarchy[] = found ? [found] : [] - if (!childCode.length) childCode = (await getChildrenFromCodes(code.system, children)).results + if (!childCode.length) { + // If we only have system (CodeSystem URL), try to find the corresponding ValueSet URL + const actualValueSetUrl = code.valueSetUrl || getValueSetFromCodeSystem(code.system) || code.system + + childCode = (await getChildrenFromCodes(actualValueSetUrl, children)).results + } return checkIsLeaf(childCode, cache) }