@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
package/dist/routes.json CHANGED
@@ -1 +1 @@
1
- {"$schema":"https://json.openmrs.org/routes.schema.json","backendDependencies":{"fhir2":">=1.2","webservices.rest":"^2.24.0"},"pages":[],"extensions":[{"name":"clinical-workflow-triage-dashboard-link","component":"triageDashboardLink","slot":"homepage-dashboard-slot","order":0,"meta":{"name":"clinical-workflow-triage","slot":"clinical-workflow-triage-dashboard-slot","title":"clinical-workflow-triage"}},{"name":"clinical-workflow-triage-dashboard","component":"triageDashboard","slot":"clinical-workflow-triage-dashboard-slot"},{"name":"ewf-mru-dashboard-link","component":"mruLeftPanelLink","slot":"homepage-dashboard-slot","order":1,"meta":{"name":"mru","title":"MRU","path":"mru","slot":"mru-dashboard-slot"}},{"name":"ewf-mru-dashboard","component":"mruDashboard","slot":"mru-dashboard-slot","online":true,"offline":false},{"name":"patient-scoreboard-link","component":"patientScoreboardLink","slot":"homepage-dashboard-slot","order":1,"meta":{"name":"patient-scoreboard","title":"Patient Scoreboard","path":"patient-scoreboard","slot":"patient-scoreboard-slot"}},{"name":"patient-scoreboard","component":"patientScoreboard","slot":"patient-scoreboard-slot","online":true,"offline":false}],"workspaces":[{"name":"triage-workspace","component":"triageWorkspace","title":"Triage Form","canMaximize":true,"type":"clinical-form"},{"name":"patient-registration-workspace","component":"patientRegistrationWorkspace","title":"Patient Registration","canMaximize":true,"type":"registration-form"},{"name":"billing-information-workspace","component":"billingInformationWorkspace","title":"Billing Information Workspace","type":"clinical-form"}],"workspaces2":[{"name":"clinical-workflow-patient-form-entry-workspace","component":"@openmrs/esm-patient-forms-app#exportedPatientFormEntryWorkspace","window":"clinical-workflow-window"}],"workspaceWindows2":[{"name":"clinical-workflow-window","group":"clinical-workflow-group","width":"wider","canMaximize":true}],"workspaceGroups2":[{"name":"clinical-workflow-group","overlay":true,"persistence":"closable"}],"version":"5.4.2-pre.20"}
1
+ {"$schema":"https://json.openmrs.org/routes.schema.json","backendDependencies":{"fhir2":">=1.2","webservices.rest":"^2.24.0"},"pages":[],"extensions":[{"name":"clinical-workflow-triage-dashboard-link","component":"triageDashboardLink","slot":"homepage-dashboard-slot","order":0,"meta":{"name":"clinical-workflow-triage","slot":"clinical-workflow-triage-dashboard-slot","title":"clinical-workflow-triage"}},{"name":"clinical-workflow-triage-dashboard","component":"triageDashboard","slot":"clinical-workflow-triage-dashboard-slot"},{"name":"ewf-mru-dashboard-link","component":"mruLeftPanelLink","slot":"homepage-dashboard-slot","order":1,"meta":{"name":"mru","title":"MRU","path":"mru","slot":"mru-dashboard-slot"}},{"name":"ewf-mru-dashboard","component":"mruDashboard","slot":"mru-dashboard-slot","online":true,"offline":false},{"name":"patient-scoreboard-link","component":"patientScoreboardLink","slot":"homepage-dashboard-slot","order":1,"meta":{"name":"patient-scoreboard","title":"Patient Scoreboard","path":"patient-scoreboard","slot":"patient-scoreboard-slot"}},{"name":"patient-scoreboard","component":"patientScoreboard","slot":"patient-scoreboard-slot","online":true,"offline":false},{"name":"past-visits-detail-overview-shadow","slot":"patient-chart-encounters-dashboard-slot","component":"pastVisitsDetailOverviewShadow","order":0,"meta":{"title":"Visits","view":"visits"},"online":true,"offline":true}],"workspaces":[{"name":"triage-workspace","component":"triageWorkspace","title":"Triage Form","canMaximize":true,"type":"clinical-form"},{"name":"patient-registration-workspace","component":"patientRegistrationWorkspace","title":"Patient Registration","canMaximize":true,"type":"registration-form"},{"name":"billing-information-workspace","component":"billingInformationWorkspace","title":"Billing Information Workspace","type":"clinical-form"}],"workspaces2":[{"name":"clinical-workflow-patient-form-entry-workspace","component":"@openmrs/esm-patient-forms-app#exportedPatientFormEntryWorkspace","window":"clinical-workflow-window"},{"name":"visit-notes-form-shadow-workspace","component":"visitNotesFormWorkspace","window":"visit-note-shadow"}],"workspaceWindows2":[{"name":"clinical-workflow-window","group":"clinical-workflow-group","width":"wider","canMaximize":true},{"name":"visit-note-shadow","group":"patient-chart","icon":"visitNoteActionButton","order":3}],"workspaceGroups2":[{"name":"clinical-workflow-group","overlay":true,"persistence":"closable"}],"version":"5.4.2-pre.26"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@palladium-ethiopia/esm-clinical-workflow-app",
3
- "version": "5.4.2-pre.20",
3
+ "version": "5.4.2-pre.26",
4
4
  "description": "Express workflow app for OpenMRS 3",
5
5
  "keywords": [
6
6
  "openmrs",
@@ -1,4 +1,5 @@
1
1
  import { Type } from '@openmrs/esm-framework';
2
+ import notesConfigSchema, { type VisitNoteConfigObject } from './patient-notes/visit-note-config-schema';
2
3
 
3
4
  export const configSchema = {
4
5
  triageLocationForms: {
@@ -46,6 +47,54 @@ export const configSchema = {
46
47
  _description: 'Patient attribute type UUID for Medico Legal Cases',
47
48
  _default: '',
48
49
  },
50
+ diagnosisConceptClass: {
51
+ _type: Type.UUID,
52
+ _default: '8d4918b0-c2cc-11de-8d13-0010c6dffd0f',
53
+ _description: 'The concept class UUID for diagnoses',
54
+ },
55
+ isPrimaryDiagnosisRequired: {
56
+ _type: Type.Boolean,
57
+ _default: true,
58
+ _description: 'Indicates whether a primary diagnosis is required when submitting a visit note',
59
+ },
60
+ visitNoteConfig: notesConfigSchema,
61
+ disableEmptyTabs: {
62
+ _type: Type.Boolean,
63
+ _default: false,
64
+ _description: 'Disable notes/tests/medications/encounters tabs when empty',
65
+ },
66
+ encounterEditableDuration: {
67
+ _type: Type.Number,
68
+ _default: 0,
69
+ _description:
70
+ 'The number of minutes an encounter is editable after it is created. 0 means the encounter is editable forever.',
71
+ },
72
+ encounterEditableDurationOverridePrivileges: {
73
+ _type: Type.Array,
74
+ _elements: {
75
+ _type: Type.String,
76
+ },
77
+ _default: [],
78
+ _description:
79
+ 'The privileges that allow users to edit encounters even after the editable duration (set by `encounterEditableDuration`) has expired. Any privilege in the list is sufficient to edit the encounter.',
80
+ },
81
+ notesConceptUuids: {
82
+ _type: Type.Array,
83
+ _elements: {
84
+ _type: Type.ConceptUuid,
85
+ },
86
+ _default: ['162169AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'],
87
+ },
88
+ showAllEncountersTab: {
89
+ _type: Type.Boolean,
90
+ _description: 'Shows the All Encounters Tab of Patient Visits section in Patient Chart',
91
+ _default: true,
92
+ },
93
+ drugOrderTypeUUID: {
94
+ _type: Type.UUID,
95
+ _description: "UUID for the 'Drug' order type to fetch medications",
96
+ _default: '131168f4-15f5-102d-96e4-000c29c2a5d7',
97
+ },
49
98
  };
50
99
 
51
100
  export type ClinicalWorkflowConfig = {
@@ -67,3 +116,52 @@ export type ClinicalWorkflowConfig = {
67
116
  defaultIdentifierTypeUuid: string;
68
117
  medicoLegalCasesAttributeTypeUuid: string;
69
118
  };
119
+
120
+ export interface VisitNoteConfig {
121
+ diagnosisConceptClass: string;
122
+ isPrimaryDiagnosisRequired: boolean;
123
+ visitNoteConfig: VisitNoteConfigObject;
124
+ }
125
+
126
+ export interface ChartConfig {
127
+ defaultFacilityUrl: string;
128
+ disableChangingVisitLocation: boolean;
129
+ disableEmptyTabs: boolean;
130
+ encounterEditableDuration: number;
131
+ encounterEditableDurationOverridePrivileges: Array<string>;
132
+ freeTextFieldConceptUuid: string;
133
+ logo: {
134
+ alt: string;
135
+ name: string;
136
+ src: string;
137
+ };
138
+ notesConceptUuids: string[];
139
+ offlineVisitTypeUuid: string;
140
+ restrictByVisitLocationTag: boolean;
141
+ showAllEncountersTab: boolean;
142
+ showRecommendedVisitTypeTab: boolean;
143
+ showServiceQueueFields: boolean; // used by extension from esm-service-queues-app
144
+ showUpcomingAppointments: boolean; // used by extension from esm-appointments-app
145
+ visitTypeResourceUrl: string;
146
+ visitAttributeTypes: Array<{
147
+ displayInThePatientBanner: boolean;
148
+ required: boolean;
149
+ showWhenExpression?: string;
150
+ uuid: string;
151
+ }>;
152
+ visitDiagnosisConceptUuid: string;
153
+ requireActiveVisitForEncounterTile: boolean;
154
+ trueConceptUuid: string;
155
+ falseConceptUuid: string;
156
+ tileDefinitions: Array<{
157
+ title: string;
158
+ columns: Array<{
159
+ title: string;
160
+ concept: string;
161
+ encounterType: string;
162
+ hasSummary?: boolean;
163
+ }>;
164
+ }>;
165
+ otherConceptUuid: string;
166
+ drugOrderTypeUUID: string;
167
+ }
package/src/index.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { defineConfigSchema, getAsyncLifecycle, getSyncLifecycle } from '@openmrs/esm-framework';
1
+ import { defineConfigSchema, getAsyncLifecycle, getGlobalStore, getSyncLifecycle } from '@openmrs/esm-framework';
2
2
  import { createDashboardLink } from './createDashboardLink';
3
3
  import { configSchema } from './config-schema';
4
4
  import { dashboardMeta } from './dashboard.meta';
@@ -6,6 +6,8 @@ import MRUDashboard from './mru/dashboard.component';
6
6
  import { spaBasePath } from './constants';
7
7
  import BillingInformationWorkspace from './mru/billing-information/billing-information.workspace';
8
8
  import PatientScoreboard from './patient-scoreboard/patient-scoreboard.component';
9
+ import visitNotesActionButtonExtension from './patient-notes/visit-note-action-button.extension';
10
+ import pastVisitsOverviewComponent from './patient-chart/visit/visits-widget/visit-detail-overview.component';
9
11
 
10
12
  const moduleName = '@ethiopia/esm-clinical-workflow-app';
11
13
 
@@ -16,6 +18,23 @@ const options = {
16
18
 
17
19
  export function startupApp() {
18
20
  defineConfigSchema(moduleName, configSchema);
21
+
22
+ // Remove the core visit-note workspace window since we're providing a custom one
23
+ const workspace2Store = getGlobalStore<any>('workspace2');
24
+ const removeCoreButton = () => {
25
+ const state = workspace2Store.getState();
26
+ if (state.registeredWindowsByName['visit-note']) {
27
+ const newWindows = { ...state.registeredWindowsByName };
28
+ delete newWindows['visit-note'];
29
+ workspace2Store.setState({
30
+ registeredWindowsByName: newWindows,
31
+ });
32
+ }
33
+ };
34
+
35
+ // Try immediately and also subscribe to handle cases where the core module registers later
36
+ removeCoreButton();
37
+ workspace2Store.subscribe(removeCoreButton);
19
38
  }
20
39
 
21
40
  export const root = getAsyncLifecycle(() => import('./root.component'), options);
@@ -49,3 +68,15 @@ export const patientScoreboardLink = getSyncLifecycle(
49
68
  );
50
69
 
51
70
  export const patientScoreboard = getSyncLifecycle(PatientScoreboard, options);
71
+
72
+ export const visitNoteActionButton = getSyncLifecycle(visitNotesActionButtonExtension, options);
73
+
74
+ export const visitNotesFormWorkspace = getAsyncLifecycle(
75
+ () => import('./patient-notes/visit-notes-form-shadow.workspace'),
76
+ options,
77
+ );
78
+
79
+ export const pastVisitsDetailOverviewShadow = getSyncLifecycle(pastVisitsOverviewComponent, {
80
+ featureName: 'visits-detail-overview',
81
+ moduleName,
82
+ });
@@ -0,0 +1,13 @@
1
+ import { type Encounter, restBaseUrl, useOpenmrsFetchAll } from '@openmrs/esm-framework';
2
+
3
+ export function useEncountersByVisit(patientUuid: string, visitUuid: string) {
4
+ const customRepresentation =
5
+ 'custom:(uuid,encounterType:(uuid,display),encounterProviders:(provider:(person:(display))),encounterDatetime,visit:(uuid))';
6
+ const url = `${restBaseUrl}/encounter?patient=${patientUuid}&order=desc&visit=${visitUuid}&v=${customRepresentation}`;
7
+ const { data: encounters, ...rest } = useOpenmrsFetchAll<Encounter>(url);
8
+
9
+ return {
10
+ encounters,
11
+ ...rest,
12
+ };
13
+ }
@@ -0,0 +1,11 @@
1
+ export const clinicalFormsWorkspace = 'clinical-forms-workspace';
2
+ export const formEntryWorkspace = 'patient-form-entry-workspace';
3
+ export const spaRoot = window['getOpenmrsSpaBase']();
4
+ export const basePath = '/patient/:patientUuid/chart';
5
+ export const dashboardPath = `${basePath}/:view/*`;
6
+ export const spaBasePath = `${window.spaBase}${basePath}`;
7
+ export const moduleName = '@openmrs/esm-patient-chart-app';
8
+ export const patientChartWorkspaceSlot = 'patient-chart-workspace-slot';
9
+ export const patientChartWorkspaceHeaderSlot = 'patient-chart-workspace-header-slot';
10
+ export const omrsDateFormat = 'YYYY-MM-DDTHH:mm:ss.SSSZZ';
11
+ export const jsonSchemaResourceName = 'JSON schema';
@@ -0,0 +1,43 @@
1
+ import React from 'react';
2
+ import { Tag } from '@carbon/react';
3
+ import { CheckmarkFilled, Help } from '@carbon/react/icons';
4
+ import { type Diagnosis } from '@openmrs/esm-emr-api';
5
+ import styles from './diagnosis-tags.module.scss';
6
+
7
+ interface DiagnosisTagsProps {
8
+ diagnoses: Array<Diagnosis>;
9
+ }
10
+
11
+ export const DiagnosisTags: React.FC<DiagnosisTagsProps> = ({ diagnoses = [] }) => {
12
+ if (!diagnoses || diagnoses.length === 0) {
13
+ return null;
14
+ }
15
+
16
+ return (
17
+ <div className={styles.diagnosesText}>
18
+ {diagnoses.map((diagnosis, index) => {
19
+ const { uuid, display, certainty = 'PROVISIONAL', rank } = diagnosis;
20
+ const color = rank === 1 ? 'red' : 'blue';
21
+ const displayText = display.length > 30 ? `${display.substring(0, 30)}...` : display;
22
+ const certaintyText = certainty === 'CONFIRMED' ? 'Confirmed' : 'Presumed';
23
+ const fullTooltip = `${display} (${certaintyText})`;
24
+
25
+ return (
26
+ /* The title goes here on the wrapper */
27
+ <div key={uuid || index} className={styles.tagWrapper} title={fullTooltip}>
28
+ <Tag className={styles.tag} type={color}>
29
+ <span className={styles.tagContent}>
30
+ <span className={styles.textLabel}>{displayText}</span>
31
+ {certainty === 'CONFIRMED' ? (
32
+ <CheckmarkFilled size={14} className={`${styles.tagIcon} ${styles.confirmedIcon}`} />
33
+ ) : (
34
+ <Help size={14} className={`${styles.tagIcon} ${styles.presumedIcon}`} />
35
+ )}
36
+ </span>
37
+ </Tag>
38
+ </div>
39
+ );
40
+ })}
41
+ </div>
42
+ );
43
+ };
@@ -0,0 +1,57 @@
1
+ @use '@carbon/colors';
2
+ @use '@carbon/layout';
3
+ @use '@carbon/type';
4
+
5
+ .diagnosesText {
6
+ display: flex;
7
+ flex-wrap: wrap;
8
+ gap: layout.$spacing-03;
9
+ margin-bottom: layout.$spacing-05;
10
+ }
11
+
12
+ .tagWrapper {
13
+ display: inline-flex;
14
+ /* Makes the wrapper behave like the tag for the mouse */
15
+ height: fit-content;
16
+ width: fit-content;
17
+ }
18
+
19
+ .tag {
20
+ max-width: layout.rem(320px);
21
+ margin: 0 !important;
22
+ /* Prevents internal Carbon elements from stealing hover event */
23
+ pointer-events: none;
24
+
25
+ :global(.cds--tag__label) {
26
+ display: flex;
27
+ align-items: center;
28
+ max-width: 100%;
29
+ }
30
+ }
31
+
32
+ .tagContent {
33
+ display: flex;
34
+ align-items: center;
35
+ gap: layout.$spacing-02;
36
+ width: 100%;
37
+ }
38
+
39
+ .textLabel {
40
+ overflow: hidden;
41
+ text-overflow: ellipsis;
42
+ white-space: nowrap;
43
+ }
44
+
45
+ .tagIcon {
46
+ flex-shrink: 0;
47
+ display: flex;
48
+ align-items: center;
49
+ }
50
+
51
+ .confirmedIcon {
52
+ color: colors.$green-60;
53
+ }
54
+
55
+ .presumedIcon {
56
+ color: colors.$orange-60;
57
+ }
@@ -0,0 +1,20 @@
1
+ import { ExtensionSlot, type Visit } from '@openmrs/esm-framework';
2
+ import React from 'react';
3
+ import styles from './visit-actions-cell.scss';
4
+
5
+ interface Props {
6
+ visit: Visit;
7
+ patient: fhir.Patient;
8
+ }
9
+
10
+ const VisitActionsCell: React.FC<Props> = ({ visit, patient }) => {
11
+ return (
12
+ <ExtensionSlot
13
+ name="visit-detail-overview-actions"
14
+ className={styles.visitActions}
15
+ state={{ patientUuid: visit.patient.uuid, patient, visit, compact: true }}
16
+ />
17
+ );
18
+ };
19
+
20
+ export default VisitActionsCell;
@@ -0,0 +1,4 @@
1
+ .visitActions {
2
+ display: flex;
3
+ align-items: center;
4
+ }
@@ -0,0 +1,19 @@
1
+ import { formatDate, type Visit } from '@openmrs/esm-framework';
2
+ import React from 'react';
3
+ import { useTranslation } from 'react-i18next';
4
+
5
+ interface Props {
6
+ visit: Visit;
7
+ patient: fhir.Patient;
8
+ }
9
+
10
+ const VisitDateCell: React.FC<Props> = ({ visit }) => {
11
+ const { t } = useTranslation();
12
+ const { startDatetime, stopDatetime } = visit;
13
+ const fromDate = formatDate(new Date(startDatetime));
14
+ const toDate = stopDatetime ? formatDate(new Date(stopDatetime)) : null;
15
+
16
+ return <>{toDate ? t('fromDateToDate', '{{fromDate}} - {{toDate}}', { fromDate, toDate }) : fromDate}</>;
17
+ };
18
+
19
+ export default VisitDateCell;
@@ -0,0 +1,31 @@
1
+ import React from 'react';
2
+ import { Tag } from '@carbon/react';
3
+ import { type Visit } from '@openmrs/esm-framework';
4
+ import { DiagnosisTags } from './diagnosis-tags.component';
5
+ import { useTranslation } from 'react-i18next';
6
+ import styles from './visit-diagnoses-cell-with-certainty.module.scss';
7
+
8
+ interface Props {
9
+ visit: Visit;
10
+ patient: fhir.Patient;
11
+ }
12
+
13
+ const VisitDiagnosisCell: React.FC<Props> = ({ visit }) => {
14
+ const { t } = useTranslation();
15
+ const diagnoses = visit.encounters
16
+ .flatMap((enc) => enc.diagnoses)
17
+ .filter((d) => !d.voided)
18
+ .sort((a, b) => a.rank - b.rank);
19
+
20
+ if (!diagnoses.length) {
21
+ return null;
22
+ }
23
+
24
+ return (
25
+ <div className={styles.cell}>
26
+ <DiagnosisTags diagnoses={diagnoses} />
27
+ </div>
28
+ );
29
+ };
30
+
31
+ export default VisitDiagnosisCell;
@@ -0,0 +1,16 @@
1
+ .cell {
2
+ display: flex;
3
+ flex-direction: column;
4
+ gap: 0.25rem;
5
+ }
6
+
7
+ .certaintyRow {
8
+ display: flex;
9
+ flex-wrap: wrap;
10
+ gap: 0.25rem;
11
+ }
12
+
13
+ .certaintyTag {
14
+ font-size: 0.625rem;
15
+ padding: 0 0.375rem;
16
+ }
@@ -0,0 +1,144 @@
1
+ import React, { useState } from 'react';
2
+ import { useTranslation } from 'react-i18next';
3
+ import {
4
+ DataTable,
5
+ DataTableSkeleton,
6
+ Pagination,
7
+ Table,
8
+ TableBody,
9
+ TableCell,
10
+ TableContainer,
11
+ TableExpandedRow,
12
+ TableExpandHeader,
13
+ TableExpandRow,
14
+ TableHead,
15
+ TableHeader,
16
+ TableRow,
17
+ } from '@carbon/react';
18
+ import { ErrorState, isDesktop, useLayoutType } from '@openmrs/esm-framework';
19
+ import { EmptyState } from '@openmrs/esm-patient-common-lib';
20
+ import { usePaginatedVisits } from '../visits-widget/visit.resource';
21
+ import VisitActionsCell from './visit-actions-cell.component';
22
+ import VisitDateCell from './visit-date-cell.component';
23
+ import VisitDiagnosisCell from './visit-diagnoses-cell-with-certainty.component';
24
+ import VisitSummary from '../visits-widget/past-visits-components/visit-summary.component';
25
+ import VisitTypeCell from './visit-type-cell.component';
26
+ import styles from './visit-history-table.scss';
27
+
28
+ interface VisitHistoryTableProps {
29
+ patientUuid: string;
30
+ patient: fhir.Patient;
31
+ }
32
+
33
+ /**
34
+ * This show a list of visit histories in the visit tab in patient chart
35
+ */
36
+ const VisitHistoryTable: React.FC<VisitHistoryTableProps> = ({ patientUuid, patient }) => {
37
+ const defaultPageSize = 10;
38
+ const [pageSize, setPageSize] = useState(defaultPageSize);
39
+ const pageSizes = [10, 20, 30, 40, 50];
40
+
41
+ const { data: visits, currentPage, error, isLoading, totalCount, goTo } = usePaginatedVisits(patientUuid, pageSize);
42
+ const { t } = useTranslation();
43
+ const desktopLayout = isDesktop(useLayoutType());
44
+
45
+ // TODO: make this configurable
46
+ const columns = [
47
+ { key: 'visitDate', header: t('date', 'Date'), CellComponent: VisitDateCell },
48
+ { key: 'visitType', header: t('visitType', 'Visit type'), CellComponent: VisitTypeCell },
49
+ { key: 'diagnoses', header: t('diagnoses', 'Diagnoses'), CellComponent: VisitDiagnosisCell },
50
+ { key: 'actions', header: '', CellComponent: VisitActionsCell },
51
+ ];
52
+
53
+ const layout = useLayoutType();
54
+
55
+ const rowData = visits?.map((visit) => {
56
+ const row: Record<string, JSX.Element | string> = { id: visit.uuid };
57
+ for (const { key, CellComponent } of columns) {
58
+ row[key] = <CellComponent key={key} visit={visit} patient={patient} />;
59
+ }
60
+ return row;
61
+ });
62
+
63
+ if (isLoading) {
64
+ return <DataTableSkeleton role="progressbar" compact={isDesktop(layout)} zebra />;
65
+ }
66
+
67
+ if (error) {
68
+ return <ErrorState error={error} headerTitle={t('pastVisits', 'Past visits')} />;
69
+ }
70
+
71
+ if (visits.length === 0) {
72
+ return (
73
+ <div className={styles.emptyStateContainer}>
74
+ <EmptyState headerTitle={t('pastVisits', 'Past visits')} displayText={t('visits', 'visits')} />
75
+ </div>
76
+ );
77
+ }
78
+ return (
79
+ <div className={styles.container}>
80
+ {/* @ts-ignore */}
81
+ <DataTable headers={columns} rows={rowData} size={desktopLayout ? 'sm' : 'lg'} useZebraStyles>
82
+ {({ rows, headers, getTableProps, getHeaderProps, getExpandHeaderProps, getRowProps, getExpandedRowProps }) => (
83
+ <>
84
+ <TableContainer>
85
+ <Table {...getTableProps()}>
86
+ <TableHead>
87
+ <TableRow>
88
+ <TableExpandHeader enableToggle {...getExpandHeaderProps()} />
89
+ {headers.map((header) => (
90
+ <TableHeader
91
+ {...getHeaderProps({
92
+ header,
93
+ className: header.key === 'actions' ? styles.actionsColumn : '',
94
+ })}>
95
+ {header.header}
96
+ </TableHeader>
97
+ ))}
98
+ </TableRow>
99
+ </TableHead>
100
+ <TableBody>
101
+ {rows.map((row, i) => {
102
+ const visit = visits[i];
103
+ return (
104
+ <React.Fragment key={row.id}>
105
+ <TableExpandRow {...getRowProps({ row })}>
106
+ {row.cells.map((cell) => {
107
+ return <TableCell key={cell.id}>{cell?.value}</TableCell>;
108
+ })}
109
+ </TableExpandRow>
110
+ {row.isExpanded ? (
111
+ <TableExpandedRow {...getExpandedRowProps({ row })} colSpan={headers.length + 2}>
112
+ <VisitSummary visit={visit} patientUuid={patientUuid} />
113
+ </TableExpandedRow>
114
+ ) : (
115
+ <TableExpandedRow className={styles.hiddenRow} colSpan={headers.length + 2} />
116
+ )}
117
+ </React.Fragment>
118
+ );
119
+ })}
120
+ </TableBody>
121
+ </Table>
122
+ </TableContainer>
123
+ <Pagination
124
+ forwardText={t('nextPage', 'Next page')}
125
+ backwardText={t('previousPage', 'Previous page')}
126
+ page={currentPage}
127
+ pageSize={pageSize}
128
+ pageSizes={pageSizes}
129
+ totalItems={totalCount}
130
+ onChange={({ pageSize, page }) => {
131
+ setPageSize(pageSize);
132
+ if (page !== currentPage) {
133
+ goTo(page);
134
+ }
135
+ }}
136
+ />
137
+ </>
138
+ )}
139
+ </DataTable>
140
+ </div>
141
+ );
142
+ };
143
+
144
+ export default VisitHistoryTable;
@@ -0,0 +1,25 @@
1
+ @use '@carbon/layout';
2
+ @use '@openmrs/esm-styleguide/src/vars' as *;
3
+
4
+ .container {
5
+ margin: layout.$spacing-05 0;
6
+ border: 1px solid $ui-03;
7
+
8
+ // fixes padding of encounters table within the expanded visit table row
9
+ :global(tr.cds--parent-row.cds--expandable-row + tr[data-child-row] td) {
10
+ padding-inline-start: layout.$spacing-05;
11
+ }
12
+ }
13
+
14
+ .emptyStateContainer {
15
+ text-align: center;
16
+ margin: layout.$spacing-05 0;
17
+ }
18
+
19
+ .actionsColumn {
20
+ width: 1% !important; // fixes the width of action column to fit content
21
+ }
22
+
23
+ .hiddenRow {
24
+ display: none;
25
+ }
@@ -0,0 +1,15 @@
1
+ import { type Visit } from '@openmrs/esm-framework';
2
+ import React from 'react';
3
+
4
+ interface Props {
5
+ visit: Visit;
6
+ patient: fhir.Patient;
7
+ }
8
+
9
+ const VisitTypeCell: React.FC<Props> = ({ visit }) => {
10
+ const { visitType } = visit;
11
+
12
+ return <>{visitType.display}</>;
13
+ };
14
+
15
+ export default VisitTypeCell;
@@ -0,0 +1,67 @@
1
+ import React from 'react';
2
+ import { useTranslation } from 'react-i18next';
3
+ import { SkeletonText } from '@carbon/react';
4
+ import { type Obs, useConfig } from '@openmrs/esm-framework';
5
+ import styles from './styles.scss';
6
+
7
+ interface EncounterObservationsProps {
8
+ observations: Array<Obs>;
9
+ }
10
+
11
+ const EncounterObservations: React.FC<EncounterObservationsProps> = ({ observations }) => {
12
+ const { t } = useTranslation();
13
+ const { obsConceptUuidsToHide = [] } = useConfig();
14
+
15
+ function getAnswerFromDisplay(display: string): string {
16
+ const colonIndex = display.indexOf(':');
17
+ if (colonIndex === -1) {
18
+ return '';
19
+ } else {
20
+ return display.substring(colonIndex + 1).trim();
21
+ }
22
+ }
23
+
24
+ const filteredObservations = !!obsConceptUuidsToHide.length
25
+ ? observations?.filter((obs) => {
26
+ return !obsConceptUuidsToHide.includes(obs?.concept?.uuid);
27
+ })
28
+ : observations;
29
+
30
+ if (!filteredObservations || filteredObservations.length == 0) {
31
+ return (
32
+ <div className={styles.observation}>
33
+ <p>{t('noObservationsFound', 'No observations found')}</p>
34
+ </div>
35
+ );
36
+ }
37
+
38
+ return (
39
+ <div className={styles.observation}>
40
+ {filteredObservations?.map((obs, index) => {
41
+ if (obs.groupMembers) {
42
+ return (
43
+ <React.Fragment key={index}>
44
+ <span className={styles.parentConcept}>{obs.concept.display}</span>
45
+ <span />
46
+ {obs.groupMembers.map((member) => (
47
+ <React.Fragment key={index}>
48
+ <span className={styles.childConcept}>{member.concept.display}</span>
49
+ <span>{getAnswerFromDisplay(member.display)}</span>
50
+ </React.Fragment>
51
+ ))}
52
+ </React.Fragment>
53
+ );
54
+ } else {
55
+ return (
56
+ <React.Fragment key={index}>
57
+ <span>{obs.concept.display}</span>
58
+ <span>{getAnswerFromDisplay(obs.display)}</span>
59
+ </React.Fragment>
60
+ );
61
+ }
62
+ })}
63
+ </div>
64
+ );
65
+ };
66
+
67
+ export default EncounterObservations;
@@ -0,0 +1,3 @@
1
+ import EncounterObservations from './encounter-observations.component';
2
+
3
+ export default EncounterObservations;