@palladium-ethiopia/esm-clinical-workflow-app 5.4.2-pre.20 → 5.4.2-pre.26

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 (84) hide show
  1. package/.turbo/turbo-build.log +5 -5
  2. package/dist/152.js +1 -1
  3. package/dist/152.js.map +1 -1
  4. package/dist/164.js +1 -0
  5. package/dist/164.js.map +1 -0
  6. package/dist/208.js +1 -1
  7. package/dist/208.js.map +1 -1
  8. package/dist/209.js +1 -1
  9. package/dist/209.js.map +1 -1
  10. package/dist/363.js +1 -1
  11. package/dist/363.js.map +1 -1
  12. package/dist/534.js +1 -0
  13. package/dist/534.js.map +1 -0
  14. package/dist/677.js +1 -1
  15. package/dist/677.js.map +1 -1
  16. package/dist/689.js +1 -1
  17. package/dist/689.js.map +1 -1
  18. package/dist/712.js +1 -1
  19. package/dist/712.js.map +1 -1
  20. package/dist/771.js +1 -1
  21. package/dist/771.js.map +1 -1
  22. package/dist/825.js +1 -0
  23. package/dist/825.js.map +1 -0
  24. package/dist/914.js +37 -0
  25. package/dist/914.js.map +1 -0
  26. package/dist/926.js +17 -0
  27. package/dist/926.js.map +1 -0
  28. package/dist/ethiopia-esm-clinical-workflow-app.js +5 -5
  29. package/dist/ethiopia-esm-clinical-workflow-app.js.buildmanifest.json +144 -144
  30. package/dist/ethiopia-esm-clinical-workflow-app.js.map +1 -1
  31. package/dist/main.js +34 -8
  32. package/dist/main.js.map +1 -1
  33. package/dist/routes.json +1 -1
  34. package/package.json +1 -1
  35. package/src/config-schema.ts +98 -0
  36. package/src/index.ts +32 -1
  37. package/src/patient-chart/clinical-views/hooks/useEncountersByVisit.ts +13 -0
  38. package/src/patient-chart/constants.ts +11 -0
  39. package/src/patient-chart/visit/visit-history-table/diagnosis-tags.component.tsx +43 -0
  40. package/src/patient-chart/visit/visit-history-table/diagnosis-tags.module.scss +57 -0
  41. package/src/patient-chart/visit/visit-history-table/visit-actions-cell.component.tsx +20 -0
  42. package/src/patient-chart/visit/visit-history-table/visit-actions-cell.scss +4 -0
  43. package/src/patient-chart/visit/visit-history-table/visit-date-cell.component.tsx +19 -0
  44. package/src/patient-chart/visit/visit-history-table/visit-diagnoses-cell-with-certainty.component.tsx +31 -0
  45. package/src/patient-chart/visit/visit-history-table/visit-diagnoses-cell-with-certainty.module.scss +16 -0
  46. package/src/patient-chart/visit/visit-history-table/visit-history-table.component.tsx +144 -0
  47. package/src/patient-chart/visit/visit-history-table/visit-history-table.scss +25 -0
  48. package/src/patient-chart/visit/visit-history-table/visit-type-cell.component.tsx +15 -0
  49. package/src/patient-chart/visit/visits-widget/encounter-observations/encounter-observations.component.tsx +67 -0
  50. package/src/patient-chart/visit/visits-widget/encounter-observations/index.ts +3 -0
  51. package/src/patient-chart/visit/visits-widget/encounter-observations/styles.scss +22 -0
  52. package/src/patient-chart/visit/visits-widget/past-visits-components/encounters-table/all-encounters-table.component.tsx +44 -0
  53. package/src/patient-chart/visit/visits-widget/past-visits-components/encounters-table/encounters-table.component.tsx +388 -0
  54. package/src/patient-chart/visit/visits-widget/past-visits-components/encounters-table/encounters-table.resource.ts +97 -0
  55. package/src/patient-chart/visit/visits-widget/past-visits-components/encounters-table/encounters-table.scss +113 -0
  56. package/src/patient-chart/visit/visits-widget/past-visits-components/encounters-table/visit-encounters-table.component.tsx +42 -0
  57. package/src/patient-chart/visit/visits-widget/past-visits-components/medications-summary.component.tsx +157 -0
  58. package/src/patient-chart/visit/visits-widget/past-visits-components/notes-summary.component.tsx +34 -0
  59. package/src/patient-chart/visit/visits-widget/past-visits-components/tests-summary.component.tsx +16 -0
  60. package/src/patient-chart/visit/visits-widget/past-visits-components/visit-actions-cell.scss +4 -0
  61. package/src/patient-chart/visit/visits-widget/past-visits-components/visit-summary.component.tsx +176 -0
  62. package/src/patient-chart/visit/visits-widget/past-visits-components/visit-summary.scss +72 -0
  63. package/src/patient-chart/visit/visits-widget/single-visit-details/visit-timeline/visit-timeline.component.tsx +94 -0
  64. package/src/patient-chart/visit/visits-widget/single-visit-details/visit-timeline/visit-timeline.scss +60 -0
  65. package/src/patient-chart/visit/visits-widget/visit-detail-overview.component.tsx +50 -0
  66. package/src/patient-chart/visit/visits-widget/visit-detail-overview.scss +262 -0
  67. package/src/patient-chart/visit/visits-widget/visit.resource.tsx +144 -0
  68. package/src/patient-notes/types/index.ts +194 -0
  69. package/src/patient-notes/visit-note-action-button.extension.tsx +28 -0
  70. package/src/patient-notes/visit-note-config-schema.ts +38 -0
  71. package/src/patient-notes/visit-notes-form-shadow.workspace.tsx +963 -0
  72. package/src/patient-notes/visit-notes-form.scss +453 -0
  73. package/src/patient-notes/visit-notes.resource.ts +113 -0
  74. package/src/routes.json +23 -0
  75. package/translations/am.json +168 -0
  76. package/translations/en.json +168 -0
  77. package/dist/410.js +0 -1
  78. package/dist/410.js.map +0 -1
  79. package/dist/484.js +0 -11
  80. package/dist/484.js.map +0 -1
  81. package/dist/540.js +0 -1
  82. package/dist/540.js.map +0 -1
  83. package/dist/545.js +0 -43
  84. package/dist/545.js.map +0 -1
@@ -0,0 +1,42 @@
1
+ import React, { useMemo, useState } from 'react';
2
+ import { usePagination, type Visit } from '@openmrs/esm-framework';
3
+ import EncountersTable from './encounters-table.component';
4
+ import { type EncountersTableProps } from './encounters-table.resource';
5
+
6
+ interface VisitEncountersTableProps {
7
+ patientUuid: string;
8
+ visit: Visit;
9
+ }
10
+
11
+ /**
12
+ * This component shows a table of encounters from a single visit of a patient
13
+ */
14
+ const VisitEncountersTable: React.FC<VisitEncountersTableProps> = ({ patientUuid, visit }) => {
15
+ const [pageSize, setPageSize] = useState(10);
16
+ const mappedEncounters = useMemo(
17
+ () =>
18
+ visit.encounters.map((encounter) => {
19
+ encounter.visit = visit;
20
+ return encounter;
21
+ }),
22
+ [visit],
23
+ );
24
+ const { results: paginatedEncounters, currentPage, goTo } = usePagination(mappedEncounters, pageSize);
25
+
26
+ const encountersTableProps: EncountersTableProps = {
27
+ patientUuid,
28
+ totalCount: visit.encounters.length,
29
+ currentPage,
30
+ goTo,
31
+ isLoading: false,
32
+ showVisitType: false,
33
+ paginatedEncounters: paginatedEncounters,
34
+ showEncounterTypeFilter: false,
35
+ pageSize,
36
+ setPageSize,
37
+ };
38
+
39
+ return <EncountersTable {...encountersTableProps} />;
40
+ };
41
+
42
+ export default VisitEncountersTable;
@@ -0,0 +1,157 @@
1
+ import React from 'react';
2
+ import capitalize from 'lodash-es/capitalize';
3
+ import { useTranslation } from 'react-i18next';
4
+ import { EmptyState } from '@openmrs/esm-patient-common-lib';
5
+ import { Tag, Tooltip } from '@carbon/react';
6
+ import { formatDate, useConfig } from '@openmrs/esm-framework';
7
+ import type { OrderItem } from '../visit.resource';
8
+ import styles from '../visit-detail-overview.scss';
9
+ import { type ChartConfig } from '../../../../config-schema';
10
+
11
+ interface MedicationSummaryProps {
12
+ medications: Array<OrderItem>;
13
+ }
14
+
15
+ const MedicationSummary: React.FC<MedicationSummaryProps> = ({ medications }) => {
16
+ const { t } = useTranslation();
17
+ const { drugOrderTypeUUID } = useConfig<ChartConfig>();
18
+
19
+ const isPastMedication = (order: OrderItem['order']) => {
20
+ if (!order) {
21
+ return false;
22
+ }
23
+
24
+ return (
25
+ order.action === 'DISCONTINUE' ||
26
+ (order.dateStopped && new Date(order.dateStopped) <= new Date()) ||
27
+ (order.autoExpireDate && new Date(order.autoExpireDate) <= new Date())
28
+ );
29
+ };
30
+
31
+ const drugOrders = medications?.filter((medication) => {
32
+ return medication?.order?.orderType?.uuid === drugOrderTypeUUID;
33
+ });
34
+
35
+ if (drugOrders.length === 0) {
36
+ return (
37
+ <EmptyState displayText={t('medications__lower', 'medications')} headerTitle={t('medications', 'Medications')} />
38
+ );
39
+ }
40
+
41
+ return (
42
+ <div className={styles.medicationRecord}>
43
+ {drugOrders.map(
44
+ (medication, index) =>
45
+ (medication?.order?.dose || medication?.order?.dosingInstructions) && (
46
+ <React.Fragment key={index}>
47
+ <div className={styles.medicationContainer}>
48
+ <div>
49
+ <p className={styles.bodyLong01}>
50
+ <strong>{capitalize(medication?.order?.drug?.display)}</strong>{' '}
51
+ {medication?.order?.drug?.strength && (
52
+ <>&mdash; {medication?.order?.drug?.strength?.toLowerCase()}</>
53
+ )}{' '}
54
+ {medication?.order?.doseUnits?.display && (
55
+ <>&mdash; {medication?.order?.doseUnits?.display?.toLowerCase()}</>
56
+ )}{' '}
57
+ {isPastMedication(medication.order) && (
58
+ <Tooltip align="right" label={<>{formatDate(new Date(medication.order.dateStopped))}</>}>
59
+ <Tag type="gray" className={styles.tag}>
60
+ {t('discontinued', 'Discontinued')}
61
+ </Tag>
62
+ </Tooltip>
63
+ )}
64
+ </p>
65
+ <p className={styles.bodyLong01}>
66
+ {medication?.order?.dose ? (
67
+ <>
68
+ <span className={styles.label01}> {t('dose', 'Dose').toUpperCase()} </span>{' '}
69
+ <span className={styles.dosage}>
70
+ {medication?.order?.dose} {medication?.order?.doseUnits?.display?.toLowerCase()}
71
+ </span>{' '}
72
+ {medication.order?.route?.display && (
73
+ <span>&mdash; {medication?.order?.route?.display?.toLowerCase()} &mdash; </span>
74
+ )}
75
+ {medication?.order?.frequency?.display?.toLowerCase()} &mdash;{' '}
76
+ {!medication?.order?.duration
77
+ ? t('orderIndefiniteDuration', 'Indefinite duration')
78
+ : t('orderDurationAndUnit', 'for {{duration}} {{durationUnit}}', {
79
+ duration: medication?.order?.duration,
80
+ durationUnit: medication?.order?.durationUnits?.display?.toLowerCase(),
81
+ })}
82
+ {medication?.order?.numRefills !== 0 && (
83
+ <span>
84
+ <span className={styles.label01}> &mdash; {t('refills', 'Refills').toUpperCase()}</span>{' '}
85
+ {medication?.order?.numRefills}
86
+ {''}
87
+ </span>
88
+ )}
89
+ {medication?.order?.dosingInstructions && (
90
+ <span> &mdash; {medication?.order?.dosingInstructions?.toLocaleLowerCase()}</span>
91
+ )}
92
+ </>
93
+ ) : (
94
+ <>
95
+ <span className={styles.label01}>
96
+ {t('dosingInstructions', 'Dosing Instructions').toUpperCase()}{' '}
97
+ </span>
98
+ <span className={styles.dosage}>{medication?.order?.dosingInstructions}</span>
99
+ {medication?.order?.duration && (
100
+ <span>
101
+ {' '}
102
+ &mdash;{' '}
103
+ {t('orderDurationAndUnit', 'for {{duration}} {{durationUnit}}', {
104
+ duration: medication?.order?.duration,
105
+ durationUnit: medication?.order?.durationUnits?.display?.toLowerCase(),
106
+ })}
107
+ </span>
108
+ )}
109
+ {medication?.order?.numRefills !== 0 && (
110
+ <span>
111
+ <span className={styles.label01}> &mdash; {t('refills', 'Refills').toUpperCase()}</span>{' '}
112
+ {medication?.order?.numRefills}
113
+ </span>
114
+ )}
115
+ </>
116
+ )}
117
+ </p>
118
+ <p className={styles.bodyLong01}>
119
+ {medication?.order?.orderReasonNonCoded ? (
120
+ <span>
121
+ <span className={styles.label01}>{t('indication', 'Indication').toUpperCase()}</span>{' '}
122
+ {medication?.order?.orderReasonNonCoded}
123
+ </span>
124
+ ) : null}
125
+ {medication?.order?.orderReasonNonCoded && medication?.order?.quantity && <>&mdash;</>}
126
+ {medication?.order?.quantity ? (
127
+ <span>
128
+ <span className={styles.label01}> {t('quantity', 'Quantity').toUpperCase()}</span>{' '}
129
+ {medication?.order?.quantity}
130
+ </span>
131
+ ) : null}
132
+ {medication?.order?.dateStopped ? (
133
+ <span className={styles.bodyShort01}>
134
+ <span className={styles.label01}>
135
+ {medication?.order?.quantity ? ` — ` : ''} {t('endDate', 'End date').toUpperCase()}
136
+ </span>{' '}
137
+ {formatDate(new Date(medication?.order?.dateStopped))}
138
+ </span>
139
+ ) : null}
140
+ </p>
141
+ </div>
142
+ </div>
143
+
144
+ <p className={styles.metadata}>
145
+ <div className={styles.startDateColumn}>
146
+ <span>{formatDate(new Date(medication.order.dateActivated))}</span> &middot;{' '}
147
+ <span>{medication.order.orderer?.display ?? '--'}</span>
148
+ </div>
149
+ </p>
150
+ </React.Fragment>
151
+ ),
152
+ )}
153
+ </div>
154
+ );
155
+ };
156
+
157
+ export default MedicationSummary;
@@ -0,0 +1,34 @@
1
+ import React from 'react';
2
+ import classNames from 'classnames';
3
+ import { useTranslation } from 'react-i18next';
4
+ import type { Note } from '../visit.resource';
5
+ import { EmptyState } from '@openmrs/esm-patient-common-lib';
6
+ import styles from '../visit-detail-overview.scss';
7
+
8
+ interface NotesSummaryProps {
9
+ notes: Array<Note>;
10
+ }
11
+
12
+ const NotesSummary: React.FC<NotesSummaryProps> = ({ notes }) => {
13
+ const { t } = useTranslation();
14
+
15
+ if (notes.length === 0) {
16
+ return <EmptyState displayText={t('notes__lower', 'notes')} headerTitle={t('notes', 'Notes')} />;
17
+ }
18
+
19
+ return (
20
+ <>
21
+ {notes.map((note: Note, index) => (
22
+ <div className={styles.notesContainer} key={index}>
23
+ <p className={classNames(styles.noteText, styles.bodyLong01)}>{note.note}</p>
24
+ <p className={styles.metadata}>
25
+ {note.time} {note.provider.name ? <span>&middot; {note.provider.name} </span> : null}
26
+ {note.provider.role ? <span>&middot; {note.provider.role}</span> : null}
27
+ </p>
28
+ </div>
29
+ ))}
30
+ </>
31
+ );
32
+ };
33
+
34
+ export default NotesSummary;
@@ -0,0 +1,16 @@
1
+ import React from 'react';
2
+ import { type Encounter, ExtensionSlot } from '@openmrs/esm-framework';
3
+ import { type ExternalOverviewProps } from '@openmrs/esm-patient-common-lib';
4
+
5
+ const TestsSummary = ({ patientUuid, encounters }: { patientUuid: string; encounters: Array<Encounter> }) => {
6
+ const filter = React.useMemo<ExternalOverviewProps['filter']>(() => {
7
+ const encounterIds = encounters.map((e) => `Encounter/${e.uuid}`);
8
+ return ([entry]) => {
9
+ return encounterIds.includes(entry.encounter?.reference);
10
+ };
11
+ }, [encounters]);
12
+
13
+ return <ExtensionSlot name="test-results-filtered-overview-slot" state={{ filter, patientUuid }} />;
14
+ };
15
+
16
+ export default TestsSummary;
@@ -0,0 +1,4 @@
1
+ .visitActions {
2
+ display: flex;
3
+ align-items: center;
4
+ }
@@ -0,0 +1,176 @@
1
+ import React, { useMemo } from 'react';
2
+ import classNames from 'classnames';
3
+ import { useTranslation } from 'react-i18next';
4
+ import { Tab, TabList, TabPanel, TabPanels, Tabs } from '@carbon/react';
5
+ import {
6
+ type Diagnosis,
7
+ Extension,
8
+ ExtensionSlot,
9
+ formatTime,
10
+ parseDate,
11
+ useAssignedExtensions,
12
+ useConfig,
13
+ type Visit,
14
+ } from '@openmrs/esm-framework';
15
+ import { DiagnosisTags } from '../../visit-history-table/diagnosis-tags.component';
16
+ import type { ExternalOverviewProps } from '@openmrs/esm-patient-common-lib';
17
+ import { type Note, type Order, type OrderItem } from '../visit.resource';
18
+ import MedicationSummary from './medications-summary.component';
19
+ import NotesSummary from './notes-summary.component';
20
+ import TestsSummary from './tests-summary.component';
21
+ import VisitEncountersTable from './encounters-table/visit-encounters-table.component';
22
+ import VisitTimeline from '../single-visit-details/visit-timeline/visit-timeline.component';
23
+ import { type ChartConfig } from '../../../../config-schema';
24
+ import styles from './visit-summary.scss';
25
+
26
+ interface VisitSummaryProps {
27
+ visit: Visit;
28
+ patientUuid: string;
29
+ }
30
+
31
+ const visitSummaryPanelSlot = 'visit-summary-panels';
32
+
33
+ const VisitSummary: React.FC<VisitSummaryProps> = ({ visit, patientUuid }) => {
34
+ const config = useConfig<ChartConfig>();
35
+ const { t } = useTranslation();
36
+ const extensions = useAssignedExtensions(visitSummaryPanelSlot);
37
+
38
+ const [diagnoses, notes, medications]: [Array<Diagnosis>, Array<Note>, Array<OrderItem>] = useMemo(() => {
39
+ // Medication Tab
40
+ const medications: Array<OrderItem> = [];
41
+ // Diagnoses in a Visit
42
+ const diagnoses: Array<Diagnosis> = [];
43
+ // Notes Tab
44
+ const notes: Array<Note> = [];
45
+
46
+ visit?.encounters?.forEach((enc) => {
47
+ if (enc.hasOwnProperty('orders')) {
48
+ medications.push(
49
+ ...enc.orders.map((order: Order) => ({
50
+ order,
51
+ provider: {
52
+ name: enc.encounterProviders.length ? enc.encounterProviders[0].provider.person.display : '',
53
+ role: enc.encounterProviders.length ? enc.encounterProviders[0].encounterRole.display : '',
54
+ },
55
+ })),
56
+ );
57
+ }
58
+
59
+ // Check if there is a diagnosis associated with this encounter
60
+ if (enc.hasOwnProperty('diagnoses')) {
61
+ if (enc.diagnoses.length > 0) {
62
+ const validDiagnoses = enc.diagnoses.filter((diagnosis) => !diagnosis.voided);
63
+ diagnoses.push(...validDiagnoses);
64
+ }
65
+ }
66
+
67
+ // Check for Visit Diagnoses and Notes
68
+ if (enc.hasOwnProperty('obs')) {
69
+ enc.obs.forEach((obs) => {
70
+ if (config.notesConceptUuids?.includes(obs.concept.uuid)) {
71
+ // Putting all notes in a single array.
72
+ notes.push({
73
+ note: obs.value as string, // TODO: add better typing check
74
+ provider: {
75
+ name: enc.encounterProviders.length ? enc.encounterProviders[0].provider.person.display : '',
76
+ role: enc.encounterProviders.length ? enc.encounterProviders[0].encounterRole.display : '',
77
+ },
78
+ time: enc.encounterDatetime ? formatTime(parseDate(enc.encounterDatetime)) : '',
79
+ concept: obs.concept,
80
+ });
81
+ }
82
+ });
83
+ }
84
+ });
85
+
86
+ // Sort the diagnoses by rank, so that primary diagnoses come first
87
+ diagnoses.sort((a, b) => a.rank - b.rank);
88
+
89
+ // Sort medications by dateActivated DESC (newest first) to align with backend ordering
90
+ medications.sort((a, b) => new Date(b.order.dateActivated).getTime() - new Date(a.order.dateActivated).getTime());
91
+
92
+ return [diagnoses, notes, medications];
93
+ }, [config.notesConceptUuids, visit?.encounters]);
94
+
95
+ const testsFilter = useMemo<ExternalOverviewProps['filter']>(() => {
96
+ const encounterIds = visit?.encounters?.map((e) => `Encounter/${e.uuid}`);
97
+ return ([entry]) => {
98
+ return encounterIds.includes(entry.encounter?.reference);
99
+ };
100
+ }, [visit?.encounters]);
101
+
102
+ return (
103
+ <div className={styles.summaryContainer}>
104
+ <p className={styles.diagnosisLabel}>{t('diagnoses', 'Diagnoses')}</p>
105
+ <div className={styles.diagnosesList}>
106
+ {diagnoses.length > 0 ? (
107
+ <DiagnosisTags diagnoses={diagnoses} />
108
+ ) : (
109
+ <p className={classNames(styles.bodyLong01, styles.text02)} style={{ marginBottom: '0.5rem' }}>
110
+ {t('noDiagnosesFound', 'No diagnoses found')}
111
+ </p>
112
+ )}
113
+ </div>
114
+ <Tabs>
115
+ <TabList aria-label="Visit summary tabs" className={styles.tablist}>
116
+ <Tab className={classNames(styles.tab, styles.bodyLong01)} id="timeline-tab">
117
+ {t('timeline', 'Timeline')}
118
+ </Tab>
119
+ <Tab
120
+ className={classNames(styles.tab, styles.bodyLong01)}
121
+ id="notes-tab"
122
+ disabled={notes.length <= 0 && config.disableEmptyTabs}>
123
+ {t('notes', 'Notes')}
124
+ </Tab>
125
+ <Tab className={styles.tab} id="tests-tab" disabled={testsFilter.length <= 0 && config.disableEmptyTabs}>
126
+ {t('tests', 'Tests')}
127
+ </Tab>
128
+ <Tab
129
+ className={styles.tab}
130
+ id="medications-tab"
131
+ disabled={medications.length <= 0 && config.disableEmptyTabs}>
132
+ {t('medications', 'Medications')}
133
+ </Tab>
134
+ <Tab
135
+ className={styles.tab}
136
+ id="encounters-tab"
137
+ disabled={visit?.encounters.length <= 0 && config.disableEmptyTabs}>
138
+ {t('encounters_title', 'Encounters')}
139
+ </Tab>
140
+ {extensions?.map((extension, index) => (
141
+ <Tab key={index} className={styles.tab} id={`${extension.meta.title || index}-tab`}>
142
+ {t(extension.meta.title, {
143
+ ns: extension.moduleName,
144
+ defaultValue: extension.meta.title,
145
+ })}
146
+ </Tab>
147
+ ))}
148
+ </TabList>
149
+ <TabPanels>
150
+ <TabPanel>
151
+ <VisitTimeline visitUuid={visit.uuid} patientUuid={patientUuid} />
152
+ </TabPanel>
153
+ <TabPanel>
154
+ <NotesSummary notes={notes} />
155
+ </TabPanel>
156
+ <TabPanel>
157
+ <TestsSummary patientUuid={patientUuid} encounters={visit?.encounters} />
158
+ </TabPanel>
159
+ <TabPanel>
160
+ <MedicationSummary medications={medications} />
161
+ </TabPanel>
162
+ <TabPanel>
163
+ <VisitEncountersTable visit={visit} patientUuid={patientUuid} />
164
+ </TabPanel>
165
+ <ExtensionSlot name={visitSummaryPanelSlot}>
166
+ <TabPanel>
167
+ <Extension state={{ patientUuid, visit }} />
168
+ </TabPanel>
169
+ </ExtensionSlot>
170
+ </TabPanels>
171
+ </Tabs>
172
+ </div>
173
+ );
174
+ };
175
+
176
+ export default VisitSummary;
@@ -0,0 +1,72 @@
1
+ @use '@carbon/colors';
2
+ @use '@carbon/layout';
3
+ @use '@carbon/type';
4
+ @use '@openmrs/esm-styleguide/src/vars' as *;
5
+
6
+ .diagnosisLabel {
7
+ @include type.type-style('heading-compact-01');
8
+ color: $text-02;
9
+ margin-top: 5px;
10
+ }
11
+
12
+ .diagnosesList {
13
+ display: flex;
14
+ flex-flow: row wrap;
15
+ padding-bottom: layout.$spacing-03;
16
+ margin: 0 layout.$spacing-05;
17
+ border-bottom: 1px solid $ui-03;
18
+ }
19
+
20
+ .summaryContainer {
21
+ background-color: $ui-background;
22
+ display: grid;
23
+ grid-template-columns: max-content auto;
24
+ padding: layout.$spacing-05;
25
+
26
+ :global(.cds--tabs) {
27
+ min-height: 10rem;
28
+ }
29
+ }
30
+
31
+ .tab {
32
+ outline: 0;
33
+ outline-offset: 0;
34
+ min-height: 2rem !important;
35
+
36
+ &:active,
37
+ &:focus {
38
+ outline: layout.$spacing-01 solid var(--brand-03) !important;
39
+ }
40
+
41
+ &[aria-selected='true'] {
42
+ border-left: 3px solid var(--brand-03);
43
+ border-bottom: none;
44
+ font-weight: 600;
45
+ margin-left: 0 !important;
46
+ }
47
+
48
+ &[aria-selected='false'] {
49
+ border-bottom: none;
50
+ border-left: layout.$spacing-01 solid $ui-03;
51
+ margin-left: 0 !important;
52
+ }
53
+ }
54
+
55
+ .tablist {
56
+ :global(.cds--tab--list) {
57
+ flex-direction: column;
58
+ max-height: fit-content;
59
+ }
60
+
61
+ > button :global(.cds--tabs .cds--tabs__nav-link) {
62
+ border-bottom: none;
63
+ }
64
+ }
65
+
66
+ .text02 {
67
+ color: colors.$gray-70;
68
+ }
69
+
70
+ .bodyLong01 {
71
+ @include type.type-style('body-01');
72
+ }
@@ -0,0 +1,94 @@
1
+ import React from 'react';
2
+ import dayjs from 'dayjs';
3
+ import { useTranslation } from 'react-i18next';
4
+ import { SkeletonText } from '@carbon/react';
5
+ import { formatDate } from '@openmrs/esm-framework';
6
+ import { CardHeader, EmptyState } from '@openmrs/esm-patient-common-lib';
7
+ import { useEncountersByVisit } from '../../../../clinical-views/hooks/useEncountersByVisit';
8
+ import styles from './visit-timeline.scss';
9
+
10
+ interface VisitTimelineProps {
11
+ patientUuid: string;
12
+ visitUuid: string;
13
+ }
14
+
15
+ function VisitTimeline({ patientUuid, visitUuid }: VisitTimelineProps) {
16
+ const { t } = useTranslation();
17
+ const { encounters, isLoading } = useEncountersByVisit(patientUuid, visitUuid);
18
+
19
+ if (isLoading) {
20
+ return (
21
+ <div className={styles.visitTimeline}>
22
+ <CardHeader title={t('timeline', 'Timeline')}>{null}</CardHeader>
23
+ <p className={styles.timelineHeader}>
24
+ <span>{t('encounter', 'Encounter')}</span> <span>&middot;</span>
25
+ <span>{t('provider', 'Provider')}</span> <span>&middot;</span>{' '}
26
+ <span>
27
+ {t('timeStarted', 'Time started')} <span>&mdash;</span> {t('timeCompleted', 'Time completed')}{' '}
28
+ </span>
29
+ </p>
30
+ <div className={styles.timelineEntries}>
31
+ {Array.from({ length: 3 }).map((_, index) => (
32
+ <p className={styles.timelineEntry} key={index}>
33
+ <div className={styles.timelineDot} />
34
+ <SkeletonText className={styles.skeleton} />
35
+ <span>&middot;</span>
36
+ <SkeletonText className={styles.skeleton} />
37
+ <span>&middot;</span>
38
+ <SkeletonText className={styles.skeleton} />
39
+ <span>&mdash;</span>
40
+ <SkeletonText className={styles.skeleton} />
41
+ </p>
42
+ ))}
43
+ <div className={styles.timelineLine} />
44
+ </div>
45
+ </div>
46
+ );
47
+ }
48
+
49
+ if (encounters?.length === 0) {
50
+ return (
51
+ <EmptyState
52
+ displayText={t('encountersForThisVisit', 'encounters for this visit')}
53
+ headerTitle={t('timeline', 'Timeline')}
54
+ />
55
+ );
56
+ }
57
+
58
+ return (
59
+ <div className={styles.visitTimeline}>
60
+ <p className={styles.timelineHeader}>
61
+ <span>{t('encounter', 'Encounter')}</span> <span>&middot;</span>
62
+ <span>{t('provider', 'Provider')}</span> <span>&middot;</span>
63
+ <span>{t('timeStarted', 'Time started')}</span>
64
+ </p>
65
+ <div className={styles.timelineEntries}>
66
+ {encounters?.map((encounter) => (
67
+ <p className={styles.timelineEntry} key={encounter.uuid}>
68
+ <div className={styles.timelineDot} />
69
+ <span className={styles.encounterType}>{encounter.encounterType.display}</span>
70
+ <span>&middot;</span>
71
+ {encounter.encounterProviders.length === 0 ? (
72
+ <span>{t('noProvider', 'No provider')}</span>
73
+ ) : (
74
+ <span>
75
+ {encounter.encounterProviders
76
+ .map((encounterProvider) => encounterProvider.provider.person.display)
77
+ .join(', ')}
78
+ </span>
79
+ )}
80
+ <span>&middot;</span>{' '}
81
+ <span>
82
+ {formatDate(new Date(encounter.encounterDatetime), {
83
+ time: dayjs(encounter.encounterDatetime).isSame(dayjs(), 'day') ? 'for today' : true,
84
+ })}
85
+ </span>
86
+ </p>
87
+ ))}
88
+ <div className={styles.timelineLine} />
89
+ </div>
90
+ </div>
91
+ );
92
+ }
93
+
94
+ export default VisitTimeline;
@@ -0,0 +1,60 @@
1
+ @use '@carbon/colors';
2
+ @use '@carbon/layout';
3
+ @use '@carbon/type';
4
+
5
+ .visitTimeline {
6
+ border: 1px solid colors.$gray-20;
7
+ background-color: colors.$white;
8
+ padding-top: layout.$spacing-05;
9
+ }
10
+
11
+ .timelineHeader {
12
+ padding: 0 layout.$spacing-10;
13
+ @include type.type-style('helper-text-01');
14
+ display: flex;
15
+ gap: layout.$spacing-03;
16
+ }
17
+
18
+ .timelineEntries {
19
+ margin: 0 layout.$spacing-08;
20
+ margin-top: layout.$spacing-04;
21
+ display: flex;
22
+ flex-direction: column;
23
+ gap: layout.$spacing-05;
24
+ margin-bottom: layout.$spacing-05;
25
+ position: relative;
26
+ }
27
+
28
+ .timelineEntry {
29
+ display: flex;
30
+ gap: layout.$spacing-03;
31
+ align-items: center;
32
+ }
33
+
34
+ .encounterType {
35
+ font-weight: 700;
36
+ }
37
+
38
+ .timelineDot {
39
+ height: 8px;
40
+ width: 8px;
41
+ background-color: colors.$blue-30;
42
+ border-radius: 50%;
43
+ display: inline-block;
44
+ margin-right: layout.$spacing-03;
45
+ z-index: 10;
46
+ }
47
+
48
+ .timelineLine {
49
+ position: absolute;
50
+ border-left: 1px solid colors.$blue-20;
51
+ height: 100%;
52
+ left: 3.4px;
53
+ top: 4px;
54
+ z-index: 9;
55
+ }
56
+
57
+ .skeleton {
58
+ width: 5% !important; // using hardcoded px values could be a bad idea.
59
+ margin-bottom: 0 !important;
60
+ }