@digihmis/esm-triage-app 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (148) hide show
  1. package/README.md +4 -0
  2. package/dist/117.js +1 -0
  3. package/dist/117.js.map +1 -0
  4. package/dist/132.js +1 -0
  5. package/dist/132.js.map +1 -0
  6. package/dist/152.js +1 -0
  7. package/dist/152.js.map +1 -0
  8. package/dist/183.js +66 -0
  9. package/dist/183.js.map +1 -0
  10. package/dist/209.js +1 -0
  11. package/dist/209.js.map +1 -0
  12. package/dist/22.js +1 -0
  13. package/dist/22.js.map +1 -0
  14. package/dist/222.js +1 -0
  15. package/dist/222.js.map +1 -0
  16. package/dist/317.js +1 -0
  17. package/dist/317.js.map +1 -0
  18. package/dist/349.js +1 -0
  19. package/dist/349.js.map +1 -0
  20. package/dist/371.js +1 -0
  21. package/dist/371.js.map +1 -0
  22. package/dist/378.js +1 -0
  23. package/dist/378.js.map +1 -0
  24. package/dist/442.js +1 -0
  25. package/dist/442.js.map +1 -0
  26. package/dist/45.js +1 -0
  27. package/dist/45.js.map +1 -0
  28. package/dist/466.js +1 -0
  29. package/dist/466.js.map +1 -0
  30. package/dist/508.js +1 -0
  31. package/dist/508.js.map +1 -0
  32. package/dist/61.js +1 -0
  33. package/dist/61.js.map +1 -0
  34. package/dist/640.js +1 -0
  35. package/dist/640.js.map +1 -0
  36. package/dist/689.js +1 -0
  37. package/dist/689.js.map +1 -0
  38. package/dist/709.js +1 -0
  39. package/dist/709.js.map +1 -0
  40. package/dist/712.js +1 -0
  41. package/dist/712.js.map +1 -0
  42. package/dist/720.js +1 -0
  43. package/dist/720.js.map +1 -0
  44. package/dist/737.js +12 -0
  45. package/dist/737.js.map +1 -0
  46. package/dist/742.js +1 -0
  47. package/dist/742.js.map +1 -0
  48. package/dist/771.js +1 -0
  49. package/dist/771.js.map +1 -0
  50. package/dist/806.js +1 -0
  51. package/dist/806.js.map +1 -0
  52. package/dist/809.js +1 -0
  53. package/dist/809.js.map +1 -0
  54. package/dist/9.js +15 -0
  55. package/dist/9.js.map +1 -0
  56. package/dist/912.js +1 -0
  57. package/dist/912.js.map +1 -0
  58. package/dist/913.js +1 -0
  59. package/dist/913.js.map +1 -0
  60. package/dist/940.js +1 -0
  61. package/dist/940.js.map +1 -0
  62. package/dist/974.js +1 -0
  63. package/dist/974.js.map +1 -0
  64. package/dist/976.js +1 -0
  65. package/dist/976.js.map +1 -0
  66. package/dist/digihmis-esm-triage-app.js +6 -0
  67. package/dist/digihmis-esm-triage-app.js.buildmanifest.json +905 -0
  68. package/dist/digihmis-esm-triage-app.js.map +1 -0
  69. package/dist/main.js +90 -0
  70. package/dist/main.js.map +1 -0
  71. package/dist/routes.json +1 -0
  72. package/jest.config.js +26 -0
  73. package/package.json +51 -0
  74. package/rspack.config.js +1 -0
  75. package/src/config-schema.ts +142 -0
  76. package/src/dashboard.meta.ts +16 -0
  77. package/src/declarations.d.ts +6 -0
  78. package/src/icon-wrapper/icon-wrapper.extension.tsx +17 -0
  79. package/src/index.ts +41 -0
  80. package/src/left-panel/left-panel.component.tsx +100 -0
  81. package/src/left-panel/left-panel.scss +13 -0
  82. package/src/page-header/page-header.component.tsx +35 -0
  83. package/src/page-header/page-header.scss +76 -0
  84. package/src/past-vitals/action-menu/vitals-anthropometrics.component.tsx +44 -0
  85. package/src/past-vitals/action-menu/vitals-anthropometrics.scss +0 -0
  86. package/src/past-vitals/paginated-antropometrics/paginated-antropometrics.component.tsx +132 -0
  87. package/src/past-vitals/paginated-antropometrics/paginated-antropometrics.scss +9 -0
  88. package/src/past-vitals/paginated-vitals/paginated-vitals.component.tsx +157 -0
  89. package/src/past-vitals/paginated-vitals/paginated-vitals.scss +94 -0
  90. package/src/past-vitals/paginated-vitals/type/index.ts +1 -0
  91. package/src/past-vitals/patient-banner/patient-banner.component.tsx +60 -0
  92. package/src/past-vitals/patient-banner/patient-banner.scss +72 -0
  93. package/src/past-vitals/patient-vitals-history.component.tsx +85 -0
  94. package/src/past-vitals/patient-vitals-history.resource.ts +30 -0
  95. package/src/past-vitals/patient-vitals-history.scss +75 -0
  96. package/src/past-vitals/triage-anthropometrics/antropometrics-chart.component.tsx +170 -0
  97. package/src/past-vitals/triage-anthropometrics/antropometrics-chart.scss +70 -0
  98. package/src/past-vitals/triage-anthropometrics/triage-antropometrics.component.tsx +124 -0
  99. package/src/past-vitals/triage-anthropometrics/triage-antropometrics.scss +37 -0
  100. package/src/past-vitals/triage-anthropometrics/type/index.ts +17 -0
  101. package/src/past-vitals/triage-notes/triage-note.scss +94 -0
  102. package/src/past-vitals/triage-notes/triage-notes.component.tsx +75 -0
  103. package/src/past-vitals/triage-notes/triage-notes.resource.ts +72 -0
  104. package/src/past-vitals/triage-notes/type/index.ts +50 -0
  105. package/src/past-vitals/triage-vitals/triage-vitals-history.component.tsx +229 -0
  106. package/src/past-vitals/triage-vitals/triage-vitals-history.scss +48 -0
  107. package/src/past-vitals/triage-vitals/type/index.ts +32 -0
  108. package/src/past-vitals/triage-vitals/vital-chart.component.tsx +198 -0
  109. package/src/past-vitals/triage-vitals/vitals-chart.scss +70 -0
  110. package/src/past-vitals/utils/index.ts +5 -0
  111. package/src/past-vitals/vitals-and-biometrics-header/vitals-header-item.component.tsx +61 -0
  112. package/src/past-vitals/vitals-and-biometrics-header/vitals-header-item.scss +171 -0
  113. package/src/past-vitals/vitals-and-biometrics-header/vitals-header-item.test.tsx +31 -0
  114. package/src/past-vitals/vitals-and-biometrics-header/vitals-header.extension.tsx +225 -0
  115. package/src/past-vitals/vitals-and-biometrics-header/vitals-header.scss +233 -0
  116. package/src/queue-room-table/queue-room-table.component.tsx +380 -0
  117. package/src/queue-room-table/queue-room-table.resource.ts +73 -0
  118. package/src/queue-room-table/queue-room-table.scss +193 -0
  119. package/src/queue-room-table/type/index.ts +30 -0
  120. package/src/queue-room-table/useQueueActions.ts +24 -0
  121. package/src/record-vitals-workspace/common/index.ts +19 -0
  122. package/src/record-vitals-workspace/component/vitals-biometrics-input.component.tsx +433 -0
  123. package/src/record-vitals-workspace/component/vitals-biometrics-input.scss +358 -0
  124. package/src/record-vitals-workspace/helper/index.ts +200 -0
  125. package/src/record-vitals-workspace/record-vitals.resource.ts +570 -0
  126. package/src/record-vitals-workspace/record-vitals.scss +140 -0
  127. package/src/record-vitals-workspace/record-vitals.workspace.tsx +612 -0
  128. package/src/record-vitals-workspace/schema/index.ts +28 -0
  129. package/src/record-vitals-workspace/type/index.ts +170 -0
  130. package/src/record-vitals-workspace/utils/index.ts +100 -0
  131. package/src/routes.json +70 -0
  132. package/src/setup-tests.ts +1 -0
  133. package/src/triage-queue-dashboard/triage-dashboard.resource.ts +75 -0
  134. package/src/triage-queue-dashboard/triage-queue-dashboard.component.tsx +261 -0
  135. package/src/triage-queue-dashboard/triage-queue-dashboard.scss +166 -0
  136. package/src/triage-queue-dashboard/type/index.ts +31 -0
  137. package/translations/am.json +92 -0
  138. package/translations/ar.json +92 -0
  139. package/translations/en.json +92 -0
  140. package/translations/es.json +92 -0
  141. package/translations/fr.json +92 -0
  142. package/translations/he.json +92 -0
  143. package/translations/km.json +92 -0
  144. package/translations/pt.json +92 -0
  145. package/translations/sw.json +92 -0
  146. package/translations/vi.json +92 -0
  147. package/translations/zh.json +92 -0
  148. package/tsconfig.json +5 -0
@@ -0,0 +1,75 @@
1
+ @use '@carbon/layout';
2
+ @use '@carbon/type';
3
+ @use '@carbon/colors';
4
+ @use '@openmrs/esm-styleguide/src/vars' as *;
5
+
6
+ :global(.cds--tabs .cds--tabs__nav-item--selected) {
7
+ border-bottom: 2px solid var(--cds-border-interactive, #4a90b8);
8
+ transition: color 70ms cubic-bezier(0.2, 0, 0.38, 0.9);
9
+ }
10
+
11
+ :global(.cds--tabs .cds--tabs__nav-item:focus) {
12
+ outline: 0;
13
+ outline-offset: -2px;
14
+ }
15
+
16
+ .tabsContainer {
17
+ padding-left: layout.$spacing-06;
18
+ padding-right: layout.$spacing-06;
19
+ background: var(--cds-field);
20
+
21
+ :global(.omrs-breakpoint-lt-tablet) & {
22
+ padding-left: layout.$spacing-03;
23
+ padding-right: layout.$spacing-03;
24
+ }
25
+ }
26
+
27
+ .tabsHeaderRow {
28
+ display: flex;
29
+ align-items: center;
30
+ justify-content: space-between;
31
+ gap: layout.$spacing-05;
32
+
33
+ :global(.omrs-breakpoint-lt-desktop) & {
34
+ gap: layout.$spacing-03;
35
+ }
36
+
37
+ :global(.omrs-breakpoint-lt-tablet) & {
38
+ gap: layout.$spacing-03;
39
+ }
40
+ }
41
+
42
+ .buttonNotesTab {
43
+ margin-right: layout.$spacing-03;
44
+
45
+ :global(.omrs-breakpoint-lt-tablet) & {
46
+ padding: layout.$spacing-02 layout.$spacing-03;
47
+ white-space: nowrap;
48
+ font-size: 0.875rem;
49
+ margin-right: layout.$spacing-02;
50
+ }
51
+ }
52
+
53
+ .buttonOtherTabs {
54
+ margin-right: layout.$spacing-06;
55
+
56
+ :global(.omrs-breakpoint-lt-tablet) & {
57
+ padding: layout.$spacing-02 layout.$spacing-03;
58
+ white-space: nowrap;
59
+ font-size: 0.875rem;
60
+ margin-right: layout.$spacing-03;
61
+ }
62
+ }
63
+
64
+ .notesContainer {
65
+ padding-right: layout.$spacing-03;
66
+ }
67
+
68
+ .vitalsContainer {
69
+ padding-top: layout.$spacing-03;
70
+ padding-right: layout.$spacing-06;
71
+
72
+ :global(.omrs-breakpoint-lt-tablet) & {
73
+ padding-right: layout.$spacing-03;
74
+ }
75
+ }
@@ -0,0 +1,170 @@
1
+ import React, { useMemo, useState } from 'react';
2
+ import classNames from 'classnames';
3
+ import { useTranslation } from 'react-i18next';
4
+ import { Tab, TabListVertical, TabPanel, TabPanels, TabsVertical } from '@carbon/react';
5
+ import { LineChart, ScaleTypes } from '@carbon/charts-react';
6
+ import { formatDate, parseDate } from '@openmrs/esm-framework';
7
+ import { type ConfigObject } from '../../config-schema';
8
+ import { type PatientVitalsAndBiometrics } from '../../record-vitals-workspace/common';
9
+ import styles from './antropometrics-chart.scss';
10
+
11
+ type BiometricType = 'weight' | 'height' | 'bmi';
12
+
13
+ interface BiometricsChartProps {
14
+ conceptUnits: Map<string, string>;
15
+ config: ConfigObject;
16
+ patientBiometrics: Array<PatientVitalsAndBiometrics>;
17
+ }
18
+
19
+ interface BiometricChartData {
20
+ groupName: BiometricType;
21
+ title: string;
22
+ value: number | string;
23
+ }
24
+
25
+ const BiometricsChart: React.FC<BiometricsChartProps> = ({ patientBiometrics, conceptUnits, config }) => {
26
+ const { t } = useTranslation();
27
+ const { bmiUnit } = config.biometrics;
28
+ const [selectedBiometrics, setSelectedBiometrics] = useState<BiometricChartData>({
29
+ title: `${t('weight', 'Weight')} (${conceptUnits.get(config.concepts.weightUuid) ?? ''})`,
30
+ value: 'weight',
31
+ groupName: 'weight',
32
+ });
33
+
34
+ const biometrics: { id: BiometricType; title: string; value: BiometricType }[] = [
35
+ {
36
+ id: 'weight',
37
+ title: `${t('weight', 'Weight')} (${conceptUnits.get(config.concepts.weightUuid) ?? ''})`,
38
+ value: 'weight',
39
+ },
40
+ {
41
+ id: 'height',
42
+ title: `${t('height', 'Height')} (${conceptUnits.get(config.concepts.heightUuid) ?? ''})`,
43
+ value: 'height',
44
+ },
45
+ { id: 'bmi', title: `${t('bmi', 'BMI')} (${bmiUnit})`, value: 'bmi' },
46
+ ];
47
+
48
+ const chartData = useMemo(
49
+ () =>
50
+ patientBiometrics
51
+ .filter((biometrics) => biometrics[selectedBiometrics.value])
52
+ .slice(0, 10)
53
+ .sort((biometricA, biometricB) => new Date(biometricA.date).getTime() - new Date(biometricB.date).getTime())
54
+ .map(
55
+ (biometrics) =>
56
+ biometrics[selectedBiometrics.value] && {
57
+ group: selectedBiometrics.title,
58
+ key: formatDate(parseDate(biometrics.date), { year: true }),
59
+ value: biometrics[selectedBiometrics.value],
60
+ date: biometrics.date,
61
+ },
62
+ ),
63
+ [patientBiometrics, selectedBiometrics.title, selectedBiometrics.value],
64
+ );
65
+
66
+ const chartOptions = useMemo(() => {
67
+ return {
68
+ title: selectedBiometrics.title,
69
+ axes: {
70
+ bottom: {
71
+ title: t('date', 'Date'),
72
+ mapsTo: 'date',
73
+ scaleType: ScaleTypes.TIME,
74
+ },
75
+ left: {
76
+ mapsTo: 'value',
77
+ title: selectedBiometrics.title,
78
+ scaleType: ScaleTypes.LINEAR,
79
+ includeZero: false,
80
+ },
81
+ },
82
+ legend: {
83
+ enabled: false,
84
+ },
85
+ color: {
86
+ scale: {
87
+ [selectedBiometrics.title]: '#6929c4',
88
+ },
89
+ },
90
+ tooltip: {
91
+ customHTML: ([{ value, date }]) =>
92
+ `<div class="cds--tooltip cds--tooltip--shown" style="min-width: max-content; font-weight:600">${formatDate(
93
+ parseDate(date),
94
+ { year: true },
95
+ )} -
96
+ <span style="color: #c6c6c6; font-size: 1rem; font-weight:400">${value}</span></div>`,
97
+ },
98
+ toolbar: {
99
+ enabled: true,
100
+ numberOfIcons: 4,
101
+ controls: [
102
+ {
103
+ type: 'Zoom in',
104
+ },
105
+ {
106
+ type: 'Zoom out',
107
+ },
108
+ {
109
+ type: 'Reset zoom',
110
+ },
111
+ {
112
+ type: 'Export as CSV',
113
+ },
114
+ {
115
+ type: 'Export as PNG',
116
+ },
117
+ {
118
+ type: 'Make fullscreen',
119
+ },
120
+ ],
121
+ },
122
+ zoomBar: {
123
+ top: {
124
+ enabled: true,
125
+ },
126
+ },
127
+ height: '400px',
128
+ };
129
+ }, [selectedBiometrics, t]);
130
+
131
+ return (
132
+ <div className={styles.biometricChartContainer}>
133
+ <div className={styles.biometricsArea}>
134
+ <label className={styles.biometricLabel} htmlFor="biometrics-chart-radio-group">
135
+ {t('biometricDisplayed', 'Biometric displayed')}
136
+ </label>
137
+ <TabsVertical>
138
+ <TabListVertical aria-label="Biometrics tabs">
139
+ {biometrics.map(({ id, title, value }) => (
140
+ <Tab
141
+ className={classNames(styles.tab, styles.bodyLong01, {
142
+ [styles.selectedTab]: selectedBiometrics.title === title,
143
+ })}
144
+ id={`${id}-tab`}
145
+ key={id}
146
+ onClick={() =>
147
+ setSelectedBiometrics({
148
+ title: title,
149
+ value: value,
150
+ groupName: id,
151
+ })
152
+ }>
153
+ {title}
154
+ </Tab>
155
+ ))}
156
+ </TabListVertical>
157
+ <TabPanels>
158
+ {biometrics.map(({ id }) => (
159
+ <TabPanel key={id}>
160
+ <LineChart data={chartData} options={chartOptions} key={id} />
161
+ </TabPanel>
162
+ ))}
163
+ </TabPanels>
164
+ </TabsVertical>
165
+ </div>
166
+ </div>
167
+ );
168
+ };
169
+
170
+ export default BiometricsChart;
@@ -0,0 +1,70 @@
1
+ @use '@carbon/layout';
2
+ @use '@carbon/type';
3
+ @use '@openmrs/esm-styleguide/src/vars' as *;
4
+
5
+ .label01 {
6
+ @include type.type-style('label-01');
7
+ }
8
+
9
+ .bodyShort01 {
10
+ @include type.type-style('body-compact-01');
11
+ }
12
+
13
+ .biometricChartContainer {
14
+ display: flex;
15
+ margin: 0 layout.$spacing-05;
16
+ flex-direction: row;
17
+ justify-content: space-between;
18
+ }
19
+
20
+ .biometricsArea {
21
+ flex-grow: 1;
22
+ margin: layout.$spacing-03 0;
23
+
24
+ // This is a hack to override the max-width of the overflow menu button
25
+ :global(
26
+ .cds--chart-holder
27
+ .cds--cc--toolbar
28
+ .cds--overflow-menu-options
29
+ .cds--overflow-menu-options__option
30
+ .cds--overflow-menu-options__btn
31
+ ) {
32
+ max-width: none !important;
33
+ }
34
+
35
+ // Ensures the chart toolbar tooltips are visible
36
+ :global(.cds--tabs--vertical ~ .cds--tab-content) {
37
+ overflow-y: visible;
38
+ }
39
+ }
40
+
41
+ .biometricLabel {
42
+ @extend .label01;
43
+ margin-bottom: layout.$spacing-05;
44
+ display: inline-block;
45
+ }
46
+
47
+ .tab {
48
+ outline: 0;
49
+ outline-offset: 0;
50
+ min-height: layout.$spacing-07 !important;
51
+ block-size: layout.$spacing-05 !important;
52
+
53
+ &:active,
54
+ &:focus {
55
+ outline: layout.$spacing-01 solid var(--brand-03) !important;
56
+ }
57
+
58
+ &[aria-selected='true'] {
59
+ box-shadow: inset 4px 0 0 0 var(--brand-03) !important;
60
+ border-bottom: none;
61
+ font-weight: 600;
62
+ margin-left: 0 !important;
63
+ }
64
+
65
+ &[aria-selected='false'] {
66
+ border-bottom: none;
67
+ border-left: layout.$spacing-01 solid $ui-03;
68
+ margin-left: 0 !important;
69
+ }
70
+ }
@@ -0,0 +1,124 @@
1
+ import React, { useMemo, useState } from 'react';
2
+ import { useTranslation } from 'react-i18next';
3
+ import { Button, ContentSwitcher, DataTableSkeleton, IconSwitch, InlineLoading } from '@carbon/react';
4
+ import { Add, Analytics, Table } from '@carbon/react/icons';
5
+ import { formatDatetime, parseDate, useConfig, useLayoutType } from '@openmrs/esm-framework';
6
+ import { CardHeader, EmptyState, ErrorState } from '@openmrs/esm-patient-common-lib';
7
+ import { useConceptUnits, useVitalsAndBiometrics, withUnit } from '../../record-vitals-workspace/common';
8
+ import { type ConfigObject } from '../../config-schema';
9
+ import styles from './triage-antropometrics.scss';
10
+ import { BiometricsTableHeader, BiometricsTableRow } from './type';
11
+ import PaginatedBiometrics from '../paginated-antropometrics/paginated-antropometrics.component';
12
+ import BiometricsChart from './antropometrics-chart.component';
13
+
14
+ interface BiometricsBaseProps {
15
+ pageSize: number;
16
+ patientUuid: string;
17
+ patient: fhir.Patient;
18
+ urlLabel: string;
19
+ }
20
+
21
+ const BiometricsBase: React.FC<BiometricsBaseProps> = ({ patientUuid, patient, pageSize, urlLabel }) => {
22
+ const { t } = useTranslation();
23
+ const displayText = t('anthropometrics_lower', 'anthropometrics');
24
+ const headerTitle = t('anthropometrics', 'Anthropometrics');
25
+ const [chartView, setChartView] = useState(false);
26
+ const isTablet = useLayoutType() === 'tablet';
27
+
28
+ const config = useConfig<ConfigObject>();
29
+ const { bmiUnit } = config.biometrics;
30
+ const { data: biometrics, isLoading, error, isValidating } = useVitalsAndBiometrics(patientUuid, 'biometrics');
31
+ const { conceptUnits } = useConceptUnits();
32
+
33
+ const tableHeaders: Array<BiometricsTableHeader> = [
34
+ {
35
+ key: 'dateRender',
36
+ header: t('dateAndTime', 'Date and time'),
37
+ isSortable: true,
38
+ sortFunc: (valueA, valueB) => new Date(valueA.date).getTime() - new Date(valueB.date).getTime(),
39
+ },
40
+ {
41
+ key: 'weightRender',
42
+ header: withUnit(t('weight', 'Weight'), conceptUnits.get(config.concepts.weightUuid) ?? ''),
43
+ isSortable: true,
44
+ sortFunc: (valueA, valueB) => (valueA.weight && valueB.weight ? valueA.weight - valueB.weight : 0),
45
+ },
46
+ {
47
+ key: 'heightRender',
48
+ header: withUnit(t('height', 'Height'), conceptUnits.get(config.concepts.heightUuid) ?? ''),
49
+ isSortable: true,
50
+ sortFunc: (valueA, valueB) => (valueA.height && valueB.height ? valueA.height - valueB.height : 0),
51
+ },
52
+ {
53
+ key: 'bmiRender',
54
+ header: `${t('bmi', 'BMI')} (${bmiUnit})`,
55
+ isSortable: true,
56
+ sortFunc: (valueA, valueB) => (valueA.bmi && valueB.bmi ? valueA.bmi - valueB.bmi : 0),
57
+ },
58
+ {
59
+ key: 'muacRender',
60
+ header: withUnit(t('muac', 'MUAC'), conceptUnits.get(config.concepts.midUpperArmCircumferenceUuid) ?? ''),
61
+ isSortable: true,
62
+ sortFunc: (valueA, valueB) => (valueA.muac && valueB.muac ? valueA.muac - valueB.muac : 0),
63
+ },
64
+ ];
65
+
66
+ const tableRows: Array<BiometricsTableRow> = useMemo(
67
+ () =>
68
+ biometrics?.map((biometricsData, index) => {
69
+ return {
70
+ ...biometricsData,
71
+ dateRender: formatDatetime(parseDate(biometricsData.date.toString()), { mode: 'wide' }),
72
+ weightRender: biometricsData.weight ?? '--',
73
+ heightRender: biometricsData.height ?? '--',
74
+ bmiRender: biometricsData.bmi ?? '--',
75
+ muacRender: biometricsData.muac ?? '--',
76
+ };
77
+ }),
78
+ [biometrics],
79
+ );
80
+
81
+ if (isLoading) {
82
+ return <DataTableSkeleton role="progressbar" />;
83
+ }
84
+
85
+ if (error) {
86
+ return <ErrorState error={error} headerTitle={headerTitle} />;
87
+ }
88
+
89
+ if (biometrics?.length) {
90
+ return (
91
+ <div className={styles.widgetCard}>
92
+ <CardHeader title={headerTitle}>
93
+ <div className={styles.biometricsHeaderActionItems}>
94
+ <ContentSwitcher
95
+ onChange={(evt) => setChartView(evt.name === 'chartView')}
96
+ size={isTablet ? 'md' : 'sm'}
97
+ selectedIndex={chartView ? 1 : 0}>
98
+ <IconSwitch name="tableView" text="Table view">
99
+ <Table size={16} />
100
+ </IconSwitch>
101
+ <IconSwitch name="chartView" text="Chart view">
102
+ <Analytics size={16} />
103
+ </IconSwitch>
104
+ </ContentSwitcher>
105
+ </div>
106
+ </CardHeader>
107
+ {chartView ? (
108
+ <BiometricsChart patientBiometrics={biometrics} conceptUnits={conceptUnits} config={config} />
109
+ ) : (
110
+ <PaginatedBiometrics
111
+ tableRows={tableRows}
112
+ pageSize={pageSize}
113
+ urlLabel={urlLabel}
114
+ tableHeaders={tableHeaders}
115
+ patient={patient}
116
+ />
117
+ )}
118
+ </div>
119
+ );
120
+ }
121
+ return <EmptyState displayText={displayText} headerTitle={headerTitle} />;
122
+ };
123
+
124
+ export default BiometricsBase;
@@ -0,0 +1,37 @@
1
+ @use '@carbon/layout';
2
+ @use '@openmrs/esm-styleguide/src/vars' as *;
3
+
4
+ .widgetCard {
5
+ border: 1px solid $ui-03;
6
+ background-color: $ui-02;
7
+ }
8
+
9
+ .biometricsHeaderActionItems {
10
+ align-items: center;
11
+ display: flex;
12
+ flex: 1 1 0%;
13
+ justify-content: end;
14
+ padding-right: layout.$spacing-05;
15
+
16
+ & :global(.cds--content-switcher) {
17
+ max-width: max-content;
18
+
19
+ :global(.cds--content-switcher__label) {
20
+ height: layout.$spacing-05;
21
+ }
22
+ }
23
+ }
24
+
25
+ .divider {
26
+ width: 1px;
27
+ height: layout.$spacing-05;
28
+ color: $ui-05;
29
+ margin: 0 layout.$spacing-05;
30
+ }
31
+
32
+ .backgroundDataFetchingIndicator {
33
+ align-items: center;
34
+ display: flex;
35
+ flex: 1 1 0%;
36
+ justify-content: end;
37
+ }
@@ -0,0 +1,17 @@
1
+ import { type PatientVitalsAndBiometrics } from '../../../record-vitals-workspace/common';
2
+
3
+ export interface BiometricsTableRow extends PatientVitalsAndBiometrics {
4
+ id: string;
5
+ dateRender: string;
6
+ weightRender: string | number;
7
+ heightRender: string | number;
8
+ bmiRender: string | number;
9
+ muacRender: string | number;
10
+ }
11
+
12
+ export interface BiometricsTableHeader {
13
+ key: 'dateRender' | 'weightRender' | 'heightRender' | 'bmiRender' | 'muacRender';
14
+ header: string;
15
+ isSortable?: boolean;
16
+ sortFunc: (valueA: BiometricsTableRow, valueB: BiometricsTableRow) => number;
17
+ }
@@ -0,0 +1,94 @@
1
+ @use '@carbon/layout';
2
+ @use '@carbon/type';
3
+ @use '@carbon/colors';
4
+
5
+ .medicalNotesContainer {
6
+ display: grid;
7
+ grid-template-columns: repeat(3, 1fr);
8
+ gap: layout.$spacing-05;
9
+ max-width: 100%;
10
+ width: 100%;
11
+ margin-top: layout.$spacing-05;
12
+
13
+ @media (max-width: 1056px) {
14
+ grid-template-columns: repeat(2, 1fr);
15
+ }
16
+
17
+ @media (max-width: 672px) {
18
+ grid-template-columns: 1fr;
19
+ }
20
+ }
21
+ .emptyMedicalNotesContainer {
22
+ margin-top: layout.$spacing-03;
23
+ }
24
+
25
+ .noteCard {
26
+ background-color: colors.$white;
27
+ border: 1px solid colors.$gray-20;
28
+ border-radius: 4px;
29
+ padding: layout.$spacing-05;
30
+ display: flex;
31
+ flex-direction: column;
32
+ gap: layout.$spacing-04;
33
+ height: 100%;
34
+ }
35
+
36
+ .noteHeader {
37
+ display: flex;
38
+ justify-content: space-between;
39
+ align-items: center;
40
+ }
41
+
42
+ .noteTitle {
43
+ @include type.type-style('heading-compact-01');
44
+ margin: 0;
45
+ font-weight: 600;
46
+ color: colors.$gray-100;
47
+ }
48
+
49
+ .noteDate {
50
+ @include type.type-style('body-compact-01');
51
+ color: colors.$gray-70;
52
+ font-size: 0.875rem;
53
+ }
54
+
55
+ .tagsContainer {
56
+ display: flex;
57
+ gap: layout.$spacing-03;
58
+ flex-wrap: wrap;
59
+ }
60
+
61
+ .noteContent {
62
+ @include type.type-style('body-compact-01');
63
+ margin: 0;
64
+ color: colors.$gray-100;
65
+ line-height: 1.6;
66
+ font-size: 0.875rem;
67
+ flex: 1;
68
+ text-align: justify;
69
+ }
70
+
71
+ .noteFooter {
72
+ display: flex;
73
+ justify-content: flex-start;
74
+ padding-top: layout.$spacing-03;
75
+ border-top: 1px solid colors.$gray-20;
76
+ margin-top: auto;
77
+ }
78
+
79
+ .authorName {
80
+ @include type.type-style('body-compact-01');
81
+ color: colors.$gray-70;
82
+ font-size: 0.875rem;
83
+ }
84
+
85
+ .loadMoreContainer {
86
+ display: flex;
87
+ justify-content: center;
88
+ padding: layout.$spacing-05 0;
89
+ grid-column: 1 / -1;
90
+ }
91
+
92
+ .loadMoreButton {
93
+ min-width: 200px;
94
+ }
@@ -0,0 +1,75 @@
1
+ import React, { useState } from 'react';
2
+ import classNames from 'classnames';
3
+ import { useTranslation } from 'react-i18next';
4
+ import { Button } from '@carbon/react';
5
+ import type { Note } from './type';
6
+ import { EmptyState } from '@openmrs/esm-patient-common-lib';
7
+ import styles from './triage-note.scss';
8
+ import { ConfigObject } from '../../config-schema';
9
+ import { useConfig } from '@openmrs/esm-framework';
10
+ import { useTriageNotes } from './triage-notes.resource';
11
+
12
+ interface NotesSummaryProps {
13
+ patientUuid: string;
14
+ initialDisplayCount?: number;
15
+ loadMoreCount?: number;
16
+ }
17
+
18
+ const NotesSummary: React.FC<NotesSummaryProps> = ({ patientUuid, initialDisplayCount = 9, loadMoreCount = 9 }) => {
19
+ const { t } = useTranslation();
20
+ const [displayCount, setDisplayCount] = useState(initialDisplayCount);
21
+
22
+ const config = useConfig<ConfigObject>();
23
+ const { generalPatientNoteUuid } = config.concepts;
24
+ const { encounterTypeUuid } = config.vitals;
25
+
26
+ const { notes, error, isLoading } = useTriageNotes(patientUuid, generalPatientNoteUuid, encounterTypeUuid);
27
+
28
+ const handleLoadMore = () => {
29
+ setDisplayCount((prev) => prev + loadMoreCount);
30
+ };
31
+
32
+ if (notes.length === 0) {
33
+ return (
34
+ <div className={styles.emptyMedicalNotesContainer}>
35
+ <EmptyState displayText={t('notes__lower', 'notes')} headerTitle={t('nursesNote', "Nurse's note")} />
36
+ </div>
37
+ );
38
+ }
39
+
40
+ const displayedNotes = notes.slice(0, displayCount);
41
+ const hasMore = displayCount < notes.length;
42
+ const remainingCount = notes.length - displayCount;
43
+
44
+ return (
45
+ <>
46
+ <div className={styles.medicalNotesContainer}>
47
+ {displayedNotes.map((note, index) => (
48
+ <div key={index} className={styles.noteCard}>
49
+ <div className={styles.noteHeader}>
50
+ <h4 className={styles.noteTitle}>{t('nursesNote', "Nurse's note")}</h4>
51
+ <span className={styles.noteDate}>{note.time}</span>
52
+ </div>
53
+
54
+ <p className={styles.noteContent}>{note.note}</p>
55
+
56
+ <div className={styles.noteFooter}>
57
+ <span className={styles.authorName}>{note.provider.name}</span>
58
+ </div>
59
+ </div>
60
+ ))}
61
+
62
+ {hasMore && (
63
+ <div className={styles.loadMoreContainer}>
64
+ <Button kind="ghost" onClick={handleLoadMore} className={styles.loadMoreButton}>
65
+ {t('loadMore', 'Load more')} ({remainingCount}{' '}
66
+ {remainingCount === 1 ? t('note', 'note') : t('notes', 'notes')} {t('remaining', 'remaining')})
67
+ </Button>
68
+ </div>
69
+ )}
70
+ </div>
71
+ </>
72
+ );
73
+ };
74
+
75
+ export default NotesSummary;