@ampath/esm-dha-workflow-app 4.0.0-next.10

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 (112) hide show
  1. package/.editorconfig +12 -0
  2. package/.eslintignore +2 -0
  3. package/.eslintrc +57 -0
  4. package/.husky/pre-commit +7 -0
  5. package/.husky/pre-push +6 -0
  6. package/.prettierignore +14 -0
  7. package/.turbo.json +18 -0
  8. package/.yarn/plugins/@yarnpkg/plugin-outdated.cjs +35 -0
  9. package/LICENSE +401 -0
  10. package/README.md +37 -0
  11. package/__mocks__/react-i18next.js +50 -0
  12. package/dist/161.js +1 -0
  13. package/dist/161.js.map +1 -0
  14. package/dist/184.js +2 -0
  15. package/dist/184.js.LICENSE.txt +14 -0
  16. package/dist/184.js.map +1 -0
  17. package/dist/197.js +1 -0
  18. package/dist/198.js +2 -0
  19. package/dist/198.js.LICENSE.txt +24 -0
  20. package/dist/198.js.map +1 -0
  21. package/dist/255.js +1 -0
  22. package/dist/255.js.map +1 -0
  23. package/dist/282.js +2 -0
  24. package/dist/282.js.LICENSE.txt +32 -0
  25. package/dist/282.js.map +1 -0
  26. package/dist/300.js +1 -0
  27. package/dist/335.js +1 -0
  28. package/dist/353.js +1 -0
  29. package/dist/353.js.map +1 -0
  30. package/dist/420.js +1 -0
  31. package/dist/420.js.map +1 -0
  32. package/dist/540.js +2 -0
  33. package/dist/540.js.LICENSE.txt +9 -0
  34. package/dist/540.js.map +1 -0
  35. package/dist/55.js +1 -0
  36. package/dist/652.js +1 -0
  37. package/dist/860.js +1 -0
  38. package/dist/860.js.map +1 -0
  39. package/dist/91.js +1 -0
  40. package/dist/91.js.map +1 -0
  41. package/dist/916.js +2 -0
  42. package/dist/916.js.LICENSE.txt +39 -0
  43. package/dist/916.js.map +1 -0
  44. package/dist/93.js +2 -0
  45. package/dist/93.js.LICENSE.txt +5 -0
  46. package/dist/93.js.map +1 -0
  47. package/dist/961.js +2 -0
  48. package/dist/961.js.LICENSE.txt +19 -0
  49. package/dist/961.js.map +1 -0
  50. package/dist/99.js +1 -0
  51. package/dist/main.js +1 -0
  52. package/dist/main.js.map +1 -0
  53. package/dist/openmrs-esm-home-app.js +1 -0
  54. package/dist/openmrs-esm-home-app.js.buildmanifest.json +626 -0
  55. package/dist/openmrs-esm-home-app.js.map +1 -0
  56. package/dist/routes.json +1 -0
  57. package/e2e/README.md +115 -0
  58. package/e2e/core/global-setup.ts +32 -0
  59. package/e2e/core/index.ts +1 -0
  60. package/e2e/core/test.ts +20 -0
  61. package/e2e/fixtures/api.ts +26 -0
  62. package/e2e/fixtures/index.ts +1 -0
  63. package/e2e/pages/index.ts +1 -0
  64. package/e2e/pages/root-page.ts +32 -0
  65. package/e2e/specs/template-app.spec.ts +23 -0
  66. package/e2e/support/github/Dockerfile +34 -0
  67. package/e2e/support/github/docker-compose.yml +24 -0
  68. package/e2e/support/github/run-e2e-docker-env.sh +37 -0
  69. package/example.env +6 -0
  70. package/jest.config.js +33 -0
  71. package/package.json +111 -0
  72. package/playwright.config.ts +32 -0
  73. package/prettier.config.js +8 -0
  74. package/src/config-schema.ts +43 -0
  75. package/src/consultation/consultation.tsx +7 -0
  76. package/src/declarations.d.ts +5 -0
  77. package/src/index.ts +51 -0
  78. package/src/left-panel/left-panel.component.tsx +22 -0
  79. package/src/left-panel/left-panel.scss +42 -0
  80. package/src/registry/client-details/client-details.tsx +40 -0
  81. package/src/registry/modal/client-details-modal/client-details-modal.scss +28 -0
  82. package/src/registry/modal/client-details-modal/client-details-modal.tsx +75 -0
  83. package/src/registry/modal/otp-verification-modal/otp-verification-modal.scss +13 -0
  84. package/src/registry/modal/otp-verification-modal/otp-verification-modal.tsx +176 -0
  85. package/src/registry/payment-details/payment-options/payment-options.tsx +21 -0
  86. package/src/registry/registry.component.scss +63 -0
  87. package/src/registry/registry.component.tsx +269 -0
  88. package/src/registry/registry.resource.ts +58 -0
  89. package/src/registry/types/index.ts +160 -0
  90. package/src/registry/utils/hie-adapter.ts +56 -0
  91. package/src/registry/utils/mask-data.ts +21 -0
  92. package/src/resources/resources.component.tsx +56 -0
  93. package/src/resources/resources.scss +68 -0
  94. package/src/root.component.tsx +25 -0
  95. package/src/root.scss +11 -0
  96. package/src/root.test.tsx +51 -0
  97. package/src/routes.json +28 -0
  98. package/src/side-nav/side-menu.scss +38 -0
  99. package/src/side-nav-menu/nav-link-config.ts +42 -0
  100. package/src/side-nav-menu/nav-links.tsx +24 -0
  101. package/src/widgets/workflow-registry-link.extension.tsx +12 -0
  102. package/tools/i18next-parser.config.js +89 -0
  103. package/tools/setup-tests.ts +1 -0
  104. package/tools/update-openmrs-deps.mjs +43 -0
  105. package/translations/am.json +24 -0
  106. package/translations/en.json +24 -0
  107. package/translations/es.json +24 -0
  108. package/translations/fr.json +24 -0
  109. package/translations/he.json +24 -0
  110. package/translations/km.json +24 -0
  111. package/tsconfig.json +24 -0
  112. package/webpack.config.js +1 -0
@@ -0,0 +1,22 @@
1
+ import React from 'react';
2
+ import { useTranslation } from 'react-i18next';
3
+ import { SideNav } from '@carbon/react';
4
+ import { attach, ExtensionSlot, isDesktop, useLayoutType } from '@openmrs/esm-framework';
5
+ import styles from './left-panel.scss';
6
+
7
+ attach('nav-menu-slot', 'bed-management-left-panel');
8
+
9
+ const LeftPanel: React.FC = () => {
10
+ const { t } = useTranslation();
11
+ const layout = useLayoutType();
12
+
13
+ return (
14
+ isDesktop(layout) && (
15
+ <SideNav className={styles.leftPanel} expanded>
16
+ <ExtensionSlot name="dha-workflow-slot" />
17
+ </SideNav>
18
+ )
19
+ );
20
+ };
21
+
22
+ export default LeftPanel;
@@ -0,0 +1,42 @@
1
+ @use '@carbon/colors';
2
+ @use '@carbon/layout';
3
+ @use '@carbon/type';
4
+
5
+ .leftPanel {
6
+ border-right: 1px solid colors.$gray-20;
7
+ padding-top: layout.$spacing-05;
8
+
9
+ // Left nav menu item
10
+ :global(.cds--side-nav__link) {
11
+ color: colors.$gray-70;
12
+ @include type.type-style('heading-compact-01');
13
+
14
+ &:focus,
15
+ &:hover {
16
+ background-color: colors.$gray-10-hover;
17
+ }
18
+
19
+ // Active menu item
20
+ &:global(.active-left-nav-link) {
21
+ color: colors.$gray-100;
22
+ border-left: layout.$spacing-02 solid var(--brand-01);
23
+ outline: none;
24
+ padding: 0 layout.$spacing-04;
25
+ background-color: colors.$gray-20;
26
+ }
27
+ }
28
+ }
29
+
30
+ /* Desktop */
31
+ :global(.omrs-breakpoint-gt-tablet) {
32
+ :global(.cds--side-nav__link) {
33
+ height: layout.$spacing-07;
34
+ }
35
+ }
36
+
37
+ /* Tablet */
38
+ :global(.omrs-breakpoint-lt-desktop) {
39
+ :global(.cds--side-nav__link) {
40
+ height: layout.$spacing-09;
41
+ }
42
+ }
@@ -0,0 +1,40 @@
1
+ import React, { useMemo } from 'react';
2
+ import { type HieClient } from '../types';
3
+ import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@carbon/react';
4
+ import { generateHieClientDetails } from '../utils/hie-adapter';
5
+ interface ClientDetailsProps {
6
+ client: HieClient;
7
+ }
8
+ export const ClientDetails: React.FC<ClientDetailsProps> = ({ client }) => {
9
+ const clientDetails = useMemo(() => generateHieClientDetails(client), [client]);
10
+
11
+ if (!client || !clientDetails) {
12
+ return (
13
+ <>
14
+ <h4>No patient Data to Display</h4>
15
+ </>
16
+ );
17
+ }
18
+ return (
19
+ <>
20
+ <Table>
21
+ <TableHead>
22
+ <TableRow>
23
+ <TableHeader>Field</TableHeader>
24
+ <TableHeader>Value</TableHeader>
25
+ </TableRow>
26
+ </TableHead>
27
+ <TableBody>
28
+ {Object.keys(clientDetails).map((key) => (
29
+ <TableRow>
30
+ <TableCell>{key}</TableCell>
31
+ <TableCell>{clientDetails[key]}</TableCell>
32
+ </TableRow>
33
+ ))}
34
+ </TableBody>
35
+ </Table>
36
+ </>
37
+ );
38
+ };
39
+
40
+ export default ClientDetails;
@@ -0,0 +1,28 @@
1
+ .clientDetailsLayout{
2
+ display: flex;
3
+ flex-direction: column;
4
+ width: 100%;
5
+ row-gap: 5px;
6
+ }
7
+ .sectionHeader{
8
+ display: flex;
9
+ flex-direction: column;
10
+ row-gap: 5px;
11
+ margin-top: 5px;
12
+ margin-bottom: 10px;
13
+ }
14
+ .sectionContent{
15
+ display: flex;
16
+ flex-direction: column;
17
+ row-gap: 5px;
18
+ }
19
+ .actionSection{
20
+ display: flex;
21
+ flex-direction: row;
22
+ width: 100%;
23
+ column-gap: 5px;
24
+ }
25
+ .btnContainer{
26
+ margin-top: 2px;
27
+ margin-bottom: 2px;
28
+ }
@@ -0,0 +1,75 @@
1
+ import { Button, Modal, ModalBody, Tab, TabList, TabPanel, TabPanels, Tabs } from '@carbon/react';
2
+ import { type HieClient } from '../../types';
3
+ import React from 'react';
4
+ import styles from './client-details-modal.scss';
5
+ import ClientDetails from '../../client-details/client-details';
6
+ import PaymentOptionsComponent from '../../payment-details/payment-options/payment-options';
7
+
8
+ interface ClientDetailsModalProps {
9
+ client: HieClient;
10
+ open: boolean;
11
+ onModalClose: () => void;
12
+ onSubmit: () => void;
13
+ }
14
+
15
+ const ClientDetailsModal: React.FC<ClientDetailsModalProps> = ({ client, open, onModalClose, onSubmit }) => {
16
+ if (!client) {
17
+ return <>No Client data</>;
18
+ }
19
+ const registerOnAfyaYangu = () => {
20
+ window.open('https://afyayangu.go.ke/', '_blank');
21
+ };
22
+ return (
23
+ <>
24
+ <Modal
25
+ open={open}
26
+ size="md"
27
+ onSecondarySubmit={onModalClose}
28
+ onRequestClose={onModalClose}
29
+ onRequestSubmit={registerOnAfyaYangu}
30
+ primaryButtonText="Register on Afya Yangu"
31
+ secondaryButtonText="Cancel"
32
+ >
33
+ <ModalBody>
34
+ <div className={styles.clientDetailsLayout}>
35
+ <div className={styles.sectionHeader}>
36
+ <h4 className={styles.sectionTitle}>Patient/Payment Details</h4>
37
+ </div>
38
+ <div className={styles.sectionContent}>
39
+ <Tabs>
40
+ <TabList contained>
41
+ <Tab>Patient Details</Tab>
42
+ <Tab>Payment Details</Tab>
43
+ </TabList>
44
+ <TabPanels>
45
+ <TabPanel>
46
+ <ClientDetails client={client} />
47
+ </TabPanel>
48
+ <TabPanel>
49
+ <PaymentOptionsComponent />
50
+ </TabPanel>
51
+ </TabPanels>
52
+ </Tabs>
53
+ </div>
54
+ <div className={styles.actionSection}>
55
+ <div className={styles.btnContainer}>
56
+ <Button kind="primary">Book Appointment</Button>
57
+ </div>
58
+ <div className={styles.btnContainer}>
59
+ <Button kind="secondary">Walk In Orders</Button>
60
+ </div>
61
+ <div className={styles.btnContainer}>
62
+ <Button kind="tertiary">Send To Triage</Button>
63
+ </div>
64
+ <div className={styles.btnContainer}>
65
+ <Button kind="primary">Send To Consultation</Button>
66
+ </div>
67
+ </div>
68
+ </div>
69
+ </ModalBody>
70
+ </Modal>
71
+ </>
72
+ );
73
+ };
74
+
75
+ export default ClientDetailsModal;
@@ -0,0 +1,13 @@
1
+ .modalVerificationLayout {
2
+ display: flex;
3
+ flex-direction: column;
4
+ row-gap: 10px;
5
+ width: 100%;
6
+ }
7
+ .sectionHeader{
8
+ display: flex;
9
+ flex-direction: column;
10
+ column-gap: 5px;
11
+ margin-top: 5px;
12
+ margin-bottom: 5px;
13
+ }
@@ -0,0 +1,176 @@
1
+ import React, { useState } from 'react';
2
+ import { type RequestCustomOtpDto } from '../../types';
3
+ import { Button, InlineLoading, Modal, ModalBody, TextInput } from '@carbon/react';
4
+ import styles from './otp-verification-modal.scss';
5
+ import { showSnackbar } from '@openmrs/esm-framework';
6
+ import { requestCustomOtp, validateCustomOtp } from '../../registry.resource';
7
+ import { maskValue } from '../../utils/mask-data';
8
+
9
+ interface OtpVerificationModalpProps {
10
+ requestCustomOtpDto: RequestCustomOtpDto;
11
+ phoneNumber: string;
12
+ open: boolean;
13
+ onModalClose: () => void;
14
+ }
15
+ const OtpVerificationModal: React.FC<OtpVerificationModalpProps> = ({
16
+ requestCustomOtpDto,
17
+ phoneNumber,
18
+ open,
19
+ onModalClose,
20
+ }) => {
21
+ const [otp, setOtp] = useState('');
22
+ const [otpStatus, setOtpStatus] = useState<string>('DRAFT');
23
+ const [loading, setLoading] = useState<boolean>(false);
24
+ const [sessionId, setSessionId] = useState<string>('');
25
+
26
+ const handleSendOtp = async () => {
27
+ if (!requestCustomOtpDto.identificationNumber) {
28
+ showAlert('error', 'Invalid Identification Value', 'Please enter a valid ID value');
29
+ return;
30
+ }
31
+ setLoading(true);
32
+ try {
33
+ const response = await requestCustomOtp(requestCustomOtpDto);
34
+ setSessionId(response.sessionId);
35
+ setOtpStatus('OTP_SENT');
36
+
37
+ showAlert('success', 'OTP sent successfully', `A code was sent to ${response.maskedPhone}`);
38
+ } catch (err: any) {
39
+ const errorMessage = err.message || 'Failed to send OTP';
40
+ showAlert('error', 'Error sending OTP', errorMessage);
41
+ } finally {
42
+ setLoading(false);
43
+ }
44
+ };
45
+ const showAlert = (alertType: 'error' | 'success', title: string, subtitle: string) => {
46
+ showSnackbar({
47
+ kind: alertType,
48
+ title: title,
49
+ subtitle: subtitle,
50
+ });
51
+ };
52
+
53
+ const handleVerifyOtp = async () => {
54
+ if (!otp.trim()) {
55
+ showAlert('error', 'Please enter the OTP code', '');
56
+ return;
57
+ }
58
+
59
+ setLoading(true);
60
+
61
+ try {
62
+ const payload = { sessionId, otp, locationUuid: requestCustomOtpDto.locationUuid };
63
+ await validateCustomOtp(payload);
64
+
65
+ setOtpStatus('OTP_VERIFIED');
66
+
67
+ showSnackbar({
68
+ kind: 'success',
69
+ title: 'OTP Verified',
70
+ subtitle: 'You can now fetch data from Client Registry.',
71
+ });
72
+ } catch (err: any) {
73
+ const errorMessage = err.message || 'OTP verification failed';
74
+ showSnackbar({
75
+ kind: 'error',
76
+ title: 'OTP Verification Failed',
77
+ subtitle: errorMessage,
78
+ });
79
+ } finally {
80
+ setLoading(false);
81
+ }
82
+ };
83
+ const registerOnAfyaYangu = () => {
84
+ window.open('https://afyayangu.go.ke/', '_blank');
85
+ };
86
+
87
+ const onSubmit = () => {};
88
+ return (
89
+ <>
90
+ <Modal
91
+ open={open}
92
+ size="md"
93
+ onSecondarySubmit={onModalClose}
94
+ onRequestClose={onModalClose}
95
+ onRequestSubmit={registerOnAfyaYangu}
96
+ primaryButtonText="Register on Afya Yangu"
97
+ secondaryButtonText="Cancel"
98
+ >
99
+ <ModalBody>
100
+ <div className={styles.modalVerificationLayout}>
101
+ <div className={styles.sectionHeader}>
102
+ <h4 className={styles.sectionTitle}>One Time Password (OTP)</h4>
103
+ <h6>Enter one time password to proceed</h6>
104
+ </div>
105
+ <div className={styles.sectionContent}>
106
+ <div className={styles.contentHeader}>
107
+ {otpStatus === 'DRAFT' ? (
108
+ <>
109
+ <h6>Send Code to Phone {maskValue(phoneNumber)}</h6>
110
+ </>
111
+ ) : (
112
+ <></>
113
+ )}
114
+
115
+ {otpStatus === 'OTP_SENT' ? (
116
+ <>
117
+ <TextInput
118
+ id="otp-input"
119
+ labelText="Enter OTP"
120
+ value={otp}
121
+ onChange={(e) => setOtp(e.target.value)}
122
+ placeholder="Enter the code sent to your phone"
123
+ />
124
+ </>
125
+ ) : (
126
+ <></>
127
+ )}
128
+
129
+ {otpStatus === 'OTP_VERIFIED' ? (
130
+ <>
131
+ <h6>OTP Verification Successfull!</h6>
132
+ </>
133
+ ) : (
134
+ <></>
135
+ )}
136
+ </div>
137
+ </div>
138
+ <div className={styles.sectionAction}>
139
+ {otpStatus === 'DRAFT' ? (
140
+ <>
141
+ <Button kind="primary" onClick={handleSendOtp}>
142
+ {loading ? <InlineLoading description="Sending OTP..." /> : 'Send OTP'}
143
+ </Button>
144
+ </>
145
+ ) : (
146
+ <></>
147
+ )}
148
+
149
+ {otpStatus === 'OTP_SENT' ? (
150
+ <>
151
+ <Button kind="primary" onClick={handleVerifyOtp}>
152
+ {loading ? <InlineLoading description="Verifying OTP..." /> : 'Verify'}
153
+ </Button>
154
+ </>
155
+ ) : (
156
+ <></>
157
+ )}
158
+
159
+ {otpStatus === 'OTP_VERIFIED' ? (
160
+ <>
161
+ <Button kind="primary" onClick={onModalClose}>
162
+ Continue
163
+ </Button>
164
+ </>
165
+ ) : (
166
+ <></>
167
+ )}
168
+ </div>
169
+ </div>
170
+ </ModalBody>
171
+ </Modal>
172
+ </>
173
+ );
174
+ };
175
+
176
+ export default OtpVerificationModal;
@@ -0,0 +1,21 @@
1
+ import { RadioButton, RadioButtonGroup } from '@carbon/react';
2
+ import React from 'react';
3
+ const PaymentOptionsComponent: React.FC = () => {
4
+ const handleSelectedPatientOption = (p: string) => {};
5
+ return (
6
+ <>
7
+ <RadioButtonGroup
8
+ defaultSelected="sha"
9
+ legendText="Patient"
10
+ onChange={(v) => handleSelectedPatientOption(v as string)}
11
+ name="payment-options-radio-group"
12
+ >
13
+ <RadioButton id="sha" labelText="Social Health Authority" value="sha" />
14
+ <RadioButton id="insurance" labelText="Other Insurance" value="other" />
15
+ <RadioButton id="cash" labelText="Cash" value="cash" />
16
+ </RadioButtonGroup>
17
+ </>
18
+ );
19
+ };
20
+
21
+ export default PaymentOptionsComponent;
@@ -0,0 +1,63 @@
1
+ .registryLayout{
2
+ display: flex;
3
+ flex-direction: row;
4
+ width: 100%;
5
+ row-gap: 10px;
6
+ padding: 2% 2%;
7
+ }
8
+ .mainContent{
9
+ display: flex;
10
+ flex-direction: column;
11
+ width: 100%;
12
+ }
13
+ .registryHeader{
14
+ display: flex;
15
+ flex-direction: column;
16
+ row-gap: 10px;
17
+ width: 100%;
18
+ }
19
+ .registryContent{
20
+ display: flex;
21
+ flex-direction: column;
22
+ width: 100%;
23
+ }
24
+ .formRow{
25
+ display: flex;
26
+ flex-direction: row;
27
+ width: 100%;
28
+ column-gap: 5px;
29
+ }
30
+ .formControl{
31
+ width: 30%;
32
+ }
33
+ .hieData{
34
+ margin-top: 15px;
35
+ display: flex;
36
+ flex-direction: column;
37
+ width: 50%;
38
+ row-gap: 10px;
39
+ }
40
+ .btnContainer{
41
+ padding: 5px 5px;
42
+ }
43
+ .selectionHeader{
44
+ display: flex;
45
+ flex-direction: column;
46
+ width: 100%;
47
+ row-gap: 5px;
48
+ }
49
+ .patientSelect{
50
+ display: flex;
51
+ flex-direction: row;
52
+ width: 100%;
53
+ column-gap: 5px;
54
+ }
55
+ .patientSelectRadio{
56
+ width: 70%;
57
+ }
58
+ .patientConfirmSelection{
59
+ display: flex;
60
+ flex-direction: row;
61
+ width: 30%;
62
+ column-gap: 5px;
63
+ }