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

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 (89) hide show
  1. package/.turbo/turbo-build.log +14 -0
  2. package/README.md +1 -0
  3. package/dist/152.js +1 -0
  4. package/dist/152.js.map +1 -0
  5. package/dist/159.js +1 -0
  6. package/dist/159.js.map +1 -0
  7. package/dist/208.js +1 -0
  8. package/dist/208.js.map +1 -0
  9. package/dist/209.js +1 -0
  10. package/dist/209.js.map +1 -0
  11. package/dist/363.js +1 -0
  12. package/dist/363.js.map +1 -0
  13. package/dist/410.js +1 -0
  14. package/dist/410.js.map +1 -0
  15. package/dist/442.js +1 -0
  16. package/dist/442.js.map +1 -0
  17. package/dist/466.js +1 -0
  18. package/dist/466.js.map +1 -0
  19. package/dist/484.js +11 -0
  20. package/dist/484.js.map +1 -0
  21. package/dist/540.js +1 -0
  22. package/dist/540.js.map +1 -0
  23. package/dist/545.js +43 -0
  24. package/dist/545.js.map +1 -0
  25. package/dist/61.js +1 -0
  26. package/dist/61.js.map +1 -0
  27. package/dist/677.js +1 -0
  28. package/dist/677.js.map +1 -0
  29. package/dist/689.js +1 -0
  30. package/dist/689.js.map +1 -0
  31. package/dist/697.js +1 -0
  32. package/dist/697.js.map +1 -0
  33. package/dist/712.js +1 -0
  34. package/dist/712.js.map +1 -0
  35. package/dist/771.js +1 -0
  36. package/dist/771.js.map +1 -0
  37. package/dist/789.js +1 -0
  38. package/dist/789.js.map +1 -0
  39. package/dist/ethiopia-esm-clinical-workflow-app.js +6 -0
  40. package/dist/ethiopia-esm-clinical-workflow-app.js.buildmanifest.json +579 -0
  41. package/dist/ethiopia-esm-clinical-workflow-app.js.map +1 -0
  42. package/dist/main.js +16 -0
  43. package/dist/main.js.map +1 -0
  44. package/dist/routes.json +1 -0
  45. package/jest.config.js +3 -0
  46. package/package.json +59 -0
  47. package/rspack.config.js +1 -0
  48. package/src/config-schema.ts +69 -0
  49. package/src/constants.ts +2 -0
  50. package/src/createDashboardLink.tsx +10 -0
  51. package/src/dashboard.meta.ts +6 -0
  52. package/src/declarations.d.ts +3 -0
  53. package/src/helper.ts +115 -0
  54. package/src/index.ts +51 -0
  55. package/src/mru/billing-information/billing-information.resource.ts +139 -0
  56. package/src/mru/billing-information/billing-information.scss +55 -0
  57. package/src/mru/billing-information/billing-information.workspace.tsx +371 -0
  58. package/src/mru/dashboard.component.tsx +18 -0
  59. package/src/mru/mru.component.tsx +106 -0
  60. package/src/mru/mru.scss +28 -0
  61. package/src/patient-registration/patient-registration.resource.tsx +129 -0
  62. package/src/patient-registration/patient.registration.workspace.scss +47 -0
  63. package/src/patient-registration/patient.registration.workspace.tsx +443 -0
  64. package/src/patient-registration/useGenerateIdentifier.ts +26 -0
  65. package/src/patient-scoreboard/appointment-cards/checked-in-appointments.card.tsx +18 -0
  66. package/src/patient-scoreboard/appointment-cards/not-arrived-appointments.card.tsx +18 -0
  67. package/src/patient-scoreboard/appointment-cards/total-appointments.card.tsx +18 -0
  68. package/src/patient-scoreboard/hooks/useAppointmentList.ts +61 -0
  69. package/src/patient-scoreboard/hooks/useVisitList.ts +104 -0
  70. package/src/patient-scoreboard/metrics-card/metrics-card.component.scss +84 -0
  71. package/src/patient-scoreboard/metrics-card/metrics-card.component.tsx +40 -0
  72. package/src/patient-scoreboard/patient-scoreboard.component.scss +47 -0
  73. package/src/patient-scoreboard/patient-scoreboard.component.tsx +70 -0
  74. package/src/patient-scoreboard/visit-cards/active-visits.card.tsx +18 -0
  75. package/src/patient-scoreboard/visit-cards/scheduled-visits.card.tsx +18 -0
  76. package/src/patient-scoreboard/visit-cards/total-visits.card.tsx +18 -0
  77. package/src/patient-scoreboard/visits-table/visits-table.component.scss +31 -0
  78. package/src/patient-scoreboard/visits-table/visits-table.component.tsx +181 -0
  79. package/src/root.component.tsx +20 -0
  80. package/src/root.scss +10 -0
  81. package/src/routes.json +108 -0
  82. package/src/triage/patient-banner.component.tsx +59 -0
  83. package/src/triage/patient-banner.scss +14 -0
  84. package/src/triage/triage-dashboard.component.tsx +116 -0
  85. package/src/triage/triage-dashboard.scss +107 -0
  86. package/src/triage/triage.resource.tsx +44 -0
  87. package/src/triage/useStartVisitAndLaunchTriageForm.ts +156 -0
  88. package/src/types/index.ts +0 -0
  89. package/tsconfig.json +4 -0
@@ -0,0 +1,59 @@
1
+ import React from 'react';
2
+ import { ExtensionSlot, usePatient, useVisit } from '@openmrs/esm-framework';
3
+ import { Button, InlineLoading } from '@carbon/react';
4
+ import { useTranslation } from 'react-i18next';
5
+ import { Close, Stethoscope } from '@carbon/react/icons';
6
+ import { useStartVisitAndLaunchTriageForm, launchTriageFormWorkspace } from './useStartVisitAndLaunchTriageForm';
7
+
8
+ import styles from './patient-banner.scss';
9
+
10
+ type PatientBannerProps = {
11
+ patientUuid: string;
12
+ formUuid: string;
13
+ formName: string;
14
+ setPatientUuid: (patientUuid: string | undefined) => void;
15
+ };
16
+
17
+ const PatientBanner: React.FC<PatientBannerProps> = ({ patientUuid, formUuid, formName, setPatientUuid }) => {
18
+ const { t } = useTranslation();
19
+ const { isLoading: isVisitLoading, activeVisit } = useVisit(patientUuid);
20
+ const { handleStartVisitAndLaunchTriageForm } = useStartVisitAndLaunchTriageForm();
21
+ const { isLoading, error, patient } = usePatient(patientUuid);
22
+
23
+ const handleLaunchTriageForm = () => {
24
+ if (activeVisit) {
25
+ launchTriageFormWorkspace(patient, patientUuid, activeVisit, formUuid, formName, t);
26
+ } else {
27
+ handleStartVisitAndLaunchTriageForm(patientUuid, formUuid, formName);
28
+ }
29
+ };
30
+
31
+ if (isLoading || isVisitLoading) {
32
+ return <InlineLoading description={t('loading', 'Loading...')} />;
33
+ }
34
+
35
+ return (
36
+ <div className={styles.patientBannerContainer}>
37
+ <div className={styles.patientBannerHeader}>
38
+ <Button kind="ghost" renderIcon={Stethoscope} onClick={() => handleLaunchTriageForm()}>
39
+ {t('triageForm', 'Triage form')}
40
+ </Button>
41
+ <Button kind="danger--ghost" renderIcon={Close} onClick={() => setPatientUuid(undefined)}>
42
+ {t('close', 'Close')}
43
+ </Button>
44
+ </div>
45
+ {patient && (
46
+ <ExtensionSlot
47
+ name="patient-header-slot"
48
+ state={{
49
+ patient,
50
+ patientUuid: patientUuid,
51
+ hideActionsOverflow: true,
52
+ }}
53
+ />
54
+ )}
55
+ </div>
56
+ );
57
+ };
58
+
59
+ export default PatientBanner;
@@ -0,0 +1,14 @@
1
+ @use '@carbon/layout';
2
+ @use '@carbon/colors';
3
+
4
+ .patientBannerContainer {
5
+ margin: layout.$spacing-05;
6
+ border: 1px solid colors.$gray-20;
7
+ background-color: colors.$white;
8
+ }
9
+
10
+ .patientBannerHeader {
11
+ display: flex;
12
+ justify-content: flex-end;
13
+ align-items: center;
14
+ }
@@ -0,0 +1,116 @@
1
+ import React, { useState } from 'react';
2
+ import { useTranslation } from 'react-i18next';
3
+ import { Button, InlineNotification, Tile } from '@carbon/react';
4
+ import { Add, Search } from '@carbon/react/icons';
5
+ import {
6
+ ExtensionSlot,
7
+ TriagePictogram,
8
+ launchWorkspace,
9
+ PageHeader,
10
+ useConfig,
11
+ useSession,
12
+ } from '@openmrs/esm-framework';
13
+
14
+ import styles from './triage-dashboard.scss';
15
+ import type { ClinicalWorkflowConfig } from '../config-schema';
16
+ import PatientBanner from './patient-banner.component';
17
+ import { getTriageFormForLocation } from './triage.resource';
18
+
19
+ const TriageDashboard: React.FC = () => {
20
+ const { t } = useTranslation();
21
+ const { sessionLocation } = useSession();
22
+ const { triageLocationForms } = useConfig<ClinicalWorkflowConfig>();
23
+ const [patientUuid, setPatientUuid] = useState<string | null>(null);
24
+
25
+ const triageFormConfig = getTriageFormForLocation(sessionLocation?.uuid, triageLocationForms);
26
+
27
+ if (!sessionLocation) {
28
+ return (
29
+ <div className={styles.triageDashboardContainer}>
30
+ <PageHeader
31
+ className={styles.pageHeader}
32
+ title={t('triageDashboard', 'Triage Dashboard')}
33
+ illustration={<TriagePictogram />}
34
+ />
35
+ <InlineNotification
36
+ kind="error"
37
+ title={t('noSessionLocation', 'No session location')}
38
+ subtitle={t('noSessionLocationSubtitle', 'Please select a location to continue')}
39
+ lowContrast
40
+ />
41
+ </div>
42
+ );
43
+ }
44
+
45
+ if (!triageFormConfig) {
46
+ return (
47
+ <div className={styles.triageDashboardContainer}>
48
+ <PageHeader
49
+ className={styles.pageHeader}
50
+ title={t('triageDashboard', 'Triage Dashboard')}
51
+ illustration={<TriagePictogram />}
52
+ />
53
+ <InlineNotification
54
+ kind="warning"
55
+ title={t('noTriageFormConfigured', 'No triage form configured')}
56
+ subtitle={t('noTriageFormConfiguredSubtitle', 'No triage form is configured for location: {{location}}', {
57
+ location: sessionLocation.display,
58
+ })}
59
+ lowContrast
60
+ />
61
+ </div>
62
+ );
63
+ }
64
+
65
+ return (
66
+ <div className={styles.triageDashboardContainer}>
67
+ <PageHeader
68
+ className={styles.pageHeader}
69
+ title={t('triageDashboard', 'Triage Dashboard')}
70
+ illustration={<TriagePictogram />}
71
+ />
72
+ <div className={styles.headerActions}>
73
+ <ExtensionSlot
74
+ className={styles.patientSearchBar}
75
+ name="patient-search-bar-slot"
76
+ state={{
77
+ selectPatientAction: (patientUuid: string) => setPatientUuid(patientUuid),
78
+ buttonProps: {
79
+ kind: 'secondary',
80
+ },
81
+ }}
82
+ />
83
+ <Button onClick={() => launchWorkspace('patient-registration-workspace')} kind="tertiary" renderIcon={Add}>
84
+ {t('registerNewPatient', 'Register New Patient')}
85
+ </Button>
86
+ </div>
87
+ {!patientUuid ? (
88
+ <div className={styles.emptyStateContainer}>
89
+ <Tile className={styles.emptyStateTile}>
90
+ <div className={styles.emptyStateContent}>
91
+ <div className={styles.emptyStateIcon}>
92
+ <Search size={48} />
93
+ </div>
94
+ <h3 className={styles.emptyStateHeading}>{t('noPatientSelected', 'No patient selected')}</h3>
95
+ <p className={styles.emptyStateDescription}>
96
+ {t(
97
+ 'searchForPatientToStartTriage',
98
+ 'Search for a patient using the search bar above to start the triage process, or register a new patient.',
99
+ )}
100
+ </p>
101
+ </div>
102
+ </Tile>
103
+ </div>
104
+ ) : (
105
+ <PatientBanner
106
+ patientUuid={patientUuid}
107
+ formUuid={triageFormConfig.formUuid}
108
+ formName={triageFormConfig.name}
109
+ setPatientUuid={setPatientUuid}
110
+ />
111
+ )}
112
+ </div>
113
+ );
114
+ };
115
+
116
+ export default TriageDashboard;
@@ -0,0 +1,107 @@
1
+ @use '@carbon/layout';
2
+ @use '@carbon/colors';
3
+
4
+ .contentSwitcher {
5
+ margin: layout.$spacing-05;
6
+ }
7
+
8
+ .headerActions {
9
+ display: flex;
10
+ column-gap: layout.$spacing-05;
11
+ margin: layout.$spacing-05;
12
+ }
13
+
14
+ .patientSearchBar {
15
+ width: 100%;
16
+
17
+ & form {
18
+ border: none;
19
+ }
20
+ }
21
+
22
+ .pageHeader {
23
+ border-bottom: 1px solid colors.$gray-20;
24
+ }
25
+
26
+ .disabled {
27
+ opacity: 0.5;
28
+ pointer-events: none;
29
+
30
+ button {
31
+ opacity: 1;
32
+ pointer-events: auto;
33
+ }
34
+ }
35
+
36
+ .emptyStateContainer {
37
+ margin: layout.$spacing-05;
38
+ display: flex;
39
+ justify-content: center;
40
+ align-items: center;
41
+ min-height: 400px;
42
+ }
43
+
44
+ .emptyStateTile {
45
+ width: 100%;
46
+ padding: layout.$spacing-07;
47
+ text-align: center;
48
+ }
49
+
50
+ .emptyStateContent {
51
+ display: flex;
52
+ flex-direction: column;
53
+ align-items: center;
54
+ gap: layout.$spacing-05;
55
+ }
56
+
57
+ .emptyStateIcon {
58
+ color: colors.$gray-50;
59
+ margin-bottom: layout.$spacing-03;
60
+
61
+ :global(svg) {
62
+ display: block;
63
+ }
64
+ }
65
+
66
+ .emptyStateHeading {
67
+ font-size: 1.25rem;
68
+ font-weight: 600;
69
+ margin: 0;
70
+ color: colors.$gray-100;
71
+ }
72
+
73
+ .emptyStateDescription {
74
+ font-size: 0.875rem;
75
+ color: colors.$gray-70;
76
+ margin: 0;
77
+ line-height: 1.5;
78
+ max-width: 500px;
79
+ }
80
+
81
+ .emptyStateActions {
82
+ margin-top: layout.$spacing-05;
83
+ display: flex;
84
+ flex-direction: column;
85
+ gap: layout.$spacing-03;
86
+ text-align: left;
87
+ width: 100%;
88
+ max-width: 500px;
89
+ }
90
+
91
+ .actionItem {
92
+ display: flex;
93
+ align-items: flex-start;
94
+ gap: layout.$spacing-03;
95
+ font-size: 0.875rem;
96
+ color: colors.$gray-70;
97
+ }
98
+
99
+ .actionIcon {
100
+ flex-shrink: 0;
101
+ color: colors.$blue-60;
102
+ margin-top: 2px;
103
+ }
104
+
105
+ .actionLabel {
106
+ line-height: 1.5;
107
+ }
@@ -0,0 +1,44 @@
1
+ import { openmrsFetch, restBaseUrl, Visit } from '@openmrs/esm-framework';
2
+ import type { ClinicalWorkflowConfig } from '../config-schema';
3
+
4
+ const queueEntryCustomRepresentation =
5
+ 'custom:(uuid,display,queue,status,patient:(uuid,display,person,identifiers:(uuid,display,identifier,identifierType)),visit:(uuid,display,startDatetime,encounters:(uuid,display,diagnoses,encounterDatetime,encounterType,obs,encounterProviders,voided),attributes:(uuid,display,value,attributeType)),priority,priorityComment,sortWeight,startedAt,endedAt,locationWaitingFor,queueComingFrom,providerWaitingFor,previousQueueEntry)';
6
+
7
+ export const createVisitForPatient = async (patientUuid: string, visitTypeUuid: string) => {
8
+ const url = `${restBaseUrl}/visit?v=full`;
9
+ const payload = {
10
+ patient: patientUuid,
11
+ visitType: visitTypeUuid,
12
+ };
13
+
14
+ return openmrsFetch<Visit>(url, {
15
+ method: 'POST',
16
+ headers: {
17
+ 'Content-Type': 'application/json',
18
+ },
19
+ body: JSON.stringify(payload),
20
+ });
21
+ };
22
+
23
+ export const getCurrentVisitForPatient = async (patientUuid: string): Promise<Visit | undefined> => {
24
+ const url = `${restBaseUrl}/visit?v=full&patient=${patientUuid}&includeInactive=false`;
25
+ const { data } = await openmrsFetch<{ results: Array<Visit> }>(url);
26
+ const currentVisit = data.results?.find((visit) => visit.stopDatetime === null);
27
+ return currentVisit;
28
+ };
29
+
30
+ export const fetchQueueEntryForPatient = async (patientUuid: string): Promise<any | undefined> => {
31
+ const url = `${restBaseUrl}/queue-entry?v=${queueEntryCustomRepresentation}&patient=${patientUuid}&includeInactive=false`;
32
+ const { data } = await openmrsFetch<{ results: Array<unknown> }>(url);
33
+ return data.results[0];
34
+ };
35
+
36
+ export const getTriageFormForLocation = (
37
+ locationUuid: string | undefined,
38
+ triageLocationForms: ClinicalWorkflowConfig['triageLocationForms'],
39
+ ): { formUuid: string; name: string } | undefined => {
40
+ if (!locationUuid) {
41
+ return undefined;
42
+ }
43
+ return triageLocationForms[locationUuid];
44
+ };
@@ -0,0 +1,156 @@
1
+ import { useCallback, useState } from 'react';
2
+ import { useTranslation } from 'react-i18next';
3
+ import type { TFunction } from 'i18next';
4
+ import {
5
+ Encounter,
6
+ fetchCurrentPatient,
7
+ launchWorkspace2,
8
+ showModal,
9
+ showSnackbar,
10
+ useConfig,
11
+ Visit,
12
+ } from '@openmrs/esm-framework';
13
+
14
+ import { createVisitForPatient, getCurrentVisitForPatient } from './triage.resource';
15
+ import type { ClinicalWorkflowConfig } from '../config-schema';
16
+
17
+ export const launchTriageFormWorkspace = (
18
+ patient: Awaited<ReturnType<typeof fetchCurrentPatient>>,
19
+ patientUuid: string,
20
+ visit: Visit,
21
+ formUuid: string,
22
+ formName: string,
23
+ t: TFunction<'translation', undefined>,
24
+ ) => {
25
+ const handleShowModal = (encounter: Encounter) => {
26
+ const dispose = showModal('transition-patient-to-latest-queue-modal', {
27
+ activeVisit: visit,
28
+ closeModal: () => dispose(),
29
+ });
30
+ };
31
+
32
+ if (!visit?.uuid || !visit?.visitType?.uuid) {
33
+ throw new Error('Invalid visit data received');
34
+ }
35
+
36
+ // Launch triage form workspace
37
+ launchWorkspace2(
38
+ 'clinical-workflow-patient-form-entry-workspace',
39
+ {
40
+ formEntryWorkspaceName: formName,
41
+ patient,
42
+ visitContext: visit,
43
+ form: {
44
+ visitUuid: visit.uuid,
45
+ uuid: formUuid,
46
+ visitTypeUuid: visit.visitType.uuid,
47
+ },
48
+ encounterUuid: '',
49
+ handlePostResponse: handleShowModal,
50
+ },
51
+ {
52
+ patientUuid: patientUuid,
53
+ patient: patient,
54
+ visitContext: visit,
55
+ },
56
+ );
57
+
58
+ // Set z-index for workspace container
59
+ setTimeout(() => {
60
+ const workspaceContainer = document.getElementById('omrs-workspaces-container');
61
+ if (workspaceContainer) {
62
+ workspaceContainer.style.zIndex = '100';
63
+ }
64
+ }, 0);
65
+ };
66
+
67
+ interface UseStartVisitAndLaunchTriageFormReturn {
68
+ handleStartVisitAndLaunchTriageForm: (patientUuid: string, formUuid: string, formName: string) => Promise<void>;
69
+ isLoading: boolean;
70
+ error: Error | null;
71
+ }
72
+
73
+ export const useStartVisitAndLaunchTriageForm = (): UseStartVisitAndLaunchTriageFormReturn => {
74
+ const { t } = useTranslation();
75
+ const { visitTypeUuid } = useConfig<ClinicalWorkflowConfig>();
76
+ const [isLoading, setIsLoading] = useState(false);
77
+ const [error, setError] = useState<Error | null>(null);
78
+
79
+ const handleStartVisitAndLaunchTriageForm = useCallback(
80
+ async (patientUuid: string, formUuid: string, formName: string) => {
81
+ if (!patientUuid?.trim()) {
82
+ const validationError = new Error('Patient UUID is required');
83
+ setError(validationError);
84
+ showSnackbar({
85
+ title: t('triageDashboardError', 'Error'),
86
+ kind: 'error',
87
+ subtitle: t('triageDashboardInvalidPatientUuid', 'Invalid patient identifier'),
88
+ isLowContrast: true,
89
+ });
90
+ return;
91
+ }
92
+
93
+ if (!formUuid?.trim()) {
94
+ const validationError = new Error('Form UUID is required');
95
+ setError(validationError);
96
+ showSnackbar({
97
+ title: t('triageDashboardError', 'Error'),
98
+ kind: 'error',
99
+ subtitle: t('triageDashboardInvalidFormUuid', 'Invalid form identifier'),
100
+ isLowContrast: true,
101
+ });
102
+ return;
103
+ }
104
+
105
+ setIsLoading(true);
106
+ setError(null);
107
+
108
+ try {
109
+ // Fetch patient data
110
+ const patient = await fetchCurrentPatient(patientUuid);
111
+
112
+ if (!patient) {
113
+ throw new Error('Failed to fetch patient data');
114
+ }
115
+
116
+ let visit = await getCurrentVisitForPatient(patientUuid);
117
+ if (!visit) {
118
+ const visitResponse = await createVisitForPatient(patientUuid, visitTypeUuid);
119
+
120
+ if (!visitResponse.ok) {
121
+ throw new Error(
122
+ visitResponse.data?.error?.message ||
123
+ t('triageDashboardErrorStartingVisit', 'Error starting visit for patient'),
124
+ );
125
+ }
126
+
127
+ visit = visitResponse.data;
128
+ }
129
+
130
+ // Launch triage form workspace with visit
131
+ launchTriageFormWorkspace(patient, patientUuid, visit, formUuid, formName, t);
132
+ } catch (err) {
133
+ const errorMessage =
134
+ err instanceof Error ? err.message : t('triageDashboardUnexpectedError', 'An unexpected error occurred');
135
+
136
+ setError(err instanceof Error ? err : new Error(errorMessage));
137
+
138
+ showSnackbar({
139
+ title: t('triageDashboardErrorStartingVisit', 'Error starting visit for patient'),
140
+ kind: 'error',
141
+ subtitle: errorMessage,
142
+ isLowContrast: true,
143
+ });
144
+ } finally {
145
+ setIsLoading(false);
146
+ }
147
+ },
148
+ [t, visitTypeUuid],
149
+ );
150
+
151
+ return {
152
+ handleStartVisitAndLaunchTriageForm,
153
+ isLoading,
154
+ error,
155
+ };
156
+ };
File without changes
package/tsconfig.json ADDED
@@ -0,0 +1,4 @@
1
+ {
2
+ "extends": "../../tsconfig.json",
3
+ "include": ["src/**/*"],
4
+ }