From 50af44551551c365e0780e713580bc55e1724af7 Mon Sep 17 00:00:00 2001 From: Zac Butko Date: Wed, 23 Feb 2022 03:24:08 -0800 Subject: [PATCH 01/14] Rough POC all tests on same timeline --- .../src/hiv/mock-concept-tree.ts | 27 ++++++ .../src/timeline/Timeline.tsx | 92 ++++++++++++++++--- .../src/timeline/helpers.tsx | 28 ++++++ .../src/timeline/useTimelineData.ts | 72 +++++++++++++++ 4 files changed, 208 insertions(+), 11 deletions(-) diff --git a/packages/esm-patient-test-results-app/src/hiv/mock-concept-tree.ts b/packages/esm-patient-test-results-app/src/hiv/mock-concept-tree.ts index f692c01a08..3eae66b942 100644 --- a/packages/esm-patient-test-results-app/src/hiv/mock-concept-tree.ts +++ b/packages/esm-patient-test-results-app/src/hiv/mock-concept-tree.ts @@ -9,6 +9,33 @@ const mockConceptTree = { obs: [ { display: 'Hematocrit', + conceptUuid: '1015AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + meta: { + datatype: 'Numeric', + hiAbsolute: 100, + hiCritical: null, + hiNormal: 51.9, + lowAbsolute: 0, + lowCritical: 21, + lowNormal: 32.3, + range: '32.3 – 51.9', + units: '%', + }, + values: [ + { + effectiveDateTime: '2020-05-07T00:00:00+00:00', + value: 42, + }, + { + effectiveDateTime: '2020-05-07T00:02:00+00:00', + value: 43, + }, + { + effectiveDateTime: '2020-05-07T00:03:00+00:00', + value: 45, + notes: 'Maybe there is some special note written here?', + }, + ], }, { display: 'Platelets', diff --git a/packages/esm-patient-test-results-app/src/timeline/Timeline.tsx b/packages/esm-patient-test-results-app/src/timeline/Timeline.tsx index d4d3f0bc65..b84f0ebaad 100644 --- a/packages/esm-patient-test-results-app/src/timeline/Timeline.tsx +++ b/packages/esm-patient-test-results-app/src/timeline/Timeline.tsx @@ -1,8 +1,17 @@ import React, { useContext } from 'react'; import { InlineLoading } from 'carbon-components-react'; import useScrollIndicator from './useScroll'; -import { usePatientPanels, useTimelineData } from './useTimelineData'; -import { PaddingContainer, TimeSlots, Grid, RowStartCell, GridItems, ShadowBox } from './helpers'; +import { useManyTimelineData, usePatientPanels, useTimelineData } from './useTimelineData'; +import { + PaddingContainer, + TimeSlots, + Grid, + RowStartCell, + NewRowStartCell, + GridItems, + NewGridItems, + ShadowBox, +} from './helpers'; import { ObsRecord, EmptyState } from '@openmrs/esm-patient-common-lib'; import styles from './timeline.scss'; import { RecentResultsGrid } from '../overview/recent-overview.component'; @@ -93,6 +102,36 @@ const DataRows: React.FC = ({ timeColumns, rowData, sortedTimes, })} ); +interface NewDataRowsProps { + rowData: { entries: any[]; meta: { units: string; range: string } }; + timeColumns: Array; + sortedTimes: Array; + showShadow: boolean; +} + +const NewDataRows: React.FC = ({ timeColumns, rowData, sortedTimes, showShadow }) => ( + + {Object.entries(rowData).map(([title, row], rowCount) => { + console.log('row', row); + const obs = row.entries; + const { units = '', range = '' } = row.meta; + + return ( + + + + + ); + })} + +); interface TimelineParams { patientUuid: string; @@ -163,18 +202,49 @@ export const MultiTimeline = ({ patientUuid }) => { const { data: panels } = usePatientPanels(patientUuid); const { activeTests } = useContext(FilterContext); - if (activeTests?.length === 0) { - return ; - } + const [xIsScrolled, yIsScrolled, containerRef] = useScrollIndicator(0, 32); const uuids = activeTests.map((test) => panels[test]); + const timelineData = useManyTimelineData(patientUuid, uuids); - return ( - <> - {activeTests && - activeTests?.map((test) => )} - - ); + const { + data: { + parsedTime: { yearColumns, dayColumns, timeColumns, sortedTimes }, + rowData, + panelName, + }, + loaded, + } = timelineData; + + if (activeTests?.length === 0) { + return ; + } + if (activeTests && timelineData && loaded) { + return ( + + + + + + + + + ); + } }; export default Timeline; diff --git a/packages/esm-patient-test-results-app/src/timeline/helpers.tsx b/packages/esm-patient-test-results-app/src/timeline/helpers.tsx index c52cb1f977..7e8c7e76b1 100644 --- a/packages/esm-patient-test-results-app/src/timeline/helpers.tsx +++ b/packages/esm-patient-test-results-app/src/timeline/helpers.tsx @@ -95,6 +95,20 @@ export const RowStartCell = ({ title, range, units, shadow = false, openTrendlin ); +export const NewRowStartCell = ({ title, range, units, shadow = false }) => ( +
+ {title} + + {range} {units} + +
+); + export const TimeSlots: React.FC<{ style?: React.CSSProperties; className?: string; @@ -117,3 +131,17 @@ export const GridItems = React.memo<{ })} )); + +export const NewGridItems = React.memo<{ + sortedTimes: Array; + obs: any; + zebra: boolean; +}>(({ sortedTimes, obs, zebra }) => ( + <> + {sortedTimes.map((_, i) => { + if (!obs[i]) return ; + const interpretation = 'NORMAL' || obs[i].meta.assessValue(obs[i].value); + return ; + })} + +)); diff --git a/packages/esm-patient-test-results-app/src/timeline/useTimelineData.ts b/packages/esm-patient-test-results-app/src/timeline/useTimelineData.ts index d1606dc796..f9a58605a2 100644 --- a/packages/esm-patient-test-results-app/src/timeline/useTimelineData.ts +++ b/packages/esm-patient-test-results-app/src/timeline/useTimelineData.ts @@ -97,6 +97,78 @@ export const useTimelineData = (patientUuid: string, panelUuid?: string) => { return timelineData; }; +const parsePanel = (panelData) => { + const outData = { + uuid: panelData.uuid, + type: panelData.type, + name: panelData.entries[0].name, + meta: panelData.entries[0].meta, + entries: [], + }; + let transformedEntries = []; + panelData.entries.forEach((entry) => { + transformedEntries.push({ + value: entry.value, + effectiveDateTime: entry.effectiveDateTime, + }); + }); + outData.entries = transformedEntries || []; + return outData; +}; + +/** + * Gets all patient sorted obs from usePatientResultsData, filters for panelUuids (if provided), + * then transforms data to be used by DataTable + * + * @param patientUuid - required patient identifier + * @param panelUuids - optional panel identifier + * @returns object of {data, loaded, error?} where data is formatted for use by the + * timeline data table + * + */ +export const useManyTimelineData = (patientUuid: string, panelUuids?: string[]) => { + const { sortedObs, loaded, error } = usePatientResultsData(patientUuid); + + const timelineData = useMemo(() => { + if (!sortedObs || !loaded || !!error) + return { + data: { parsedTime: {} as ReturnType }, + loaded, + error, + }; + // look for the specified panelUuid. If none is specified, just take any obs + const panels = Object.entries(sortedObs).filter(([, { uuid }]) => panelUuids.includes(uuid)) || []; + + let rows = {}; + + panels?.forEach((panel) => { + const [panelName, panelData] = panel; + if (panelData) { + rows[panelName] = parsePanel(panelData); + } + }); + + const allTimes = [ + ...new Set( + Object.keys(rows) + .map((row) => rows[row].entries.map((e) => e.effectiveDateTime)) + .flat(), + ), + ]; + allTimes.sort((a, b) => (new Date(a) < new Date(b) ? 1 : -1)); + Object.keys(rows).forEach((row) => { + const newEntries = allTimes.map((time) => rows[row].entries.find((entry) => entry.effectiveDateTime === time)); + rows[row].entries = newEntries; + }); + const panelName = 'Timeline'; + return { + data: { parsedTime: parseTime(allTimes), rowData: rows, panelName }, + loaded: true, + }; + }, [sortedObs, loaded, error, panelUuids]); + return timelineData; +}; + /** * Very bad way to get panelUuid that for all tests that pertain to a patient * Hopefully there's a better endpoint for this From 86554e100e18bb44025ba5a2c8f8dde3bfcfaaaf Mon Sep 17 00:00:00 2001 From: Zac Butko Date: Wed, 23 Feb 2022 10:33:03 -0800 Subject: [PATCH 02/14] Now includes interpretation + stronger typing --- .../src/timeline/Timeline.tsx | 21 ++++++++++++++++--- .../src/timeline/helpers.tsx | 3 +-- .../src/timeline/useTimelineData.ts | 6 +++++- 3 files changed, 24 insertions(+), 6 deletions(-) diff --git a/packages/esm-patient-test-results-app/src/timeline/Timeline.tsx b/packages/esm-patient-test-results-app/src/timeline/Timeline.tsx index b84f0ebaad..34e378d5b2 100644 --- a/packages/esm-patient-test-results-app/src/timeline/Timeline.tsx +++ b/packages/esm-patient-test-results-app/src/timeline/Timeline.tsx @@ -12,7 +12,7 @@ import { NewGridItems, ShadowBox, } from './helpers'; -import { ObsRecord, EmptyState } from '@openmrs/esm-patient-common-lib'; +import { ObsRecord, EmptyState, ObsMetaInfo, OBSERVATION_INTERPRETATION } from '@openmrs/esm-patient-common-lib'; import styles from './timeline.scss'; import { RecentResultsGrid } from '../overview/recent-overview.component'; import FilterContext from '../filter/filter-context'; @@ -102,8 +102,24 @@ const DataRows: React.FC = ({ timeColumns, rowData, sortedTimes, })} ); + +interface DataEntry { + value: number | string; + effectiveDateTime: string; + interpretation: OBSERVATION_INTERPRETATION; +} + +interface DataRow { + [_: string]: { + entries: Array; + meta: ObsMetaInfo; + name: string; + type: string; + uuid: string; + }; +} interface NewDataRowsProps { - rowData: { entries: any[]; meta: { units: string; range: string } }; + rowData: DataRow; timeColumns: Array; sortedTimes: Array; showShadow: boolean; @@ -112,7 +128,6 @@ interface NewDataRowsProps { const NewDataRows: React.FC = ({ timeColumns, rowData, sortedTimes, showShadow }) => ( {Object.entries(rowData).map(([title, row], rowCount) => { - console.log('row', row); const obs = row.entries; const { units = '', range = '' } = row.meta; diff --git a/packages/esm-patient-test-results-app/src/timeline/helpers.tsx b/packages/esm-patient-test-results-app/src/timeline/helpers.tsx index 7e8c7e76b1..4604e4c733 100644 --- a/packages/esm-patient-test-results-app/src/timeline/helpers.tsx +++ b/packages/esm-patient-test-results-app/src/timeline/helpers.tsx @@ -140,8 +140,7 @@ export const NewGridItems = React.memo<{ <> {sortedTimes.map((_, i) => { if (!obs[i]) return ; - const interpretation = 'NORMAL' || obs[i].meta.assessValue(obs[i].value); - return ; + return ; })} )); diff --git a/packages/esm-patient-test-results-app/src/timeline/useTimelineData.ts b/packages/esm-patient-test-results-app/src/timeline/useTimelineData.ts index f9a58605a2..27e37c70db 100644 --- a/packages/esm-patient-test-results-app/src/timeline/useTimelineData.ts +++ b/packages/esm-patient-test-results-app/src/timeline/useTimelineData.ts @@ -110,6 +110,10 @@ const parsePanel = (panelData) => { transformedEntries.push({ value: entry.value, effectiveDateTime: entry.effectiveDateTime, + interpretation: + entry.meta.assessValue && typeof entry.meta.assessValue === 'function' + ? entry.meta.assessValue(entry.value) + : '--', }); }); outData.entries = transformedEntries || []; @@ -132,7 +136,7 @@ export const useManyTimelineData = (patientUuid: string, panelUuids?: string[]) const timelineData = useMemo(() => { if (!sortedObs || !loaded || !!error) return { - data: { parsedTime: {} as ReturnType }, + data: { parsedTime: {} as ReturnType, rowData: {}, panelName: '' }, loaded, error, }; From df050faa20111570239e087778fea02e7bb4a9e1 Mon Sep 17 00:00:00 2001 From: Zac Butko Date: Wed, 23 Feb 2022 20:32:35 -0800 Subject: [PATCH 03/14] Style changes --- .../src/filter/filter-context.tsx | 8 ++--- .../src/filter/filter-reducer.ts | 2 +- .../src/filter/filter-set.scss | 18 ++++++++++ .../src/filter/filter-set.tsx | 2 +- .../src/hiv/mock-concept-tree.ts | 36 +++++++++++++++++++ .../src/timeline/useTimelineData.ts | 11 ++++-- 6 files changed, 69 insertions(+), 8 deletions(-) diff --git a/packages/esm-patient-test-results-app/src/filter/filter-context.tsx b/packages/esm-patient-test-results-app/src/filter/filter-context.tsx index 93034f8a96..6b8cb6f316 100644 --- a/packages/esm-patient-test-results-app/src/filter/filter-context.tsx +++ b/packages/esm-patient-test-results-app/src/filter/filter-context.tsx @@ -1,6 +1,5 @@ import React, { createContext, useReducer, useEffect, useMemo } from 'react'; import reducer from './filter-reducer'; -import mockConceptTree from '../hiv/mock-concept-tree'; const initialState = { checkboxes: {}, @@ -35,12 +34,13 @@ interface FilterContextProps { interface FilterProviderProps { sortedObs: any; // this data structure will change later + root: any; children: React.ReactNode; } const FilterContext = createContext(initialContext); -const FilterProvider = ({ sortedObs, children }: FilterProviderProps) => { +const FilterProvider = ({ sortedObs, root, children }: FilterProviderProps) => { const [state, dispatch] = useReducer(reducer, initialState); const actions = useMemo( @@ -62,9 +62,9 @@ const FilterProvider = ({ sortedObs, children }: FilterProviderProps) => { useEffect(() => { const tests = (sortedObs && Object.keys(sortedObs)) || []; if (tests.length && !Object.keys(state?.checkboxes).length) { - actions.initialize(mockConceptTree); + actions.initialize(root); } - }, [sortedObs, actions, state]); + }, [sortedObs, actions, state, root]); return ( { case 'initialize': const { parents, leaves } = computeParents(action.tree); return { - checkboxes: Object.fromEntries(leaves.map((leaf) => [leaf, true])), + checkboxes: Object.fromEntries(leaves?.map((leaf) => [leaf, true])) || {}, parents: parents, }; case 'toggleVal': diff --git a/packages/esm-patient-test-results-app/src/filter/filter-set.scss b/packages/esm-patient-test-results-app/src/filter/filter-set.scss index 870129ab68..f249005fb5 100644 --- a/packages/esm-patient-test-results-app/src/filter/filter-set.scss +++ b/packages/esm-patient-test-results-app/src/filter/filter-set.scss @@ -36,6 +36,14 @@ display: block; } + .bx--accordion__title { + margin: 0 0 0 0.8rem; + } + + .bx--accordion__arrow { + margin: 0.4rem 0 0 1rem; + } + // Chevron transformations .bx--accordion__item > button[aria-expanded="false"] > .bx--accordion__arrow { transform: rotate(90deg); @@ -47,7 +55,17 @@ fill: var(--brand-01); } + /** first node **/ .bx--accordion--start > .bx--accordion__item > button[aria-expanded="true"] { background-color: $ui-03; } + + .bx--accordion--start > .bx--accordion__item > button > .bx--accordion__title > .bx--checkbox-wrapper > .bx--checkbox-label { + font-size: 110%; + font-weight: 700; + } + + .bx--checkbox-label-text { + padding: 0 0 0 .75rem; + } } diff --git a/packages/esm-patient-test-results-app/src/filter/filter-set.tsx b/packages/esm-patient-test-results-app/src/filter/filter-set.tsx index 032a2f6988..b7c30d16b7 100644 --- a/packages/esm-patient-test-results-app/src/filter/filter-set.tsx +++ b/packages/esm-patient-test-results-app/src/filter/filter-set.tsx @@ -48,7 +48,7 @@ const FilterSet = ({ root, maxNest }: FilterProps) => { id={root?.display} checked={allChildrenChecked} indeterminate={indeterminate} - labelText={root?.display} + labelText={`${root?.display} (${parents?.[root?.display]?.length})`} onChange={() => updateParent(root.display)} /> } diff --git a/packages/esm-patient-test-results-app/src/hiv/mock-concept-tree.ts b/packages/esm-patient-test-results-app/src/hiv/mock-concept-tree.ts index 3eae66b942..9048d7b468 100644 --- a/packages/esm-patient-test-results-app/src/hiv/mock-concept-tree.ts +++ b/packages/esm-patient-test-results-app/src/hiv/mock-concept-tree.ts @@ -72,4 +72,40 @@ const mockConceptTree = { ], }; +export const mockConceptTree2 = { + display: 'Complete Blood Count', + obs: [ + { + display: 'Hematocrit', + conceptUuid: '1015AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + meta: { + datatype: 'Numeric', + hiAbsolute: 100, + hiCritical: null, + hiNormal: 51.9, + lowAbsolute: 0, + lowCritical: 21, + lowNormal: 32.3, + range: '32.3 – 51.9', + units: '%', + }, + values: [ + { + effectiveDateTime: '2020-05-07T00:00:00+00:00', + value: 42, + }, + { + effectiveDateTime: '2020-05-07T00:02:00+00:00', + value: 43, + }, + { + effectiveDateTime: '2020-05-07T00:03:00+00:00', + value: 45, + notes: 'Maybe there is some special note written here?', + }, + ], + }, + ], +}; + export default mockConceptTree; diff --git a/packages/esm-patient-test-results-app/src/timeline/useTimelineData.ts b/packages/esm-patient-test-results-app/src/timeline/useTimelineData.ts index 27e37c70db..28943dd468 100644 --- a/packages/esm-patient-test-results-app/src/timeline/useTimelineData.ts +++ b/packages/esm-patient-test-results-app/src/timeline/useTimelineData.ts @@ -2,6 +2,7 @@ import { useMemo } from 'react'; import usePatientResultsData from '../loadPatientTestData/usePatientResultsData'; import { ObsRecord } from '@openmrs/esm-patient-common-lib'; import { formatDate, formatTime, parseDate } from '@openmrs/esm-framework'; +import { exist } from '../loadPatientTestData/helpers'; const parseTime = (sortedTimes: string[]) => { const yearColumns: Array<{ year: string; size: number }> = [], @@ -98,11 +99,17 @@ export const useTimelineData = (patientUuid: string, panelUuid?: string) => { }; const parsePanel = (panelData) => { + const sample = panelData?.entries?.[0]; const outData = { uuid: panelData.uuid, type: panelData.type, - name: panelData.entries[0].name, - meta: panelData.entries[0].meta, + name: sample.name, + meta: { + ...sample.meta, + range: exist(sample?.meta?.lowNormal, sample?.meta?.hiNormal) + ? `${sample.meta.lowNormal} – ${sample.meta.hiNormal}` + : null, + }, entries: [], }; let transformedEntries = []; From b973c4c7af2742476acb47f51b0cc65a6cc7c9b0 Mon Sep 17 00:00:00 2001 From: Zac Butko Date: Thu, 24 Feb 2022 00:15:03 -0800 Subject: [PATCH 04/14] Table header is frozen --- .../src/hiv/mock-concept-tree.ts | 25 +++++++++++++++++-- .../src/timeline/Timeline.tsx | 5 +++- .../src/timeline/timeline.scss | 19 ++++++++------ 3 files changed, 39 insertions(+), 10 deletions(-) diff --git a/packages/esm-patient-test-results-app/src/hiv/mock-concept-tree.ts b/packages/esm-patient-test-results-app/src/hiv/mock-concept-tree.ts index 9048d7b468..31903eefc4 100644 --- a/packages/esm-patient-test-results-app/src/hiv/mock-concept-tree.ts +++ b/packages/esm-patient-test-results-app/src/hiv/mock-concept-tree.ts @@ -57,13 +57,34 @@ const mockConceptTree = { display: 'Serum chemistry panel', obs: [ { - display: 'Serum chloride', + display: 'Alkaline phosphatase', + }, + { + display: 'Blood urea nitrogen', // + }, + { + display: 'Serum Albumin', + }, + { + display: 'Serum calcium', }, { display: 'Serum carbon dioxide', }, { - display: 'Blood urea nitrogen', + display: 'Serum chloride', // + }, + { + display: 'Serum creatinine (umol/L)', // + }, + { + display: 'Serum carbon dioxide', // + }, + { + display: 'Serum sodium', // + }, + { + display: 'Serum potassium', // }, ], }, diff --git a/packages/esm-patient-test-results-app/src/timeline/Timeline.tsx b/packages/esm-patient-test-results-app/src/timeline/Timeline.tsx index 34e378d5b2..9dde2005f2 100644 --- a/packages/esm-patient-test-results-app/src/timeline/Timeline.tsx +++ b/packages/esm-patient-test-results-app/src/timeline/Timeline.tsx @@ -14,9 +14,12 @@ import { } from './helpers'; import { ObsRecord, EmptyState, ObsMetaInfo, OBSERVATION_INTERPRETATION } from '@openmrs/esm-patient-common-lib'; import styles from './timeline.scss'; -import { RecentResultsGrid } from '../overview/recent-overview.component'; import FilterContext from '../filter/filter-context'; +const RecentResultsGrid = (props) => { + return
; +}; + interface PanelNameCornerProps { showShadow: boolean; panelName: string; diff --git a/packages/esm-patient-test-results-app/src/timeline/timeline.scss b/packages/esm-patient-test-results-app/src/timeline/timeline.scss index abbe1e7b5f..8f0928fba1 100644 --- a/packages/esm-patient-test-results-app/src/timeline/timeline.scss +++ b/packages/esm-patient-test-results-app/src/timeline/timeline.scss @@ -65,22 +65,16 @@ } .padding-container { - height: fit-content; - max-height: 100%; - width: fit-content; - max-width: 100%; padding: 0px 0 0 0; box-sizing: border-box; - margin: 0; - overflow: auto; - scrollbar-width: none; display: grid; grid-auto-flow: row; column-gap: 1px; row-gap: 0px; background-color: $color-gray-30; grid-template: auto auto / 9em auto; + border-collapse: collapse; } .padding-container::-webkit-scrollbar { @@ -194,3 +188,14 @@ grid-row: 2 / -1; grid-column: 1 / 2; } + +.recent-results-grid { + display: grid; + background-color: white; + overflow: auto; + max-height: calc(100vh - 50px); +} + +.recent-results-grid::-webkit-scrollbar { + display: none; +} From 904e5f7ca8d52cdb27e729cc770d928c88a7c953 Mon Sep 17 00:00:00 2001 From: Zac Butko Date: Thu, 24 Feb 2022 00:19:25 -0800 Subject: [PATCH 05/14] small style changes --- .../src/hiv/hiv-care-and-treatment.tsx | 9 +++++++-- .../src/timeline/timeline.scss | 2 +- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/packages/esm-patient-test-results-app/src/hiv/hiv-care-and-treatment.tsx b/packages/esm-patient-test-results-app/src/hiv/hiv-care-and-treatment.tsx index 23ed18b571..8f759b3356 100644 --- a/packages/esm-patient-test-results-app/src/hiv/hiv-care-and-treatment.tsx +++ b/packages/esm-patient-test-results-app/src/hiv/hiv-care-and-treatment.tsx @@ -4,13 +4,18 @@ import mockConceptTree from './mock-concept-tree'; import FilterSet from '../filter/filter-set'; import FilterContext, { FilterProvider } from '../filter/filter-context'; import usePatientResultsData from '../loadPatientTestData/usePatientResultsData'; -import { usePatient } from '@openmrs/esm-framework'; +import { usePatient, openmrsFetch } from '@openmrs/esm-framework'; import { MultiTimeline } from '../timeline/Timeline'; import { EmptyState, ErrorState } from '@openmrs/esm-patient-common-lib'; +import useSWR from 'swr'; const HIVCareAndTreatment = () => { const { patientUuid } = usePatient(); const { sortedObs, loaded, error } = usePatientResultsData(patientUuid); + // const concept = '5035a431-51de-40f0-8f25-4a98762eb796'; // on openmrs-spa.org this is bloodwork + // const { data: result } = useSWR(`/ws/rest/v1/obstree?patient=${patientUuid}&concept=${concept}`, openmrsFetch); + // console.log('result', result); + if (!loaded) { return ; } @@ -19,7 +24,7 @@ const HIVCareAndTreatment = () => { } if (loaded && !error && sortedObs && !!Object.keys(sortedObs).length) { return ( - + diff --git a/packages/esm-patient-test-results-app/src/timeline/timeline.scss b/packages/esm-patient-test-results-app/src/timeline/timeline.scss index 8f0928fba1..2fdecafee7 100644 --- a/packages/esm-patient-test-results-app/src/timeline/timeline.scss +++ b/packages/esm-patient-test-results-app/src/timeline/timeline.scss @@ -193,7 +193,7 @@ display: grid; background-color: white; overflow: auto; - max-height: calc(100vh - 50px); + max-height: calc(100vh - 9rem); } .recent-results-grid::-webkit-scrollbar { From cd1c6c37c54dcfa7dd87b2d04de1ea765e50333c Mon Sep 17 00:00:00 2001 From: Zac Butko Date: Sun, 27 Feb 2022 14:47:56 -0800 Subject: [PATCH 06/14] Use /obstree endpoint --- .../src/filter/filter-context.tsx | 62 ++++++++-- .../src/filter/filter-reducer.ts | 24 ++-- .../src/filter/filter-set.tsx | 27 ++--- .../src/hiv/hiv-care-and-treatment.tsx | 32 +++-- .../src/timeline/Timeline.tsx | 17 ++- .../src/timeline/useObstreeData.ts | 40 +++++++ .../src/timeline/useTimelineData.ts | 113 +----------------- 7 files changed, 146 insertions(+), 169 deletions(-) create mode 100644 packages/esm-patient-test-results-app/src/timeline/useObstreeData.ts diff --git a/packages/esm-patient-test-results-app/src/filter/filter-context.tsx b/packages/esm-patient-test-results-app/src/filter/filter-context.tsx index 6b8cb6f316..9661825c4d 100644 --- a/packages/esm-patient-test-results-app/src/filter/filter-context.tsx +++ b/packages/esm-patient-test-results-app/src/filter/filter-context.tsx @@ -1,15 +1,21 @@ -import React, { createContext, useReducer, useEffect, useMemo } from 'react'; +import React, { createContext, useReducer, useEffect, useMemo, useState } from 'react'; +import { parseTime } from '../timeline/useTimelineData'; import reducer from './filter-reducer'; const initialState = { checkboxes: {}, parents: {}, + root: {}, + tests: {}, }; const initialContext = { state: initialState, checkboxes: {}, parents: {}, + root: {}, + tests: {}, + timelineData: {}, activeTests: [], someChecked: false, initialize: () => {}, @@ -20,11 +26,15 @@ const initialContext = { interface StateProps { checkboxes: { [key: string]: boolean }; parents: { [key: string]: string[] }; + root: { [key: string]: any }; } interface FilterContextProps { state: StateProps; checkboxes: { [key: string]: boolean }; parents: { [key: string]: string[] }; + root: { [key: string]: any }; + tests: { [key: string]: any }; + timelineData: { [key: string]: any }; activeTests: string[]; someChecked: boolean; initialize: any; @@ -33,14 +43,17 @@ interface FilterContextProps { } interface FilterProviderProps { - sortedObs: any; // this data structure will change later root: any; children: React.ReactNode; } +interface obsShape { + [key: string]: any; +} + const FilterContext = createContext(initialContext); -const FilterProvider = ({ sortedObs, root, children }: FilterProviderProps) => { +const FilterProvider = ({ root, children }: FilterProviderProps) => { const [state, dispatch] = useReducer(reducer, initialState); const actions = useMemo( @@ -56,15 +69,47 @@ const FilterProvider = ({ sortedObs, root, children }: FilterProviderProps) => { [dispatch], ); - const activeTests = Object.keys(state?.checkboxes)?.filter((key) => state.checkboxes[key]) || []; + const activeTests = useMemo(() => { + return Object.keys(state?.checkboxes)?.filter((key) => state.checkboxes[key]) || []; + }, [state.checkboxes]); + const someChecked = Boolean(activeTests.length); + const timelineData = useMemo(() => { + if (!state?.tests) { + return { + data: { parsedTime: {} as ReturnType, rowData: {}, panelName: '' }, + loaded: false, + }; + } + const tests: obsShape = Object.fromEntries( + Object.entries(state.tests).filter(([name, entry]) => activeTests.includes(name)), + ); + const allTimes = [ + ...new Set( + Object.values(tests) + .map((test: obsShape) => test?.obs?.map((entry) => entry.obsDatetime)) + .flat(), + ), + ]; + allTimes.sort((a, b) => (new Date(a) < new Date(b) ? 1 : -1)); + const rows = {}; + Object.keys(tests).forEach((test) => { + const newEntries = allTimes.map((time: string) => tests[test].obs.find((entry) => entry.obsDatetime === time)); + rows[test] = { ...tests[test], entries: newEntries }; + }); + const panelName = 'timeline'; + return { + data: { parsedTime: parseTime(allTimes), rowData: rows, panelName }, + loaded: true, + }; + }, [activeTests, state.tests]); + useEffect(() => { - const tests = (sortedObs && Object.keys(sortedObs)) || []; - if (tests.length && !Object.keys(state?.checkboxes).length) { + if (root?.display && !Object.keys(state?.checkboxes).length) { actions.initialize(root); } - }, [sortedObs, actions, state, root]); + }, [actions, state, root]); return ( { state, checkboxes: state.checkboxes, parents: state.parents, + root: state.root, + tests: state.tests, + timelineData, activeTests, someChecked, initialize: actions.initialize, diff --git a/packages/esm-patient-test-results-app/src/filter/filter-reducer.ts b/packages/esm-patient-test-results-app/src/filter/filter-reducer.ts index a0f161c87b..26a8d413b6 100644 --- a/packages/esm-patient-test-results-app/src/filter/filter-reducer.ts +++ b/packages/esm-patient-test-results-app/src/filter/filter-reducer.ts @@ -1,27 +1,37 @@ const computeParents = (node) => { var parents = {}; const leaves = []; - if (node?.subSets?.length) { + const tests = []; + if (node?.subSets?.length && node.subSets[0].datatype) { + leaves.push(...node.subSets.map((leaf) => leaf.display)); + tests.push( + ...node.subSets.map((leaf) => { + const { display, ...rest } = leaf; + return [display, rest]; + }), + ); + } else if (node?.subSets?.length) { node.subSets.map((subNode) => { - const { parents: newParents, leaves: newLeaves } = computeParents(subNode); + const { parents: newParents, leaves: newLeaves, tests: newTests } = computeParents(subNode); parents = { ...parents, ...newParents }; leaves.push(...newLeaves); + tests.push(...newTests); }); } - if (node?.obs?.length) { - leaves.push(...node.obs.map((leaf) => leaf.display)); - } parents[node.display] = leaves; - return { parents: parents, leaves: leaves }; + return { parents, leaves, tests }; }; const reducer = (state, action) => { switch (action.type) { case 'initialize': - const { parents, leaves } = computeParents(action.tree); + const { parents, leaves, tests } = computeParents(action.tree); + const flatTests = Object.fromEntries(tests); return { checkboxes: Object.fromEntries(leaves?.map((leaf) => [leaf, true])) || {}, parents: parents, + root: action.tree, + tests: flatTests, }; case 'toggleVal': return { diff --git a/packages/esm-patient-test-results-app/src/filter/filter-set.tsx b/packages/esm-patient-test-results-app/src/filter/filter-set.tsx index b7c30d16b7..057836d29e 100644 --- a/packages/esm-patient-test-results-app/src/filter/filter-set.tsx +++ b/packages/esm-patient-test-results-app/src/filter/filter-set.tsx @@ -8,12 +8,12 @@ interface Observation { } interface TreeNode { display: string; + datatype: string; subSets?: TreeNode[]; obs?: Observation[]; } interface FilterProps { - root: TreeNode; maxNest?: number; children?: React.ReactNode; } @@ -32,8 +32,8 @@ const isIndeterminate = (kids, checkboxes) => { return kids && !kids?.every((kid) => checkboxes[kid]) && !kids?.every((kid) => !checkboxes[kid]); }; -const FilterSet = ({ root, maxNest }: FilterProps) => { - const { someChecked, parents, checkboxes, updateParent } = useContext(FilterContext); +const FilterSet = ({ maxNest }: FilterProps) => { + const { someChecked, parents, checkboxes, updateParent, root } = useContext(FilterContext); const indeterminate = isIndeterminate(parents[root.display], checkboxes); const allChildrenChecked = parents[root.display]?.every((kid) => checkboxes[kid]); @@ -53,12 +53,9 @@ const FilterSet = ({ root, maxNest }: FilterProps) => { /> } > - {root?.subSets?.map((node, index) => ( - - ))} - {root?.obs?.map((obs, index) => ( - - ))} + {!root?.subSets?.[0]?.datatype && + root?.subSets?.map((node, index) => )} + {root?.subSets?.[0]?.datatype && root.subSets?.map((obs, index) => )}
@@ -69,7 +66,6 @@ const FilterNode = ({ root, level, maxNest = 3 }: FilterNodeProps) => { const { checkboxes, parents, updateParent } = useContext(FilterContext); const indeterminate = isIndeterminate(parents[root.display], checkboxes); const allChildrenChecked = parents[root.display]?.every((kid) => checkboxes[kid]); - return ( { } style={{ paddingLeft: level > 0 && level < maxNest ? '1rem' : '0px' }} > - {root?.subSets?.map((node, index) => ( - - ))} - {root?.obs?.map((obs, index) => ( - - ))} + {!root?.subSets?.[0]?.datatype && + root?.subSets?.map((node, index) => ( + + ))} + {root?.subSets?.[0]?.datatype && root.subSets?.map((obs, index) => )} ); diff --git a/packages/esm-patient-test-results-app/src/hiv/hiv-care-and-treatment.tsx b/packages/esm-patient-test-results-app/src/hiv/hiv-care-and-treatment.tsx index 8f759b3356..d8fb0ada32 100644 --- a/packages/esm-patient-test-results-app/src/hiv/hiv-care-and-treatment.tsx +++ b/packages/esm-patient-test-results-app/src/hiv/hiv-care-and-treatment.tsx @@ -1,44 +1,42 @@ import { Column, Grid, InlineLoading, Row } from 'carbon-components-react'; -import React, { useContext, useEffect } from 'react'; -import mockConceptTree from './mock-concept-tree'; +import React from 'react'; import FilterSet from '../filter/filter-set'; -import FilterContext, { FilterProvider } from '../filter/filter-context'; -import usePatientResultsData from '../loadPatientTestData/usePatientResultsData'; -import { usePatient, openmrsFetch } from '@openmrs/esm-framework'; +import { FilterProvider } from '../filter/filter-context'; import { MultiTimeline } from '../timeline/Timeline'; import { EmptyState, ErrorState } from '@openmrs/esm-patient-common-lib'; -import useSWR from 'swr'; +import useGetObstreeData from '../timeline/useObstreeData'; + +interface obsShape { + [key: string]: any; +} const HIVCareAndTreatment = () => { - const { patientUuid } = usePatient(); - const { sortedObs, loaded, error } = usePatientResultsData(patientUuid); - // const concept = '5035a431-51de-40f0-8f25-4a98762eb796'; // on openmrs-spa.org this is bloodwork - // const { data: result } = useSWR(`/ws/rest/v1/obstree?patient=${patientUuid}&concept=${concept}`, openmrsFetch); - // console.log('result', result); + const concept = '5035a431-51de-40f0-8f25-4a98762eb796'; // bloodwork + const { data: root, error, loading }: obsShape = useGetObstreeData(concept); - if (!loaded) { + if (loading) { return ; } if (error) { return ; } - if (loaded && !error && sortedObs && !!Object.keys(sortedObs).length) { + if (!loading && !error && root?.display && root?.subSets?.length) { return ( - + - + - + ); } - if (loaded && !error && sortedObs && !Object.keys(sortedObs).length) { + if (!loading && !error && root?.display && root?.subSets?.length === 0) { return ; } return null; diff --git a/packages/esm-patient-test-results-app/src/timeline/Timeline.tsx b/packages/esm-patient-test-results-app/src/timeline/Timeline.tsx index 9dde2005f2..d4758c0ee8 100644 --- a/packages/esm-patient-test-results-app/src/timeline/Timeline.tsx +++ b/packages/esm-patient-test-results-app/src/timeline/Timeline.tsx @@ -1,7 +1,7 @@ import React, { useContext } from 'react'; import { InlineLoading } from 'carbon-components-react'; import useScrollIndicator from './useScroll'; -import { useManyTimelineData, usePatientPanels, useTimelineData } from './useTimelineData'; +import { useTimelineData } from './useTimelineData'; import { PaddingContainer, TimeSlots, @@ -115,10 +115,11 @@ interface DataEntry { interface DataRow { [_: string]: { entries: Array; - meta: ObsMetaInfo; name: string; type: string; uuid: string; + units: string; + range: string; }; } interface NewDataRowsProps { @@ -132,7 +133,7 @@ const NewDataRows: React.FC = ({ timeColumns, rowData, sortedT {Object.entries(rowData).map(([title, row], rowCount) => { const obs = row.entries; - const { units = '', range = '' } = row.meta; + const { units = '', range = '' } = row; return ( @@ -216,15 +217,10 @@ export const Timeline: React.FC = ({ return ; }; -export const MultiTimeline = ({ patientUuid }) => { - const { data: panels } = usePatientPanels(patientUuid); - const { activeTests } = useContext(FilterContext); - +export const MultiTimeline = () => { + const { activeTests, timelineData } = useContext(FilterContext); const [xIsScrolled, yIsScrolled, containerRef] = useScrollIndicator(0, 32); - const uuids = activeTests.map((test) => panels[test]); - const timelineData = useManyTimelineData(patientUuid, uuids); - const { data: { parsedTime: { yearColumns, dayColumns, timeColumns, sortedTimes }, @@ -263,6 +259,7 @@ export const MultiTimeline = ({ patientUuid }) => { ); } + return null; }; export default Timeline; diff --git a/packages/esm-patient-test-results-app/src/timeline/useObstreeData.ts b/packages/esm-patient-test-results-app/src/timeline/useObstreeData.ts new file mode 100644 index 0000000000..0766f2e1e4 --- /dev/null +++ b/packages/esm-patient-test-results-app/src/timeline/useObstreeData.ts @@ -0,0 +1,40 @@ +import { usePatient, openmrsFetch } from '@openmrs/esm-framework'; +import { useMemo } from 'react'; +import useSWR from 'swr'; +import { assessValue, exist } from '../loadPatientTestData/helpers'; + +const augmentObstreeData = (node) => { + const outData = JSON.parse(JSON.stringify(node)); + if (outData?.subSets?.length) { + outData.subSets = outData.subSets.map((subNode) => augmentObstreeData(subNode)); + } + if (exist(outData?.hiNormal, outData?.lowNormal)) { + outData.range = `${outData.lowNormal} – ${outData.hiNormal}`; + } + if (outData?.obs?.length) { + const assess = assessValue(outData); + outData.obs = outData.obs.map((ob) => ({ ...ob, interpretation: assess(ob.value) })); + } + return { ...outData }; +}; + +const useGetObstreeData = (conceptUuid) => { + const { patientUuid } = usePatient(); + const response = useSWR(`/ws/rest/v1/obstree?patient=${patientUuid}&concept=${conceptUuid}`, openmrsFetch); + const result = useMemo(() => { + if (response.data) { + const { data, ...rest } = response; + const newData = augmentObstreeData(data?.data); + return { ...rest, loading: false, data: newData }; + } else { + return { + data: {}, + error: false, + loading: true, + }; + } + }, [response]); + return result; +}; + +export default useGetObstreeData; diff --git a/packages/esm-patient-test-results-app/src/timeline/useTimelineData.ts b/packages/esm-patient-test-results-app/src/timeline/useTimelineData.ts index 28943dd468..60e23f3576 100644 --- a/packages/esm-patient-test-results-app/src/timeline/useTimelineData.ts +++ b/packages/esm-patient-test-results-app/src/timeline/useTimelineData.ts @@ -2,9 +2,8 @@ import { useMemo } from 'react'; import usePatientResultsData from '../loadPatientTestData/usePatientResultsData'; import { ObsRecord } from '@openmrs/esm-patient-common-lib'; import { formatDate, formatTime, parseDate } from '@openmrs/esm-framework'; -import { exist } from '../loadPatientTestData/helpers'; -const parseTime = (sortedTimes: string[]) => { +export const parseTime = (sortedTimes: string[]) => { const yearColumns: Array<{ year: string; size: number }> = [], dayColumns: Array<{ year: string; day: string; size: number }> = [], timeColumns: string[] = []; @@ -97,113 +96,3 @@ export const useTimelineData = (patientUuid: string, panelUuid?: string) => { }, [sortedObs, loaded, error, panelUuid]); return timelineData; }; - -const parsePanel = (panelData) => { - const sample = panelData?.entries?.[0]; - const outData = { - uuid: panelData.uuid, - type: panelData.type, - name: sample.name, - meta: { - ...sample.meta, - range: exist(sample?.meta?.lowNormal, sample?.meta?.hiNormal) - ? `${sample.meta.lowNormal} – ${sample.meta.hiNormal}` - : null, - }, - entries: [], - }; - let transformedEntries = []; - panelData.entries.forEach((entry) => { - transformedEntries.push({ - value: entry.value, - effectiveDateTime: entry.effectiveDateTime, - interpretation: - entry.meta.assessValue && typeof entry.meta.assessValue === 'function' - ? entry.meta.assessValue(entry.value) - : '--', - }); - }); - outData.entries = transformedEntries || []; - return outData; -}; - -/** - * Gets all patient sorted obs from usePatientResultsData, filters for panelUuids (if provided), - * then transforms data to be used by DataTable - * - * @param patientUuid - required patient identifier - * @param panelUuids - optional panel identifier - * @returns object of {data, loaded, error?} where data is formatted for use by the - * timeline data table - * - */ -export const useManyTimelineData = (patientUuid: string, panelUuids?: string[]) => { - const { sortedObs, loaded, error } = usePatientResultsData(patientUuid); - - const timelineData = useMemo(() => { - if (!sortedObs || !loaded || !!error) - return { - data: { parsedTime: {} as ReturnType, rowData: {}, panelName: '' }, - loaded, - error, - }; - // look for the specified panelUuid. If none is specified, just take any obs - const panels = Object.entries(sortedObs).filter(([, { uuid }]) => panelUuids.includes(uuid)) || []; - - let rows = {}; - - panels?.forEach((panel) => { - const [panelName, panelData] = panel; - if (panelData) { - rows[panelName] = parsePanel(panelData); - } - }); - - const allTimes = [ - ...new Set( - Object.keys(rows) - .map((row) => rows[row].entries.map((e) => e.effectiveDateTime)) - .flat(), - ), - ]; - allTimes.sort((a, b) => (new Date(a) < new Date(b) ? 1 : -1)); - Object.keys(rows).forEach((row) => { - const newEntries = allTimes.map((time) => rows[row].entries.find((entry) => entry.effectiveDateTime === time)); - rows[row].entries = newEntries; - }); - const panelName = 'Timeline'; - return { - data: { parsedTime: parseTime(allTimes), rowData: rows, panelName }, - loaded: true, - }; - }, [sortedObs, loaded, error, panelUuids]); - return timelineData; -}; - -/** - * Very bad way to get panelUuid that for all tests that pertain to a patient - * Hopefully there's a better endpoint for this - * - * @param patientUuid - required patient identifier - * @returns Object of {data, loaded, error} where data is an object in format - * "PanelName": panelUuid - * - */ -export const usePatientPanels = (patientUuid: string) => { - const { sortedObs, loaded, error } = usePatientResultsData(patientUuid); - - const panels = useMemo(() => { - if (!sortedObs || !loaded || !!error) - return { - data: [{ parsedTime: {} as ReturnType }], - loaded, - error, - }; - - const outData = {}; - Object.entries(sortedObs).forEach(([key, value]) => (outData[key] = value.uuid)); - return { loaded: true, data: outData }; - }, [sortedObs, loaded, error]); - - return panels; -}; From 583aa044dbd301fe29f106e5014ca4fd41dfb209 Mon Sep 17 00:00:00 2001 From: Zac Butko Date: Tue, 1 Mar 2022 14:28:51 -0800 Subject: [PATCH 07/14] Major refactor --- .../src/filter/filter-context.tsx | 22 +- .../src/filter/filter-reducer.ts | 42 ++-- .../src/filter/filter-set.scss | 16 +- .../src/filter/filter-set.tsx | 68 ++---- .../src/hiv/hiv-care-and-treatment.tsx | 6 +- .../src/new-timeline/new-timeline.scss | 202 ++++++++++++++++++ .../src/new-timeline/new-timeline.tsx | 190 ++++++++++++++++ .../useObstreeData.ts | 14 +- .../src/timeline/Timeline.tsx | 107 +--------- 9 files changed, 473 insertions(+), 194 deletions(-) create mode 100644 packages/esm-patient-test-results-app/src/new-timeline/new-timeline.scss create mode 100644 packages/esm-patient-test-results-app/src/new-timeline/new-timeline.tsx rename packages/esm-patient-test-results-app/src/{timeline => new-timeline}/useObstreeData.ts (76%) diff --git a/packages/esm-patient-test-results-app/src/filter/filter-context.tsx b/packages/esm-patient-test-results-app/src/filter/filter-context.tsx index 9661825c4d..f8b74028f6 100644 --- a/packages/esm-patient-test-results-app/src/filter/filter-context.tsx +++ b/packages/esm-patient-test-results-app/src/filter/filter-context.tsx @@ -1,23 +1,20 @@ import React, { createContext, useReducer, useEffect, useMemo, useState } from 'react'; import { parseTime } from '../timeline/useTimelineData'; import reducer from './filter-reducer'; +import { TreeNode } from './filter-set'; const initialState = { checkboxes: {}, parents: {}, - root: {}, + root: { display: '', flatName: '' }, tests: {}, }; const initialContext = { state: initialState, - checkboxes: {}, - parents: {}, - root: {}, - tests: {}, + ...initialState, timelineData: {}, activeTests: [], - someChecked: false, initialize: () => {}, toggleVal: () => {}, updateParent: () => {}, @@ -32,11 +29,10 @@ interface FilterContextProps { state: StateProps; checkboxes: { [key: string]: boolean }; parents: { [key: string]: string[] }; - root: { [key: string]: any }; + root: TreeNode; tests: { [key: string]: any }; timelineData: { [key: string]: any }; activeTests: string[]; - someChecked: boolean; initialize: any; toggleVal: any; updateParent: any; @@ -73,8 +69,6 @@ const FilterProvider = ({ root, children }: FilterProviderProps) => { return Object.keys(state?.checkboxes)?.filter((key) => state.checkboxes[key]) || []; }, [state.checkboxes]); - const someChecked = Boolean(activeTests.length); - const timelineData = useMemo(() => { if (!state?.tests) { return { @@ -82,9 +76,10 @@ const FilterProvider = ({ root, children }: FilterProviderProps) => { loaded: false, }; } - const tests: obsShape = Object.fromEntries( - Object.entries(state.tests).filter(([name, entry]) => activeTests.includes(name)), - ); + const tests: obsShape = activeTests?.length + ? Object.fromEntries(Object.entries(state.tests).filter(([name, entry]) => activeTests.includes(name))) + : state.tests; + const allTimes = [ ...new Set( Object.values(tests) @@ -121,7 +116,6 @@ const FilterProvider = ({ root, children }: FilterProviderProps) => { tests: state.tests, timelineData, activeTests, - someChecked, initialize: actions.initialize, toggleVal: actions.toggleVal, updateParent: actions.updateParent, diff --git a/packages/esm-patient-test-results-app/src/filter/filter-reducer.ts b/packages/esm-patient-test-results-app/src/filter/filter-reducer.ts index 26a8d413b6..ad0a6bf524 100644 --- a/packages/esm-patient-test-results-app/src/filter/filter-reducer.ts +++ b/packages/esm-patient-test-results-app/src/filter/filter-reducer.ts @@ -1,34 +1,50 @@ -const computeParents = (node) => { +export const getName = (prefix, name) => { + return prefix ? `${prefix}-${name}` : name; +}; + +const computeParents = (prefix, node) => { var parents = {}; const leaves = []; const tests = []; - if (node?.subSets?.length && node.subSets[0].datatype) { - leaves.push(...node.subSets.map((leaf) => leaf.display)); - tests.push( - ...node.subSets.map((leaf) => { - const { display, ...rest } = leaf; - return [display, rest]; - }), - ); + if (node?.subSets?.length && node.subSets[0].obs) { + let activeLeaves = []; + node.subSets.forEach((leaf) => { + if (leaf.hasData) { + activeLeaves.push(leaf.flatName); + } + }); + let activeTests = []; + node.subSets.forEach((leaf) => { + if (leaf.obs.length) { + const { flatName, ...rest } = leaf; + activeTests.push([flatName, rest]); + } + }); + leaves.push(...activeLeaves); + tests.push(...activeTests); } else if (node?.subSets?.length) { node.subSets.map((subNode) => { - const { parents: newParents, leaves: newLeaves, tests: newTests } = computeParents(subNode); + const { + parents: newParents, + leaves: newLeaves, + tests: newTests, + } = computeParents(getName(prefix, node.display), subNode); parents = { ...parents, ...newParents }; leaves.push(...newLeaves); tests.push(...newTests); }); } - parents[node.display] = leaves; + parents[node.flatName] = leaves; return { parents, leaves, tests }; }; const reducer = (state, action) => { switch (action.type) { case 'initialize': - const { parents, leaves, tests } = computeParents(action.tree); + const { parents, leaves, tests } = computeParents('', action.tree); const flatTests = Object.fromEntries(tests); return { - checkboxes: Object.fromEntries(leaves?.map((leaf) => [leaf, true])) || {}, + checkboxes: Object.fromEntries(leaves?.map((leaf) => [leaf, false])) || {}, parents: parents, root: action.tree, tests: flatTests, diff --git a/packages/esm-patient-test-results-app/src/filter/filter-set.scss b/packages/esm-patient-test-results-app/src/filter/filter-set.scss index f249005fb5..0503139198 100644 --- a/packages/esm-patient-test-results-app/src/filter/filter-set.scss +++ b/packages/esm-patient-test-results-app/src/filter/filter-set.scss @@ -7,7 +7,7 @@ margin: $spacing-02 0; } -.filterContainerActive { +.filterContainerExpanded { border-left: 0.3rem solid var(--brand-01); } @@ -20,6 +20,10 @@ border-bottom: 1px solid $ui-03; } +.nestedAccordion > :global(.bx--accordion--start) > :global(.bx--accordion__item--active) { + border-left: 0.3rem solid var(--brand-01); +} + .nestedAccordion :global { // extending carbon's accordion to handle nested accordions // accordion content @@ -55,16 +59,6 @@ fill: var(--brand-01); } - /** first node **/ - .bx--accordion--start > .bx--accordion__item > button[aria-expanded="true"] { - background-color: $ui-03; - } - - .bx--accordion--start > .bx--accordion__item > button > .bx--accordion__title > .bx--checkbox-wrapper > .bx--checkbox-label { - font-size: 110%; - font-weight: 700; - } - .bx--checkbox-label-text { padding: 0 0 0 .75rem; } diff --git a/packages/esm-patient-test-results-app/src/filter/filter-set.tsx b/packages/esm-patient-test-results-app/src/filter/filter-set.tsx index 057836d29e..2b6e29fe4f 100644 --- a/packages/esm-patient-test-results-app/src/filter/filter-set.tsx +++ b/packages/esm-patient-test-results-app/src/filter/filter-set.tsx @@ -5,23 +5,20 @@ import FilterContext from './filter-context'; interface Observation { display: string; + flatName: string; + hasData?: boolean; } -interface TreeNode { +export interface TreeNode { display: string; - datatype: string; + datatype?: string; subSets?: TreeNode[]; obs?: Observation[]; -} - -interface FilterProps { - maxNest?: number; - children?: React.ReactNode; + flatName: string; } interface FilterNodeProps { root: TreeNode; level: number; - maxNest?: number; } interface FilterLeafProps { @@ -32,59 +29,37 @@ const isIndeterminate = (kids, checkboxes) => { return kids && !kids?.every((kid) => checkboxes[kid]) && !kids?.every((kid) => !checkboxes[kid]); }; -const FilterSet = ({ maxNest }: FilterProps) => { - const { someChecked, parents, checkboxes, updateParent, root } = useContext(FilterContext); - const indeterminate = isIndeterminate(parents[root.display], checkboxes); - const allChildrenChecked = parents[root.display]?.every((kid) => checkboxes[kid]); +const FilterSet = () => { + const { root } = useContext(FilterContext); return ( -
- - updateParent(root.display)} - /> - } - > - {!root?.subSets?.[0]?.datatype && - root?.subSets?.map((node, index) => )} - {root?.subSets?.[0]?.datatype && root.subSets?.map((obs, index) => )} - - +
+
); }; -const FilterNode = ({ root, level, maxNest = 3 }: FilterNodeProps) => { +const FilterNode = ({ root, level }: FilterNodeProps) => { const { checkboxes, parents, updateParent } = useContext(FilterContext); - const indeterminate = isIndeterminate(parents[root.display], checkboxes); - const allChildrenChecked = parents[root.display]?.every((kid) => checkboxes[kid]); + const indeterminate = isIndeterminate(parents[root.flatName], checkboxes); + const allChildrenChecked = parents[root.flatName]?.every((kid) => checkboxes[kid]); return ( - + updateParent(root.display)} + labelText={`${root?.display} (${parents?.[root?.flatName]?.length})`} + onChange={() => updateParent(root.flatName)} /> } - style={{ paddingLeft: level > 0 && level < maxNest ? '1rem' : '0px' }} + style={{ paddingLeft: `${level > 0 ? 1 : 0}rem` }} > - {!root?.subSets?.[0]?.datatype && - root?.subSets?.map((node, index) => ( - - ))} - {root?.subSets?.[0]?.datatype && root.subSets?.map((obs, index) => )} + {!root?.subSets?.[0]?.obs && + root?.subSets?.map((node, index) => )} + {root?.subSets?.[0]?.obs && root.subSets?.map((obs, index) => )} ); @@ -97,8 +72,9 @@ const FilterLeaf = ({ leaf }: FilterLeafProps) => { toggleVal(leaf?.display)} + checked={checkboxes?.[leaf.flatName]} + onChange={() => toggleVal(leaf.flatName)} + disabled={!leaf.hasData} />
); diff --git a/packages/esm-patient-test-results-app/src/hiv/hiv-care-and-treatment.tsx b/packages/esm-patient-test-results-app/src/hiv/hiv-care-and-treatment.tsx index d8fb0ada32..f729a734db 100644 --- a/packages/esm-patient-test-results-app/src/hiv/hiv-care-and-treatment.tsx +++ b/packages/esm-patient-test-results-app/src/hiv/hiv-care-and-treatment.tsx @@ -2,9 +2,9 @@ import { Column, Grid, InlineLoading, Row } from 'carbon-components-react'; import React from 'react'; import FilterSet from '../filter/filter-set'; import { FilterProvider } from '../filter/filter-context'; -import { MultiTimeline } from '../timeline/Timeline'; +import NewTimeline from '../new-timeline/new-timeline'; import { EmptyState, ErrorState } from '@openmrs/esm-patient-common-lib'; -import useGetObstreeData from '../timeline/useObstreeData'; +import useGetObstreeData from '../new-timeline/useObstreeData'; interface obsShape { [key: string]: any; @@ -29,7 +29,7 @@ const HIVCareAndTreatment = () => { - +
diff --git a/packages/esm-patient-test-results-app/src/new-timeline/new-timeline.scss b/packages/esm-patient-test-results-app/src/new-timeline/new-timeline.scss new file mode 100644 index 0000000000..bedb10f506 --- /dev/null +++ b/packages/esm-patient-test-results-app/src/new-timeline/new-timeline.scss @@ -0,0 +1,202 @@ + +@import "~@openmrs/esm-styleguide/src/vars"; +@import "~carbon-components/src/globals/scss/vars"; +@import "~carbon-components/src/globals/scss/mixins"; + +.grid { + width: fit-content; + background-color: $color-gray-30; + display: grid; + grid-auto-rows: auto; + gap: 1px; + justify-items: center; + align-items: center; +} + +.day-column, .year-column { + @include carbon--type-style('productive-heading-01'); + color: $text-02; +} + +.time-column { + @include carbon--type-style('body-short-01'); + color: $text-02; + scroll-snap-align: start; +} + +.padded-main { + box-sizing: border-box; + padding: 20px; +} + +.timeline-cell { + background-color: white; + width: 100%; + height: 100%; + display: grid; + justify-items: center; + align-items: center; + padding: 0.5rem; +} + +.timeline-data-cell { + @extend .timeline-cell; + justify-items: first baseline; + + p { + @include carbon--type-style('body-short-01'); + color: $text-02; + } +} + +.row-start-cell { + @extend .timeline-cell; + position: sticky; + left: 0; + display: grid; + grid-auto-flow: row; + gap: 0.5px; + justify-items: baseline; + align-items: center; + padding: 1rem; +} + +.timeline-cell-zebra { + background-color: $ui-03; +} + +.padding-container { + padding: 0px 0 0 0; + box-sizing: border-box; + + display: grid; + grid-auto-flow: row; + column-gap: 1px; + row-gap: 0px; + background-color: $color-gray-30; + grid-template: auto auto / 9em auto; + border-collapse: collapse; +} + +.padding-container::-webkit-scrollbar { + display: none; +} + +.time-slot-inner { + background-color: $ui-03; + padding: 3px 10px; + justify-self: stretch; + align-self: stretch; + display: grid; + align-items: center; + + div { + position: sticky; + left: calc(10em + 10px); + width: max-content; + } +} + +.corner-grid-element { + grid-row: span 1; + position: sticky; + left: 0px; + top: 0px; + z-index: 3; + padding: 0rem 1rem; + + div { + @include carbon--type-style('productive-heading-01'); + color: $text-01; + left: 0; + } +} + +.shadow { + box-shadow: 8px 0 20px 0 rgba(0, 0, 0 , 0.15); +} + +.trendline-link { + @include carbon--type-style('body-short-01'); + color: $interactive-01; + cursor: pointer; +} + +.range-units { + @include carbon--type-style('helper-text-01'); + color: $text-05; +} + +.off-scale-high, +.off-scale-low, +.critically-high, +.critically-low, +.high, +.low { + p { + @include carbon--type-style('productive-heading-01'); + color: $text-01; + } +} + +.high, +.low { + box-shadow: 0 0 0 1px #000000; +} + +.critically-high, +.critically-low { + box-shadow: 0 0 0 1px $danger, inset 0 0 0 1px $danger; +} + +.off-scale-low { + p::after { + content: " ↓↓↓"; + } +} + +.off-scale-high { + p::after { + content: " ↑↑↑"; + } +} + +.critically-low { + p::after { + content: " ↓↓"; + } +} + +.critically-high { + p::after { + content: " ↑↑"; + } +} + +.low { + p::after { + content: " ↓"; + } +} + +.high { + p::after { + content: " ↑"; + } +} + +.shadow-box { + grid-row: 2 / -1; + grid-column: 1 / 2; +} + +.recent-results-grid { + display: grid; + background-color: white; + overflow: auto; + max-height: calc(100vh - 9rem); +} + +.recent-results-grid::-webkit-scrollbar { + display: none; +} diff --git a/packages/esm-patient-test-results-app/src/new-timeline/new-timeline.tsx b/packages/esm-patient-test-results-app/src/new-timeline/new-timeline.tsx new file mode 100644 index 0000000000..4f1150d412 --- /dev/null +++ b/packages/esm-patient-test-results-app/src/new-timeline/new-timeline.tsx @@ -0,0 +1,190 @@ +import React, { useContext } from 'react'; +import useScrollIndicator from '../timeline/useScroll'; +import { PaddingContainer, Grid, NewGridItems, ShadowBox } from '../timeline/helpers'; +import { EmptyState, OBSERVATION_INTERPRETATION } from '@openmrs/esm-patient-common-lib'; +import FilterContext from '../filter/filter-context'; +import styles from './new-timeline.scss'; + +const RecentResultsGrid = (props) => { + return
; +}; + +const TimeSlotsInner: React.FC<{ + style?: React.CSSProperties; + className?: string; +}> = ({ className, ...props }) => ( +
+); + +export const TimeSlots: React.FC<{ + style?: React.CSSProperties; + className?: string; +}> = ({ children = undefined, ...props }) => ( + +
{children}
+
+); + +interface PanelNameCornerProps { + showShadow: boolean; + panelName: string; +} + +const PanelNameCorner: React.FC = ({ showShadow, panelName }) => ( + + {panelName} + +); + +interface DataEntry { + value: number | string; + effectiveDateTime: string; + interpretation: OBSERVATION_INTERPRETATION; +} + +interface DataRow { + [_: string]: { + entries: Array; + display: string; + name: string; + type: string; + uuid: string; + units: string; + range: string; + }; +} + +export const NewRowStartCell = ({ title, range, units, shadow = false }) => ( +
+ {title} + + {range} {units} + +
+); + +interface NewDataRowsProps { + rowData: DataRow; + timeColumns: Array; + sortedTimes: Array; + showShadow: boolean; +} + +const NewDataRows: React.FC = ({ timeColumns, rowData, sortedTimes, showShadow }) => { + return ( + + {Object.values(rowData).map((row, rowCount) => { + const obs = row.entries; + const { units = '', range = '' } = row; + + return ( + + + + + ); + })} + + ); +}; + +interface DateHeaderGridProps { + timeColumns: Array; + yearColumns: Array>; + dayColumns: Array>; + showShadow: boolean; +} + +const DateHeaderGrid: React.FC = ({ timeColumns, yearColumns, dayColumns, showShadow }) => ( + + {yearColumns.map(({ year, size }) => { + return ( + + {year} + + ); + })} + {dayColumns.map(({ day, year, size }) => { + return ( + + {day} + + ); + })} + {timeColumns.map((time, i) => { + return ( + + {time} + + ); + })} + +); + +export const NewTimeline = () => { + const { activeTests, timelineData } = useContext(FilterContext); + const [xIsScrolled, yIsScrolled, containerRef] = useScrollIndicator(0, 32); + + const { + data: { + parsedTime: { yearColumns, dayColumns, timeColumns, sortedTimes }, + rowData, + panelName, + }, + loaded, + } = timelineData; + + if (rowData && Object.keys(rowData)?.length === 0) { + return ; + } + if (activeTests && timelineData && loaded) { + return ( + + + + + + + + + ); + } + return null; +}; + +export default NewTimeline; diff --git a/packages/esm-patient-test-results-app/src/timeline/useObstreeData.ts b/packages/esm-patient-test-results-app/src/new-timeline/useObstreeData.ts similarity index 76% rename from packages/esm-patient-test-results-app/src/timeline/useObstreeData.ts rename to packages/esm-patient-test-results-app/src/new-timeline/useObstreeData.ts index 0766f2e1e4..56281a8738 100644 --- a/packages/esm-patient-test-results-app/src/timeline/useObstreeData.ts +++ b/packages/esm-patient-test-results-app/src/new-timeline/useObstreeData.ts @@ -3,10 +3,15 @@ import { useMemo } from 'react'; import useSWR from 'swr'; import { assessValue, exist } from '../loadPatientTestData/helpers'; -const augmentObstreeData = (node) => { +export const getName = (prefix, name) => { + return prefix ? `${prefix}-${name}` : name; +}; + +const augmentObstreeData = (node, prefix) => { const outData = JSON.parse(JSON.stringify(node)); + outData.flatName = getName(prefix, node.display); if (outData?.subSets?.length) { - outData.subSets = outData.subSets.map((subNode) => augmentObstreeData(subNode)); + outData.subSets = outData.subSets.map((subNode) => augmentObstreeData(subNode, getName(prefix, node?.display))); } if (exist(outData?.hiNormal, outData?.lowNormal)) { outData.range = `${outData.lowNormal} – ${outData.hiNormal}`; @@ -14,6 +19,9 @@ const augmentObstreeData = (node) => { if (outData?.obs?.length) { const assess = assessValue(outData); outData.obs = outData.obs.map((ob) => ({ ...ob, interpretation: assess(ob.value) })); + outData.hasData = true; + } else { + outData.hasData = false; } return { ...outData }; }; @@ -24,7 +32,7 @@ const useGetObstreeData = (conceptUuid) => { const result = useMemo(() => { if (response.data) { const { data, ...rest } = response; - const newData = augmentObstreeData(data?.data); + const newData = augmentObstreeData(data?.data, ''); return { ...rest, loading: false, data: newData }; } else { return { diff --git a/packages/esm-patient-test-results-app/src/timeline/Timeline.tsx b/packages/esm-patient-test-results-app/src/timeline/Timeline.tsx index d4758c0ee8..239c09eac0 100644 --- a/packages/esm-patient-test-results-app/src/timeline/Timeline.tsx +++ b/packages/esm-patient-test-results-app/src/timeline/Timeline.tsx @@ -1,20 +1,10 @@ -import React, { useContext } from 'react'; +import React from 'react'; import { InlineLoading } from 'carbon-components-react'; import useScrollIndicator from './useScroll'; import { useTimelineData } from './useTimelineData'; -import { - PaddingContainer, - TimeSlots, - Grid, - RowStartCell, - NewRowStartCell, - GridItems, - NewGridItems, - ShadowBox, -} from './helpers'; -import { ObsRecord, EmptyState, ObsMetaInfo, OBSERVATION_INTERPRETATION } from '@openmrs/esm-patient-common-lib'; +import { PaddingContainer, TimeSlots, Grid, RowStartCell, GridItems, ShadowBox } from './helpers'; +import { ObsRecord, EmptyState, OBSERVATION_INTERPRETATION } from '@openmrs/esm-patient-common-lib'; import styles from './timeline.scss'; -import FilterContext from '../filter/filter-context'; const RecentResultsGrid = (props) => { return
; @@ -106,52 +96,6 @@ const DataRows: React.FC = ({ timeColumns, rowData, sortedTimes, ); -interface DataEntry { - value: number | string; - effectiveDateTime: string; - interpretation: OBSERVATION_INTERPRETATION; -} - -interface DataRow { - [_: string]: { - entries: Array; - name: string; - type: string; - uuid: string; - units: string; - range: string; - }; -} -interface NewDataRowsProps { - rowData: DataRow; - timeColumns: Array; - sortedTimes: Array; - showShadow: boolean; -} - -const NewDataRows: React.FC = ({ timeColumns, rowData, sortedTimes, showShadow }) => ( - - {Object.entries(rowData).map(([title, row], rowCount) => { - const obs = row.entries; - const { units = '', range = '' } = row; - - return ( - - - - - ); - })} - -); - interface TimelineParams { patientUuid: string; panelUuid?: string; @@ -217,49 +161,4 @@ export const Timeline: React.FC = ({ return ; }; -export const MultiTimeline = () => { - const { activeTests, timelineData } = useContext(FilterContext); - const [xIsScrolled, yIsScrolled, containerRef] = useScrollIndicator(0, 32); - - const { - data: { - parsedTime: { yearColumns, dayColumns, timeColumns, sortedTimes }, - rowData, - panelName, - }, - loaded, - } = timelineData; - - if (activeTests?.length === 0) { - return ; - } - if (activeTests && timelineData && loaded) { - return ( - - - - - - - - - ); - } - return null; -}; - export default Timeline; From f04bc9df1a3ff7c10fb02f874e96c5d7876d9ce6 Mon Sep 17 00:00:00 2001 From: Zac Butko Date: Tue, 1 Mar 2022 23:26:13 -0800 Subject: [PATCH 08/14] Grouped timelines --- .../src/filter/filter-context.tsx | 14 ++- .../src/filter/filter-reducer.ts | 17 ++- .../src/filter/filter-set.scss | 1 - .../src/new-timeline/new-timeline.tsx | 117 +++++++++++++----- .../src/timeline/helpers.tsx | 29 +---- 5 files changed, 109 insertions(+), 69 deletions(-) diff --git a/packages/esm-patient-test-results-app/src/filter/filter-context.tsx b/packages/esm-patient-test-results-app/src/filter/filter-context.tsx index f8b74028f6..c6be79eb9a 100644 --- a/packages/esm-patient-test-results-app/src/filter/filter-context.tsx +++ b/packages/esm-patient-test-results-app/src/filter/filter-context.tsx @@ -8,6 +8,7 @@ const initialState = { parents: {}, root: { display: '', flatName: '' }, tests: {}, + lowestParents: [], }; const initialContext = { @@ -15,6 +16,7 @@ const initialContext = { ...initialState, timelineData: {}, activeTests: [], + someChecked: false, initialize: () => {}, toggleVal: () => {}, updateParent: () => {}, @@ -31,8 +33,10 @@ interface FilterContextProps { parents: { [key: string]: string[] }; root: TreeNode; tests: { [key: string]: any }; + lowestParents: { display: string; flatName: string }[]; timelineData: { [key: string]: any }; activeTests: string[]; + someChecked: boolean; initialize: any; toggleVal: any; updateParent: any; @@ -69,10 +73,12 @@ const FilterProvider = ({ root, children }: FilterProviderProps) => { return Object.keys(state?.checkboxes)?.filter((key) => state.checkboxes[key]) || []; }, [state.checkboxes]); + const someChecked = Boolean(activeTests.length); + const timelineData = useMemo(() => { if (!state?.tests) { return { - data: { parsedTime: {} as ReturnType, rowData: {}, panelName: '' }, + data: { parsedTime: {} as ReturnType, rowData: [], panelName: '' }, loaded: false, }; } @@ -88,10 +94,10 @@ const FilterProvider = ({ root, children }: FilterProviderProps) => { ), ]; allTimes.sort((a, b) => (new Date(a) < new Date(b) ? 1 : -1)); - const rows = {}; + const rows = []; Object.keys(tests).forEach((test) => { const newEntries = allTimes.map((time: string) => tests[test].obs.find((entry) => entry.obsDatetime === time)); - rows[test] = { ...tests[test], entries: newEntries }; + rows.push({ ...tests[test], entries: newEntries }); }); const panelName = 'timeline'; return { @@ -114,8 +120,10 @@ const FilterProvider = ({ root, children }: FilterProviderProps) => { parents: state.parents, root: state.root, tests: state.tests, + lowestParents: state.lowestParents, timelineData, activeTests, + someChecked, initialize: actions.initialize, toggleVal: actions.toggleVal, updateParent: actions.updateParent, diff --git a/packages/esm-patient-test-results-app/src/filter/filter-reducer.ts b/packages/esm-patient-test-results-app/src/filter/filter-reducer.ts index ad0a6bf524..50d06630eb 100644 --- a/packages/esm-patient-test-results-app/src/filter/filter-reducer.ts +++ b/packages/esm-patient-test-results-app/src/filter/filter-reducer.ts @@ -4,9 +4,11 @@ export const getName = (prefix, name) => { const computeParents = (prefix, node) => { var parents = {}; - const leaves = []; - const tests = []; + var leaves = []; + var tests = []; + var lowestParents = []; if (node?.subSets?.length && node.subSets[0].obs) { + // lowest parent let activeLeaves = []; node.subSets.forEach((leaf) => { if (leaf.hasData) { @@ -16,38 +18,41 @@ const computeParents = (prefix, node) => { let activeTests = []; node.subSets.forEach((leaf) => { if (leaf.obs.length) { - const { flatName, ...rest } = leaf; - activeTests.push([flatName, rest]); + activeTests.push([leaf.flatName, leaf]); } }); leaves.push(...activeLeaves); tests.push(...activeTests); + lowestParents.push({ flatName: node.flatName, display: node.display }); } else if (node?.subSets?.length) { node.subSets.map((subNode) => { const { parents: newParents, leaves: newLeaves, tests: newTests, + lowestParents: newLowestParents, } = computeParents(getName(prefix, node.display), subNode); parents = { ...parents, ...newParents }; leaves.push(...newLeaves); tests.push(...newTests); + lowestParents.push(...newLowestParents); }); } parents[node.flatName] = leaves; - return { parents, leaves, tests }; + return { parents, leaves, tests, lowestParents }; }; const reducer = (state, action) => { switch (action.type) { case 'initialize': - const { parents, leaves, tests } = computeParents('', action.tree); + const { parents, leaves, tests, lowestParents } = computeParents('', action.tree); const flatTests = Object.fromEntries(tests); return { checkboxes: Object.fromEntries(leaves?.map((leaf) => [leaf, false])) || {}, parents: parents, root: action.tree, tests: flatTests, + lowestParents: lowestParents, }; case 'toggleVal': return { diff --git a/packages/esm-patient-test-results-app/src/filter/filter-set.scss b/packages/esm-patient-test-results-app/src/filter/filter-set.scss index 0503139198..3a090e1770 100644 --- a/packages/esm-patient-test-results-app/src/filter/filter-set.scss +++ b/packages/esm-patient-test-results-app/src/filter/filter-set.scss @@ -17,7 +17,6 @@ .filterItem { padding: 0.5rem 1rem 0.5rem 4rem; - border-bottom: 1px solid $ui-03; } .nestedAccordion > :global(.bx--accordion--start) > :global(.bx--accordion__item--active) { diff --git a/packages/esm-patient-test-results-app/src/new-timeline/new-timeline.tsx b/packages/esm-patient-test-results-app/src/new-timeline/new-timeline.tsx index 4f1150d412..3f929b66e9 100644 --- a/packages/esm-patient-test-results-app/src/new-timeline/new-timeline.tsx +++ b/packages/esm-patient-test-results-app/src/new-timeline/new-timeline.tsx @@ -1,6 +1,6 @@ import React, { useContext } from 'react'; import useScrollIndicator from '../timeline/useScroll'; -import { PaddingContainer, Grid, NewGridItems, ShadowBox } from '../timeline/helpers'; +import { PaddingContainer, Grid, ShadowBox } from '../timeline/helpers'; import { EmptyState, OBSERVATION_INTERPRETATION } from '@openmrs/esm-patient-common-lib'; import FilterContext from '../filter/filter-context'; import styles from './new-timeline.scss'; @@ -9,20 +9,13 @@ const RecentResultsGrid = (props) => { return
; }; -const TimeSlotsInner: React.FC<{ +const TimeSlots: React.FC<{ style?: React.CSSProperties; className?: string; -}> = ({ className, ...props }) => ( -
-); - -export const TimeSlots: React.FC<{ - style?: React.CSSProperties; - className?: string; -}> = ({ children = undefined, ...props }) => ( - +}> = ({ children = undefined, className, ...props }) => ( +
{children}
- +
); interface PanelNameCornerProps { @@ -43,18 +36,16 @@ interface DataEntry { } interface DataRow { - [_: string]: { - entries: Array; - display: string; - name: string; - type: string; - uuid: string; - units: string; - range: string; - }; + entries: Array; + display: string; + name: string; + type: string; + uuid: string; + units: string; + range: string; } -export const NewRowStartCell = ({ title, range, units, shadow = false }) => ( +const NewRowStartCell = ({ title, range, units, shadow = false }) => (
(
); +const interpretationToCSS = { + OFF_SCALE_HIGH: 'off-scale-high', + CRITICALLY_HIGH: 'critically-high', + HIGH: 'high', + OFF_SCALE_LOW: 'off-scale-low', + CRITICALLY_LOW: 'critically-low', + LOW: 'low', + NORMAL: '', +}; + +const TimelineCell: React.FC<{ + text: string; + interpretation?: OBSERVATION_INTERPRETATION; + zebra: boolean; +}> = ({ text, interpretation = 'NORMAL', zebra }) => { + const additionalClassname: string = interpretationToCSS[interpretation] + ? styles[interpretationToCSS[interpretation]] + : ''; + + return ( +
+

{text}

+
+ ); +}; + +const NewGridItems = React.memo<{ + sortedTimes: Array; + obs: any; + zebra: boolean; +}>(({ sortedTimes, obs, zebra }) => ( + <> + {sortedTimes.map((_, i) => { + if (!obs[i]) return ; + return ; + })} + +)); + interface NewDataRowsProps { - rowData: DataRow; + rowData: DataRow[]; timeColumns: Array; sortedTimes: Array; showShadow: boolean; } const NewDataRows: React.FC = ({ timeColumns, rowData, sortedTimes, showShadow }) => { + console.log('rowData??', rowData); return ( - {Object.values(rowData).map((row, rowCount) => { + {rowData.map((row, index) => { const obs = row.entries; const { units = '', range = '' } = row; - + console.log('this should print?'); return ( - + = ({ timeColumns, rowData, sortedT shadow: showShadow, }} /> - + ); })} @@ -143,7 +176,7 @@ const DateHeaderGrid: React.FC = ({ timeColumns, yearColumn ); export const NewTimeline = () => { - const { activeTests, timelineData } = useContext(FilterContext); + const { activeTests, timelineData, parents, checkboxes, someChecked, lowestParents } = useContext(FilterContext); const [xIsScrolled, yIsScrolled, containerRef] = useScrollIndicator(0, 32); const { @@ -155,7 +188,7 @@ export const NewTimeline = () => { loaded, } = timelineData; - if (rowData && Object.keys(rowData)?.length === 0) { + if (rowData && rowData?.length === 0) { return ; } if (activeTests && timelineData && loaded) { @@ -171,14 +204,36 @@ export const NewTimeline = () => { showShadow: yIsScrolled, }} /> - { + if (parents[parent.flatName].some((kid) => checkboxes[kid]) || !someChecked) { + const subRows = someChecked + ? rowData?.filter((row) => parents[parent.flatName].includes(row.flatName) && checkboxes[row.flatName]) + : rowData?.filter((row) => parents[parent.flatName].includes(row.flatName)); + + // show kid rows + return ( + <> +
{parent.display}
+ + + ); + } else return null; + })} + {/* + /> */} diff --git a/packages/esm-patient-test-results-app/src/timeline/helpers.tsx b/packages/esm-patient-test-results-app/src/timeline/helpers.tsx index 4604e4c733..6b015a7caf 100644 --- a/packages/esm-patient-test-results-app/src/timeline/helpers.tsx +++ b/packages/esm-patient-test-results-app/src/timeline/helpers.tsx @@ -33,7 +33,7 @@ export const Main: React.FC = () =>
; export const ShadowBox: React.FC = () =>
; -const TimelineCell: React.FC<{ +export const TimelineCell: React.FC<{ text: string; interpretation?: OBSERVATION_INTERPRETATION; zebra: boolean; @@ -95,20 +95,6 @@ export const RowStartCell = ({ title, range, units, shadow = false, openTrendlin
); -export const NewRowStartCell = ({ title, range, units, shadow = false }) => ( -
- {title} - - {range} {units} - -
-); - export const TimeSlots: React.FC<{ style?: React.CSSProperties; className?: string; @@ -131,16 +117,3 @@ export const GridItems = React.memo<{ })} )); - -export const NewGridItems = React.memo<{ - sortedTimes: Array; - obs: any; - zebra: boolean; -}>(({ sortedTimes, obs, zebra }) => ( - <> - {sortedTimes.map((_, i) => { - if (!obs[i]) return ; - return ; - })} - -)); From 030ca2d0be94c2447e74dba73399a8f6b6c36f9b Mon Sep 17 00:00:00 2001 From: Zac Butko Date: Thu, 3 Mar 2022 00:09:23 -0800 Subject: [PATCH 09/14] Some good style changes --- .../src/filter/filter-set.scss | 17 +++++++ .../src/filter/filter-set.tsx | 6 ++- .../src/hiv/hiv-care-and-treatment.tsx | 48 ++++++++++++++----- .../src/new-timeline/new-timeline.tsx | 2 - 4 files changed, 56 insertions(+), 17 deletions(-) diff --git a/packages/esm-patient-test-results-app/src/filter/filter-set.scss b/packages/esm-patient-test-results-app/src/filter/filter-set.scss index 3a090e1770..3e2b970c31 100644 --- a/packages/esm-patient-test-results-app/src/filter/filter-set.scss +++ b/packages/esm-patient-test-results-app/src/filter/filter-set.scss @@ -2,6 +2,22 @@ @import "~carbon-components/src/globals/scss/vars"; @import "~carbon-components/src/globals/scss/mixins"; +.floatingRightButton { + position: absolute; + top: 0px; + right: 0px; + z-index: 9; +} +.expandButton { + background-color: white; + border: 1px solid black; +} + +.floatingRightButton > .expandButton > svg { + margin: 0; +} + + .filterContainer { background-color: $openmrs-background-grey; margin: $spacing-02 0; @@ -62,3 +78,4 @@ padding: 0 0 0 .75rem; } } + diff --git a/packages/esm-patient-test-results-app/src/filter/filter-set.tsx b/packages/esm-patient-test-results-app/src/filter/filter-set.tsx index 2b6e29fe4f..282a53d3ba 100644 --- a/packages/esm-patient-test-results-app/src/filter/filter-set.tsx +++ b/packages/esm-patient-test-results-app/src/filter/filter-set.tsx @@ -19,6 +19,7 @@ export interface TreeNode { interface FilterNodeProps { root: TreeNode; level: number; + open?: boolean; } interface FilterLeafProps { @@ -34,12 +35,12 @@ const FilterSet = () => { return (
- +
); }; -const FilterNode = ({ root, level }: FilterNodeProps) => { +const FilterNode = ({ root, level, open }: FilterNodeProps) => { const { checkboxes, parents, updateParent } = useContext(FilterContext); const indeterminate = isIndeterminate(parents[root.flatName], checkboxes); const allChildrenChecked = parents[root.flatName]?.every((kid) => checkboxes[kid]); @@ -56,6 +57,7 @@ const FilterNode = ({ root, level }: FilterNodeProps) => { /> } style={{ paddingLeft: `${level > 0 ? 1 : 0}rem` }} + open={open ?? false} > {!root?.subSets?.[0]?.obs && root?.subSets?.map((node, index) => )} diff --git a/packages/esm-patient-test-results-app/src/hiv/hiv-care-and-treatment.tsx b/packages/esm-patient-test-results-app/src/hiv/hiv-care-and-treatment.tsx index f729a734db..1a8ca12299 100644 --- a/packages/esm-patient-test-results-app/src/hiv/hiv-care-and-treatment.tsx +++ b/packages/esm-patient-test-results-app/src/hiv/hiv-care-and-treatment.tsx @@ -1,18 +1,26 @@ -import { Column, Grid, InlineLoading, Row } from 'carbon-components-react'; -import React from 'react'; +import { Button, Column, Grid, InlineLoading, Row } from 'carbon-components-react'; +import React, { useContext, useState } from 'react'; import FilterSet from '../filter/filter-set'; -import { FilterProvider } from '../filter/filter-context'; +import FilterContext, { FilterProvider } from '../filter/filter-context'; import NewTimeline from '../new-timeline/new-timeline'; import { EmptyState, ErrorState } from '@openmrs/esm-patient-common-lib'; import useGetObstreeData from '../new-timeline/useObstreeData'; +import { Maximize32, Minimize32 } from '@carbon/icons-react'; +import styles from '../filter/filter-set.scss'; interface obsShape { [key: string]: any; } +const Results = () => { + const { checkboxes } = useContext(FilterContext); + return

Results ({Object.keys(checkboxes).length})

; +}; + const HIVCareAndTreatment = () => { const concept = '5035a431-51de-40f0-8f25-4a98762eb796'; // bloodwork const { data: root, error, loading }: obsShape = useGetObstreeData(concept); + const [expanded, setExpanded] = useState(false); if (loading) { return ; @@ -23,16 +31,30 @@ const HIVCareAndTreatment = () => { if (!loading && !error && root?.display && root?.subSets?.length) { return ( - - - - - - - - - - +
+
+ +
+
+ + + + + + + + + + + +
+
); } diff --git a/packages/esm-patient-test-results-app/src/new-timeline/new-timeline.tsx b/packages/esm-patient-test-results-app/src/new-timeline/new-timeline.tsx index 3f929b66e9..f82b0b387c 100644 --- a/packages/esm-patient-test-results-app/src/new-timeline/new-timeline.tsx +++ b/packages/esm-patient-test-results-app/src/new-timeline/new-timeline.tsx @@ -108,13 +108,11 @@ interface NewDataRowsProps { } const NewDataRows: React.FC = ({ timeColumns, rowData, sortedTimes, showShadow }) => { - console.log('rowData??', rowData); return ( {rowData.map((row, index) => { const obs = row.entries; const { units = '', range = '' } = row; - console.log('this should print?'); return ( Date: Thu, 3 Mar 2022 00:43:58 -0800 Subject: [PATCH 10/14] Small progress towards multi-timeline --- .../src/new-timeline/new-timeline.scss | 2 + .../src/new-timeline/new-timeline.tsx | 70 ++++++++++--------- 2 files changed, 40 insertions(+), 32 deletions(-) diff --git a/packages/esm-patient-test-results-app/src/new-timeline/new-timeline.scss b/packages/esm-patient-test-results-app/src/new-timeline/new-timeline.scss index bedb10f506..687c2bc5b2 100644 --- a/packages/esm-patient-test-results-app/src/new-timeline/new-timeline.scss +++ b/packages/esm-patient-test-results-app/src/new-timeline/new-timeline.scss @@ -109,6 +109,8 @@ @include carbon--type-style('productive-heading-01'); color: $text-01; left: 0; + width: 100%; + word-wrap: normal; } } diff --git a/packages/esm-patient-test-results-app/src/new-timeline/new-timeline.tsx b/packages/esm-patient-test-results-app/src/new-timeline/new-timeline.tsx index f82b0b387c..754425c492 100644 --- a/packages/esm-patient-test-results-app/src/new-timeline/new-timeline.tsx +++ b/packages/esm-patient-test-results-app/src/new-timeline/new-timeline.tsx @@ -1,4 +1,4 @@ -import React, { useContext } from 'react'; +import React, { useContext, useState } from 'react'; import useScrollIndicator from '../timeline/useScroll'; import { PaddingContainer, Grid, ShadowBox } from '../timeline/helpers'; import { EmptyState, OBSERVATION_INTERPRETATION } from '@openmrs/esm-patient-common-lib'; @@ -176,6 +176,7 @@ const DateHeaderGrid: React.FC = ({ timeColumns, yearColumn export const NewTimeline = () => { const { activeTests, timelineData, parents, checkboxes, someChecked, lowestParents } = useContext(FilterContext); const [xIsScrolled, yIsScrolled, containerRef] = useScrollIndicator(0, 32); + const [currentPanel, setCurrentPanel] = useState(lowestParents?.[0]?.display || 'Timeline'); const { data: { @@ -191,17 +192,23 @@ export const NewTimeline = () => { } if (activeTests && timelineData && loaded) { return ( - - - - + <> +
+ + + + + + +
+
{lowestParents.map((parent) => { if (parents[parent.flatName].some((kid) => checkboxes[kid]) || !someChecked) { const subRows = someChecked @@ -211,30 +218,29 @@ export const NewTimeline = () => { // show kid rows return ( <> -
{parent.display}
- + + +
+
{parent.display}
+
+ + +
+
+
); } else return null; })} - {/* */} - - - +
+ ); } return null; From 6cfb598d71d48162c0171df3bdae6a6feda701d8 Mon Sep 17 00:00:00 2001 From: Zac Butko Date: Wed, 9 Mar 2022 08:56:55 -0800 Subject: [PATCH 11/14] X scroll synchronized --- .../src/hiv/hiv-care-and-treatment.tsx | 71 +++--- .../src/new-timeline/new-timeline.scss | 69 +++++- .../src/new-timeline/new-timeline.tsx | 230 ++++++++++++------ 3 files changed, 243 insertions(+), 127 deletions(-) diff --git a/packages/esm-patient-test-results-app/src/hiv/hiv-care-and-treatment.tsx b/packages/esm-patient-test-results-app/src/hiv/hiv-care-and-treatment.tsx index 1a8ca12299..9c231a8343 100644 --- a/packages/esm-patient-test-results-app/src/hiv/hiv-care-and-treatment.tsx +++ b/packages/esm-patient-test-results-app/src/hiv/hiv-care-and-treatment.tsx @@ -1,26 +1,22 @@ -import { Button, Column, Grid, InlineLoading, Row } from 'carbon-components-react'; -import React, { useContext, useState } from 'react'; +import { Column, ContentSwitcher, InlineLoading, Row, Switch } from 'carbon-components-react'; +import React, { useState } from 'react'; import FilterSet from '../filter/filter-set'; -import FilterContext, { FilterProvider } from '../filter/filter-context'; +import { FilterProvider } from '../filter/filter-context'; import NewTimeline from '../new-timeline/new-timeline'; import { EmptyState, ErrorState } from '@openmrs/esm-patient-common-lib'; import useGetObstreeData from '../new-timeline/useObstreeData'; -import { Maximize32, Minimize32 } from '@carbon/icons-react'; -import styles from '../filter/filter-set.scss'; interface obsShape { [key: string]: any; } -const Results = () => { - const { checkboxes } = useContext(FilterContext); - return

Results ({Object.keys(checkboxes).length})

; -}; - const HIVCareAndTreatment = () => { const concept = '5035a431-51de-40f0-8f25-4a98762eb796'; // bloodwork const { data: root, error, loading }: obsShape = useGetObstreeData(concept); - const [expanded, setExpanded] = useState(false); + + const [view, setView] = useState('split'); + + const expanded = view === 'full'; if (loading) { return ; @@ -31,29 +27,36 @@ const HIVCareAndTreatment = () => { if (!loading && !error && root?.display && root?.subSets?.length) { return ( -
-
- -
-
- - - - - - - - - - - -
+
+ + +
+

Results

+
+ + + + +
+
+
+ +
+ setView(`${e.name}`)}> + + + +
+
+
+ + + + + + + +
); diff --git a/packages/esm-patient-test-results-app/src/new-timeline/new-timeline.scss b/packages/esm-patient-test-results-app/src/new-timeline/new-timeline.scss index 687c2bc5b2..12873ec71b 100644 --- a/packages/esm-patient-test-results-app/src/new-timeline/new-timeline.scss +++ b/packages/esm-patient-test-results-app/src/new-timeline/new-timeline.scss @@ -49,10 +49,33 @@ } } +.recent-results-grid { + display: grid; + background-color: white; + overflow-x: auto; + position: sticky; + top: 0; +} + +.recent-results-grid::-webkit-scrollbar { + display: none; +} + + +.row-header { + @extend .timeline-cell; + justify-items: baseline; + align-items: center; + background-color: $color-gray-30; + grid-column: span 2 / auto; +} + .row-start-cell { @extend .timeline-cell; + position: -webkit-sticky; /* Safari */ position: sticky; left: 0; + top: auto; display: grid; grid-auto-flow: row; gap: 0.5px; @@ -65,8 +88,35 @@ background-color: $ui-03; } -.padding-container { - padding: 0px 0 0 0; +.date-header { + position: sticky; + top: 45px; + display: grid; + background-color: white; + overflow-x: auto; + z-index: 9; +} + +.date-header::-webkit-scrollbar { + display: none; +} + +.date-header-container { + position: sticky; + top: 0; + padding: 0; + box-sizing: border-box; + display: grid; + grid-auto-flow: row; + column-gap: 1px; + row-gap: 0px; + background-color: $color-gray-30; + grid-template: auto auto / 9em auto; + border-collapse: collapse; +} + +.grid-container { + padding: 0; box-sizing: border-box; display: grid; @@ -78,7 +128,10 @@ border-collapse: collapse; } -.padding-container::-webkit-scrollbar { +.date-header-container::-webkit-scrollbar { + display: none; +} +.grid-container::-webkit-scrollbar { display: none; } @@ -192,13 +245,3 @@ grid-column: 1 / 2; } -.recent-results-grid { - display: grid; - background-color: white; - overflow: auto; - max-height: calc(100vh - 9rem); -} - -.recent-results-grid::-webkit-scrollbar { - display: none; -} diff --git a/packages/esm-patient-test-results-app/src/new-timeline/new-timeline.tsx b/packages/esm-patient-test-results-app/src/new-timeline/new-timeline.tsx index 754425c492..a2097f0520 100644 --- a/packages/esm-patient-test-results-app/src/new-timeline/new-timeline.tsx +++ b/packages/esm-patient-test-results-app/src/new-timeline/new-timeline.tsx @@ -1,13 +1,9 @@ -import React, { useContext, useState } from 'react'; -import useScrollIndicator from '../timeline/useScroll'; -import { PaddingContainer, Grid, ShadowBox } from '../timeline/helpers'; +import React, { useCallback, useContext, useEffect, useRef, useState } from 'react'; +import { Grid, ShadowBox } from '../timeline/helpers'; import { EmptyState, OBSERVATION_INTERPRETATION } from '@openmrs/esm-patient-common-lib'; import FilterContext from '../filter/filter-context'; import styles from './new-timeline.scss'; - -const RecentResultsGrid = (props) => { - return
; -}; +import { makeThrottled } from '../helpers'; const TimeSlots: React.FC<{ style?: React.CSSProperties; @@ -136,53 +132,139 @@ interface DateHeaderGridProps { yearColumns: Array>; dayColumns: Array>; showShadow: boolean; + xScroll: number; + setXScroll: any; } -const DateHeaderGrid: React.FC = ({ timeColumns, yearColumns, dayColumns, showShadow }) => ( - - {yearColumns.map(({ year, size }) => { - return ( - - {year} - - ); - })} - {dayColumns.map(({ day, year, size }) => { - return ( - - {day} - - ); - })} - {timeColumns.map((time, i) => { - return ( - - {time} - - ); - })} - -); +const DateHeaderGrid: React.FC = ({ + timeColumns, + yearColumns, + dayColumns, + showShadow, + xScroll, + setXScroll, +}) => { + const ref = useRef(); + const el: HTMLElement | null = ref.current; + + if (el) { + el.scrollLeft = xScroll; + } + + const handleScroll = useCallback( + (e) => { + setXScroll(e.target.scrollLeft); + }, + [setXScroll], + ); + + useEffect(() => { + const div: HTMLElement | null = ref.current; + if (div) { + div.addEventListener('scroll', handleScroll); + return () => div.removeEventListener('scroll', handleScroll); + } + }, [handleScroll]); + + return ( +
+ + {yearColumns.map(({ year, size }) => { + return ( + + {year} + + ); + })} + {dayColumns.map(({ day, year, size }) => { + return ( + + {day} + + ); + })} + {timeColumns.map((time, i) => { + return ( + + {time} + + ); + })} + +
+ ); +}; + +const TimelineDataGroup = ({ parent, subRows, xScroll, setXScroll }) => { + const { timelineData } = useContext(FilterContext); + const { + data: { + parsedTime: { timeColumns, sortedTimes }, + rowData, + }, + } = timelineData; + + const ref = useRef(); + + const el: HTMLElement | null = ref.current; + + if (el) { + el.scrollLeft = xScroll; + } + + const handleScroll = makeThrottled((e) => { + setXScroll(e.target.scrollLeft); + }, 200); + + useEffect(() => { + const div: HTMLElement | null = ref.current; + if (div) { + div.addEventListener('scroll', handleScroll); + return () => div.removeEventListener('scroll', handleScroll); + } + }, [handleScroll]); + + return ( + <> +
+
+
+
{parent.display}
+
+ + +
+
+
+ + ); +}; export const NewTimeline = () => { const { activeTests, timelineData, parents, checkboxes, someChecked, lowestParents } = useContext(FilterContext); - const [xIsScrolled, yIsScrolled, containerRef] = useScrollIndicator(0, 32); const [currentPanel, setCurrentPanel] = useState(lowestParents?.[0]?.display || 'Timeline'); + const [xScroll, setXScroll] = useState(0); const { data: { - parsedTime: { yearColumns, dayColumns, timeColumns, sortedTimes }, + parsedTime: { yearColumns, dayColumns, timeColumns }, rowData, - panelName, }, loaded, } = timelineData; @@ -192,24 +274,24 @@ export const NewTimeline = () => { } if (activeTests && timelineData && loaded) { return ( - <> -
- - - - - - +
+
+
+ + +
- {lowestParents.map((parent) => { + {lowestParents?.map((parent, index) => { if (parents[parent.flatName].some((kid) => checkboxes[kid]) || !someChecked) { const subRows = someChecked ? rowData?.filter((row) => parents[parent.flatName].includes(row.flatName) && checkboxes[row.flatName]) @@ -217,30 +299,18 @@ export const NewTimeline = () => { // show kid rows return ( - <> - - -
-
{parent.display}
-
- - -
-
-
- + ); } else return null; })}
- +
); } return null; From 2338c757e35c6f37f0a36dce6633ab56a8b79d06 Mon Sep 17 00:00:00 2001 From: Zac Butko Date: Wed, 9 Mar 2022 17:38:33 -0800 Subject: [PATCH 12/14] Group headers stay fixed --- .../src/hiv/hiv-care-and-treatment.tsx | 9 +- .../src/new-timeline/new-timeline.scss | 88 +++++++++++-------- .../src/new-timeline/new-timeline.tsx | 49 +++++++++-- 3 files changed, 97 insertions(+), 49 deletions(-) diff --git a/packages/esm-patient-test-results-app/src/hiv/hiv-care-and-treatment.tsx b/packages/esm-patient-test-results-app/src/hiv/hiv-care-and-treatment.tsx index 9c231a8343..8b3e5a6b77 100644 --- a/packages/esm-patient-test-results-app/src/hiv/hiv-care-and-treatment.tsx +++ b/packages/esm-patient-test-results-app/src/hiv/hiv-care-and-treatment.tsx @@ -5,6 +5,7 @@ import { FilterProvider } from '../filter/filter-context'; import NewTimeline from '../new-timeline/new-timeline'; import { EmptyState, ErrorState } from '@openmrs/esm-patient-common-lib'; import useGetObstreeData from '../new-timeline/useObstreeData'; +import styles from '../new-timeline/new-timeline.scss'; interface obsShape { [key: string]: any; @@ -28,7 +29,7 @@ const HIVCareAndTreatment = () => { return (
- +

Results

@@ -49,11 +50,11 @@ const HIVCareAndTreatment = () => {
- - + + - + diff --git a/packages/esm-patient-test-results-app/src/new-timeline/new-timeline.scss b/packages/esm-patient-test-results-app/src/new-timeline/new-timeline.scss index 12873ec71b..03652af247 100644 --- a/packages/esm-patient-test-results-app/src/new-timeline/new-timeline.scss +++ b/packages/esm-patient-test-results-app/src/new-timeline/new-timeline.scss @@ -3,6 +3,32 @@ @import "~carbon-components/src/globals/scss/vars"; @import "~carbon-components/src/globals/scss/mixins"; +.results-header { + padding: 0.8rem 0; + //position: sticky; + //top: 45px; + z-index: 10; + background-color: $openmrs-background-grey; +} + +.column-panel { + overflow-y: scroll; + height: calc(100vh - 9rem) +} + +.date-header { + position: sticky; + top: 0px; + display: grid; + background-color: white; + overflow-x: auto; + z-index: 9; +} + +.date-header::-webkit-scrollbar { + display: none; +} + .grid { width: fit-content; background-color: $color-gray-30; @@ -49,27 +75,31 @@ } } -.recent-results-grid { + +.grid-container { + padding: 0; + box-sizing: border-box; display: grid; - background-color: white; + grid-auto-flow: row; + column-gap: 1px; + row-gap: 0px; + background-color: $color-gray-30; + grid-template: auto auto / 9em auto; + border-collapse: collapse; overflow-x: auto; - position: sticky; - top: 0; -} - -.recent-results-grid::-webkit-scrollbar { - display: none; + width: 100%; + background-color: white; } .row-header { - @extend .timeline-cell; - justify-items: baseline; - align-items: center; - background-color: $color-gray-30; - grid-column: span 2 / auto; + overflow: hidden; + align-self: flex-start; + background-color: $ui-03; + padding: 0.6rem 1rem; } + .row-start-cell { @extend .timeline-cell; position: -webkit-sticky; /* Safari */ @@ -88,18 +118,6 @@ background-color: $ui-03; } -.date-header { - position: sticky; - top: 45px; - display: grid; - background-color: white; - overflow-x: auto; - z-index: 9; -} - -.date-header::-webkit-scrollbar { - display: none; -} .date-header-container { position: sticky; @@ -115,19 +133,17 @@ border-collapse: collapse; } -.grid-container { - padding: 0; - box-sizing: border-box; - - display: grid; - grid-auto-flow: row; - column-gap: 1px; - row-gap: 0px; - background-color: $color-gray-30; - grid-template: auto auto / 9em auto; - border-collapse: collapse; +.date-header-inner { + overflow-x: auto; +} +.date-header-inner::-webkit-scrollbar { + display: none; } + +.date-header::-webkit-scrollbar { + display: none; +} .date-header-container::-webkit-scrollbar { display: none; } diff --git a/packages/esm-patient-test-results-app/src/new-timeline/new-timeline.tsx b/packages/esm-patient-test-results-app/src/new-timeline/new-timeline.tsx index a2097f0520..6845ea05a8 100644 --- a/packages/esm-patient-test-results-app/src/new-timeline/new-timeline.tsx +++ b/packages/esm-patient-test-results-app/src/new-timeline/new-timeline.tsx @@ -167,7 +167,7 @@ const DateHeaderGrid: React.FC = ({ }, [handleScroll]); return ( -
+
= ({ ); }; -const TimelineDataGroup = ({ parent, subRows, xScroll, setXScroll }) => { +const TimelineDataGroup = ({ parent, subRows, xScroll, setXScroll, panelName, setPanelName, groupNumber }) => { const { timelineData } = useContext(FilterContext); const { data: { @@ -214,8 +214,12 @@ const TimelineDataGroup = ({ parent, subRows, xScroll, setXScroll }) => { } = timelineData; const ref = useRef(); + const titleRef = useRef(); const el: HTMLElement | null = ref.current; + if (groupNumber === 1 && panelName === '') { + setPanelName(parent.display); + } if (el) { el.scrollLeft = xScroll; @@ -233,13 +237,31 @@ const TimelineDataGroup = ({ parent, subRows, xScroll, setXScroll }) => { } }, [handleScroll]); + const onIntersect = (entries, observer) => { + entries.forEach((entry) => { + if (entry.intersectionRatio > 0.5) { + //setPanelName(parent.display); + } + }); + }; + + const observer = new IntersectionObserver(onIntersect, { + root: null, + threshold: 0.5, + }); + if (titleRef.current) { + observer.observe(titleRef.current); + } + return ( <> -
-
+
+ {groupNumber > 1 && (
-
{parent.display}
+
{parent.display}
+ )} +
{ export const NewTimeline = () => { const { activeTests, timelineData, parents, checkboxes, someChecked, lowestParents } = useContext(FilterContext); - const [currentPanel, setCurrentPanel] = useState(lowestParents?.[0]?.display || 'Timeline'); + const [panelName, setPanelName] = useState(''); const [xScroll, setXScroll] = useState(0); + let shownGroups = 0; const { data: { @@ -269,15 +292,19 @@ export const NewTimeline = () => { loaded, } = timelineData; + useEffect(() => { + setPanelName(''); + }, [rowData]); + if (rowData && rowData?.length === 0) { return ; } if (activeTests && timelineData && loaded) { return ( -
+ <>
- + {
{lowestParents?.map((parent, index) => { if (parents[parent.flatName].some((kid) => checkboxes[kid]) || !someChecked) { + shownGroups += 1; const subRows = someChecked ? rowData?.filter((row) => parents[parent.flatName].includes(row.flatName) && checkboxes[row.flatName]) : rowData?.filter((row) => parents[parent.flatName].includes(row.flatName)); @@ -305,12 +333,15 @@ export const NewTimeline = () => { key={index} xScroll={xScroll} setXScroll={setXScroll} + panelName={panelName} + setPanelName={setPanelName} + groupNumber={shownGroups} /> ); } else return null; })}
-
+ ); } return null; From b3dd430b42efeef66d7fecf5705a79c4b64a8119 Mon Sep 17 00:00:00 2001 From: Zac Butko Date: Thu, 10 Mar 2022 00:27:56 -0800 Subject: [PATCH 13/14] Supports config, multiple concepts at once --- .../src/config-schema.ts | 36 +++++++++++++++- .../src/filter/filter-context.tsx | 20 ++++----- .../src/filter/filter-reducer.ts | 20 ++++++++- .../src/filter/filter-set.scss | 5 +-- .../src/filter/filter-set.tsx | 18 +++++--- .../src/hiv/hiv-care-and-treatment.tsx | 35 +++++++++++----- .../src/new-timeline/useObstreeData.ts | 41 +++++++++++++++++++ 7 files changed, 140 insertions(+), 35 deletions(-) diff --git a/packages/esm-patient-test-results-app/src/config-schema.ts b/packages/esm-patient-test-results-app/src/config-schema.ts index 152465255c..6c57f004b5 100644 --- a/packages/esm-patient-test-results-app/src/config-schema.ts +++ b/packages/esm-patient-test-results-app/src/config-schema.ts @@ -1,3 +1,35 @@ -export const configSchema = {}; +import { Type } from '@openmrs/esm-framework'; -export interface ConfigObject {} +export const configSchema = { + concepts: { + _type: Type.Array, + _elements: { + conceptUuid: { + _type: Type.UUID, + _description: 'UUID of concept to load from /obstree', + }, + defaultOpen: { + _type: Type.Boolean, + _description: 'Set default behavior of filter accordion', + }, + }, + _default: [ + { + conceptUuid: '5035a431-51de-40f0-8f25-4a98762eb796', + defaultOpen: false, + }, + { + conceptUuid: '5566957d-9144-4fc5-8700-1882280002c1', + defaultOpen: true, + }, + ], + }, +}; + +export interface ObsTreeEntry { + conceptUuid: string; + defaultOpen: boolean; +} +export interface ConfigObject { + concepts: Array; +} diff --git a/packages/esm-patient-test-results-app/src/filter/filter-context.tsx b/packages/esm-patient-test-results-app/src/filter/filter-context.tsx index c6be79eb9a..248c8b90bf 100644 --- a/packages/esm-patient-test-results-app/src/filter/filter-context.tsx +++ b/packages/esm-patient-test-results-app/src/filter/filter-context.tsx @@ -6,7 +6,7 @@ import { TreeNode } from './filter-set'; const initialState = { checkboxes: {}, parents: {}, - root: { display: '', flatName: '' }, + roots: [{ display: '', flatName: '' }], tests: {}, lowestParents: [], }; @@ -25,13 +25,13 @@ const initialContext = { interface StateProps { checkboxes: { [key: string]: boolean }; parents: { [key: string]: string[] }; - root: { [key: string]: any }; + roots: { [key: string]: any }[]; } interface FilterContextProps { state: StateProps; checkboxes: { [key: string]: boolean }; parents: { [key: string]: string[] }; - root: TreeNode; + roots: TreeNode[]; tests: { [key: string]: any }; lowestParents: { display: string; flatName: string }[]; timelineData: { [key: string]: any }; @@ -43,7 +43,7 @@ interface FilterContextProps { } interface FilterProviderProps { - root: any; + roots: any[]; children: React.ReactNode; } @@ -53,12 +53,12 @@ interface obsShape { const FilterContext = createContext(initialContext); -const FilterProvider = ({ root, children }: FilterProviderProps) => { +const FilterProvider = ({ roots, children }: FilterProviderProps) => { const [state, dispatch] = useReducer(reducer, initialState); const actions = useMemo( () => ({ - initialize: (tree) => dispatch({ type: 'initialize', tree: tree }), + initialize: (trees) => dispatch({ type: 'initialize', trees: trees }), toggleVal: (name) => { dispatch({ type: 'toggleVal', name: name }); }, @@ -107,10 +107,10 @@ const FilterProvider = ({ root, children }: FilterProviderProps) => { }, [activeTests, state.tests]); useEffect(() => { - if (root?.display && !Object.keys(state?.checkboxes).length) { - actions.initialize(root); + if (roots?.length && !Object.keys(state?.checkboxes).length) { + actions.initialize(roots); } - }, [actions, state, root]); + }, [actions, state, roots]); return ( { state, checkboxes: state.checkboxes, parents: state.parents, - root: state.root, + roots: state.roots, tests: state.tests, lowestParents: state.lowestParents, timelineData, diff --git a/packages/esm-patient-test-results-app/src/filter/filter-reducer.ts b/packages/esm-patient-test-results-app/src/filter/filter-reducer.ts index 50d06630eb..7124558e55 100644 --- a/packages/esm-patient-test-results-app/src/filter/filter-reducer.ts +++ b/packages/esm-patient-test-results-app/src/filter/filter-reducer.ts @@ -45,12 +45,28 @@ const computeParents = (prefix, node) => { const reducer = (state, action) => { switch (action.type) { case 'initialize': - const { parents, leaves, tests, lowestParents } = computeParents('', action.tree); + let parents = {}, + leaves = [], + tests = [], + lowestParents = []; + action.trees?.forEach((tree) => { + // if anyone knows a shorthand for this i'm stoked to learn it :) + const { + parents: newParents, + leaves: newLeaves, + tests: newTests, + lowestParents: newLP, + } = computeParents('', tree); + parents = { ...parents, ...newParents }; + leaves = [...leaves, ...newLeaves]; + tests = [...tests, ...newTests]; + lowestParents = [...lowestParents, ...newLP]; + }); const flatTests = Object.fromEntries(tests); return { checkboxes: Object.fromEntries(leaves?.map((leaf) => [leaf, false])) || {}, parents: parents, - root: action.tree, + roots: action.trees, tests: flatTests, lowestParents: lowestParents, }; diff --git a/packages/esm-patient-test-results-app/src/filter/filter-set.scss b/packages/esm-patient-test-results-app/src/filter/filter-set.scss index 3e2b970c31..c84dbf8752 100644 --- a/packages/esm-patient-test-results-app/src/filter/filter-set.scss +++ b/packages/esm-patient-test-results-app/src/filter/filter-set.scss @@ -23,10 +23,6 @@ margin: $spacing-02 0; } -.filterContainerExpanded { - border-left: 0.3rem solid var(--brand-01); -} - .filterNode { padding: 1rem; } @@ -37,6 +33,7 @@ .nestedAccordion > :global(.bx--accordion--start) > :global(.bx--accordion__item--active) { border-left: 0.3rem solid var(--brand-01); + margin: 1.5rem 0; } .nestedAccordion :global { diff --git a/packages/esm-patient-test-results-app/src/filter/filter-set.tsx b/packages/esm-patient-test-results-app/src/filter/filter-set.tsx index 282a53d3ba..a52446cd57 100644 --- a/packages/esm-patient-test-results-app/src/filter/filter-set.tsx +++ b/packages/esm-patient-test-results-app/src/filter/filter-set.tsx @@ -1,7 +1,8 @@ -import React, { useContext, useState } from 'react'; +import React, { useContext } from 'react'; import styles from './filter-set.scss'; import { Accordion, AccordionItem, Checkbox } from 'carbon-components-react'; import FilterContext from './filter-context'; +import { useConfig } from '@openmrs/esm-framework'; interface Observation { display: string; @@ -31,12 +32,17 @@ const isIndeterminate = (kids, checkboxes) => { }; const FilterSet = () => { - const { root } = useContext(FilterContext); + const { roots } = useContext(FilterContext); + const config = useConfig(); return ( -
- -
+ <> + {roots?.map((root, index) => ( +
+ +
+ ))} + ); }; @@ -49,7 +55,7 @@ const FilterNode = ({ root, level, open }: FilterNodeProps) => { ; + table: { + pageSize: number; + }; +} interface obsShape { [key: string]: any; } const HIVCareAndTreatment = () => { - const concept = '5035a431-51de-40f0-8f25-4a98762eb796'; // bloodwork - const { data: root, error, loading }: obsShape = useGetObstreeData(concept); + const config = useConfig(); + const conceptUuids = config.concepts.map((c) => c.conceptUuid); + // const { data: root, error, loading }: obsShape = useGetObstreeData(conceptUuids[0]); + const { roots, loading, errors } = useGetManyObstreeData(conceptUuids); const [view, setView] = useState('split'); @@ -22,12 +38,12 @@ const HIVCareAndTreatment = () => { if (loading) { return ; } - if (error) { - return ; + if (errors.length) { + return ; } - if (!loading && !error && root?.display && root?.subSets?.length) { + if (!loading && !errors.length && roots?.length) { return ( - +
@@ -62,9 +78,6 @@ const HIVCareAndTreatment = () => { ); } - if (!loading && !error && root?.display && root?.subSets?.length === 0) { - return ; - } return null; }; diff --git a/packages/esm-patient-test-results-app/src/new-timeline/useObstreeData.ts b/packages/esm-patient-test-results-app/src/new-timeline/useObstreeData.ts index 56281a8738..8f36b1ddbe 100644 --- a/packages/esm-patient-test-results-app/src/new-timeline/useObstreeData.ts +++ b/packages/esm-patient-test-results-app/src/new-timeline/useObstreeData.ts @@ -1,6 +1,7 @@ import { usePatient, openmrsFetch } from '@openmrs/esm-framework'; import { useMemo } from 'react'; import useSWR from 'swr'; +import useSWRInfinite from 'swr/infinite'; import { assessValue, exist } from '../loadPatientTestData/helpers'; export const getName = (prefix, name) => { @@ -45,4 +46,44 @@ const useGetObstreeData = (conceptUuid) => { return result; }; +const useGetManyObstreeData = (uuidArray) => { + const { patientUuid } = usePatient(); + const getObstreeUrl = (index) => { + if (index < uuidArray.length && patientUuid) { + return `/ws/rest/v1/obstree?patient=${patientUuid}&concept=${uuidArray[index]}`; + } else return null; + }; + const { data } = useSWRInfinite(getObstreeUrl, openmrsFetch, { initialSize: uuidArray.length }); + + const result = useMemo(() => { + return ( + data?.map((resp) => { + if (resp?.data) { + const { data, ...rest } = resp; + const newData = augmentObstreeData(data, ''); + return { ...rest, loading: false, data: newData }; + } else { + return { + data: {}, + error: false, + loading: true, + }; + } + }) || [ + { + data: {}, + error: false, + loading: true, + }, + ] + ); + }, [data]); + const roots = result.map((item) => item.data); + const loading = result.some((item) => item.loading); + const errors = result.filter((item) => item.error)?.map((item) => item.error) || []; + + return { roots, loading, errors }; +}; + export default useGetObstreeData; +export { useGetManyObstreeData }; From 47be92af712d481c12fda4f2f4cc5e1e0a50f955 Mon Sep 17 00:00:00 2001 From: Zac Butko Date: Thu, 10 Mar 2022 01:57:53 -0800 Subject: [PATCH 14/14] small tweaks --- .../src/config-schema.ts | 12 ++++++++++-- .../src/hiv/hiv-care-and-treatment.tsx | 11 +++++------ 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/packages/esm-patient-test-results-app/src/config-schema.ts b/packages/esm-patient-test-results-app/src/config-schema.ts index 6c57f004b5..573851a213 100644 --- a/packages/esm-patient-test-results-app/src/config-schema.ts +++ b/packages/esm-patient-test-results-app/src/config-schema.ts @@ -16,11 +16,19 @@ export const configSchema = { _default: [ { conceptUuid: '5035a431-51de-40f0-8f25-4a98762eb796', - defaultOpen: false, + defaultOpen: true, }, { conceptUuid: '5566957d-9144-4fc5-8700-1882280002c1', - defaultOpen: true, + defaultOpen: false, + }, + { + conceptUuid: '36d88354-1081-40af-b70a-2c4981b31367', + defaultOpen: false, + }, + { + conceptUuid: 'acb5bab3-af2a-47c4-a985-934fd0113589', + defaultOpen: false, }, ], }, diff --git a/packages/esm-patient-test-results-app/src/hiv/hiv-care-and-treatment.tsx b/packages/esm-patient-test-results-app/src/hiv/hiv-care-and-treatment.tsx index 410dc87e71..5d29ae1c17 100644 --- a/packages/esm-patient-test-results-app/src/hiv/hiv-care-and-treatment.tsx +++ b/packages/esm-patient-test-results-app/src/hiv/hiv-care-and-treatment.tsx @@ -7,6 +7,7 @@ import { ErrorState } from '@openmrs/esm-patient-common-lib'; import { useGetManyObstreeData } from '../new-timeline/useObstreeData'; import styles from '../new-timeline/new-timeline.scss'; import { useConfig } from '@openmrs/esm-framework'; +import DesktopView from '../desktop-view/desktop-view.component'; export interface ConfigObject { title: string; @@ -21,10 +22,6 @@ export interface ConfigObject { }; } -interface obsShape { - [key: string]: any; -} - const HIVCareAndTreatment = () => { const config = useConfig(); const conceptUuids = config.concepts.map((c) => c.conceptUuid); @@ -32,6 +29,7 @@ const HIVCareAndTreatment = () => { const { roots, loading, errors } = useGetManyObstreeData(conceptUuids); const [view, setView] = useState('split'); + const [leftContent, setLeftContent] = useState('tree'); const expanded = view === 'full'; @@ -50,7 +48,7 @@ const HIVCareAndTreatment = () => {

Results

- + setLeftContent(`${e.name}`)}> @@ -68,7 +66,8 @@ const HIVCareAndTreatment = () => { - + {leftContent === 'tree' && } + {leftContent === 'panel' && }