@digihmis/esm-patient-registration-app 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -0
- package/dist/117.js +1 -0
- package/dist/117.js.map +1 -0
- package/dist/131.js +1 -0
- package/dist/131.js.map +1 -0
- package/dist/132.js +1 -0
- package/dist/132.js.map +1 -0
- package/dist/152.js +1 -0
- package/dist/152.js.map +1 -0
- package/dist/209.js +1 -0
- package/dist/209.js.map +1 -0
- package/dist/247.js +1 -0
- package/dist/247.js.map +1 -0
- package/dist/28.js +1 -0
- package/dist/28.js.map +1 -0
- package/dist/317.js +1 -0
- package/dist/317.js.map +1 -0
- package/dist/349.js +1 -0
- package/dist/349.js.map +1 -0
- package/dist/371.js +1 -0
- package/dist/371.js.map +1 -0
- package/dist/378.js +1 -0
- package/dist/378.js.map +1 -0
- package/dist/442.js +1 -0
- package/dist/442.js.map +1 -0
- package/dist/45.js +1 -0
- package/dist/45.js.map +1 -0
- package/dist/459.js +12 -0
- package/dist/459.js.map +1 -0
- package/dist/466.js +1 -0
- package/dist/466.js.map +1 -0
- package/dist/508.js +1 -0
- package/dist/508.js.map +1 -0
- package/dist/61.js +1 -0
- package/dist/61.js.map +1 -0
- package/dist/640.js +1 -0
- package/dist/640.js.map +1 -0
- package/dist/689.js +1 -0
- package/dist/689.js.map +1 -0
- package/dist/709.js +1 -0
- package/dist/709.js.map +1 -0
- package/dist/712.js +1 -0
- package/dist/712.js.map +1 -0
- package/dist/720.js +1 -0
- package/dist/720.js.map +1 -0
- package/dist/742.js +1 -0
- package/dist/742.js.map +1 -0
- package/dist/771.js +1 -0
- package/dist/771.js.map +1 -0
- package/dist/806.js +1 -0
- package/dist/806.js.map +1 -0
- package/dist/912.js +1 -0
- package/dist/912.js.map +1 -0
- package/dist/913.js +1 -0
- package/dist/913.js.map +1 -0
- package/dist/940.js +1 -0
- package/dist/940.js.map +1 -0
- package/dist/957.js +15 -0
- package/dist/957.js.map +1 -0
- package/dist/digihmis-esm-patient-registration-app.js +6 -0
- package/dist/digihmis-esm-patient-registration-app.js.buildmanifest.json +827 -0
- package/dist/digihmis-esm-patient-registration-app.js.map +1 -0
- package/dist/main.js +17 -0
- package/dist/main.js.map +1 -0
- package/dist/routes.json +1 -0
- package/jest.config.js +26 -0
- package/package.json +51 -0
- package/rspack.config.js +1 -0
- package/src/active-visit-table/active-visit-table.component.tsx +240 -0
- package/src/active-visit-table/active-visits.resource.ts +338 -0
- package/src/active-visit-table/active-visits.scss +211 -0
- package/src/active-visit-table/empty-data-illustration.component.tsx +39 -0
- package/src/checkin-form/base-visit-type.component.tsx +102 -0
- package/src/checkin-form/base-visit-type.scss +75 -0
- package/src/checkin-form/checkin-form.scss +167 -0
- package/src/checkin-form/checkin-form.workspace.tsx +703 -0
- package/src/checkin-form/hooks/useDefaultFacilityLocation.ts +15 -0
- package/src/checkin-form/hooks/useDefaultVisitLocation.ts +24 -0
- package/src/checkin-form/hooks/useOfflineVisitType.ts +18 -0
- package/src/checkin-form/hooks/usePatientHistory.ts +40 -0
- package/src/checkin-form/hooks/useRecommendedVisitTypes.ts +34 -0
- package/src/checkin-form/hooks/useVisitAttributeType.ts +102 -0
- package/src/checkin-form/location-selector.component.tsx +86 -0
- package/src/checkin-form/recommended-visit-type.component.tsx +32 -0
- package/src/checkin-form/visit-attribute-type.component.tsx +256 -0
- package/src/checkin-form/visit-attribute-type.scss +5 -0
- package/src/checkin-form/visit-date-time.component.tsx +201 -0
- package/src/checkin-form/visit-form.resource.ts +308 -0
- package/src/components/custom-card/compact-patient-banner.component.tsx +70 -0
- package/src/components/custom-card/compact-patient-banner.scss +34 -0
- package/src/components/custom-card/custom-loader.component.tsx +22 -0
- package/src/components/custom-card/patient-search-context.ts +30 -0
- package/src/components/custom-input/autosuggest/autosuggest.component.tsx +213 -0
- package/src/components/custom-input/autosuggest/autosuggest.scss +158 -0
- package/src/components/custom-input/autosuggest/autosuggest.test.tsx +168 -0
- package/src/components/custom-patient-banner/custom-patient-banner.component.tsx +39 -0
- package/src/components/custom-patient-banner/custom-patient-banner.resource.ts +32 -0
- package/src/components/custom-patient-banner/custom-patient-banner.scss +26 -0
- package/src/config-schema.ts +511 -0
- package/src/constant.ts +6 -0
- package/src/constants/index.ts +1 -0
- package/src/dashboard.meta.ts +8 -0
- package/src/declarations.d.ts +6 -0
- package/src/empty-svg/empty-svg.component.tsx +565 -0
- package/src/icon-wrapper/icon-wrapper.extension.tsx +17 -0
- package/src/identifier-workspace/identifier-configure-form.workspace.tsx +278 -0
- package/src/identifier-workspace/identifier-configure.scss +106 -0
- package/src/identifier-workspace/identifier.resource.ts +90 -0
- package/src/index.ts +33 -0
- package/src/left-panel/left-panel.component.tsx +54 -0
- package/src/left-panel/left-panel.scss +13 -0
- package/src/page-header/page-header.component.tsx +35 -0
- package/src/page-header/page-header.scss +76 -0
- package/src/patient-tags/deceased-patient-tag.extension.tsx +41 -0
- package/src/patient-tags/deceased-patient-tag.scss +15 -0
- package/src/patient-tags/patient-tags.resource.ts +30 -0
- package/src/patient-tags/visit-tag.extension.tsx +43 -0
- package/src/patient-tags/visit-tag.scss +19 -0
- package/src/registration-form/form-manager.ts +449 -0
- package/src/registration-form/registration-form.component.tsx +1565 -0
- package/src/registration-form/registration-form.resource.ts +264 -0
- package/src/registration-form/registration-form.scss +282 -0
- package/src/routes.json +62 -0
- package/src/search-bar/search-bar.component.tsx +285 -0
- package/src/search-bar/search-bar.resource.ts +274 -0
- package/src/search-bar/search-bar.scss +427 -0
- package/src/search-dashboard/search-dashboard.component.tsx +17 -0
- package/src/search-dashboard/search-dashboard.scss +13 -0
- package/src/setup-tests.ts +1 -0
- package/src/type/index.ts +463 -0
- package/src/utils/fhir-mapper.ts +73 -0
- package/src/visit-summary/visit-summary.component.tsx +32 -0
- package/src/visit-summary/visit-summary.resource.ts +56 -0
- package/src/visit-summary/visit-summary.scss +74 -0
- package/translations/am.json +112 -0
- package/translations/ar.json +120 -0
- package/translations/en.json +112 -0
- package/translations/es.json +114 -0
- package/translations/fr.json +114 -0
- package/translations/he.json +114 -0
- package/translations/km.json +111 -0
- package/translations/pt.json +114 -0
- package/translations/sw.json +112 -0
- package/translations/vi.json +111 -0
- package/translations/zh.json +111 -0
- package/tsconfig.json +5 -0
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
import React, { useState, useCallback, useMemo, useEffect } from 'react';
|
|
2
|
+
import { useTranslation } from 'react-i18next';
|
|
3
|
+
import {
|
|
4
|
+
Button,
|
|
5
|
+
Search,
|
|
6
|
+
Checkbox,
|
|
7
|
+
Layer,
|
|
8
|
+
SkeletonText,
|
|
9
|
+
RadioButton,
|
|
10
|
+
RadioButtonGroup,
|
|
11
|
+
InlineLoading,
|
|
12
|
+
ButtonSet,
|
|
13
|
+
} from '@carbon/react';
|
|
14
|
+
import { useConfig, showSnackbar, useLayoutType } from '@openmrs/esm-framework';
|
|
15
|
+
import { CardHeader, EmptyState } from '@openmrs/esm-patient-common-lib';
|
|
16
|
+
import styles from './identifier-configure.scss';
|
|
17
|
+
import { RegistrationConfig } from '../config-schema';
|
|
18
|
+
import { PatientIdentifierType } from '../type';
|
|
19
|
+
import { fetchPatientIdentifierTypesWithSources } from './identifier.resource';
|
|
20
|
+
import classNames from 'classnames';
|
|
21
|
+
|
|
22
|
+
interface IdentifierConfigureWorkspaceProps {
|
|
23
|
+
closeWorkspace: () => void;
|
|
24
|
+
promptBeforeClosing?: (testFcn: () => boolean) => void;
|
|
25
|
+
patientUuid?: string;
|
|
26
|
+
currentIdentifiers?: Array<string>;
|
|
27
|
+
onSave?: (selectedIdentifiers: Array<string>) => void;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export default function IdentifierConfigureWorkspace({
|
|
31
|
+
closeWorkspace,
|
|
32
|
+
promptBeforeClosing,
|
|
33
|
+
currentIdentifiers = [],
|
|
34
|
+
onSave,
|
|
35
|
+
}: IdentifierConfigureWorkspaceProps) {
|
|
36
|
+
const { t } = useTranslation();
|
|
37
|
+
const { defaultPatientIdentifierTypes } = useConfig<RegistrationConfig>();
|
|
38
|
+
const isTablet = useLayoutType() === 'tablet';
|
|
39
|
+
|
|
40
|
+
const [searchString, setSearchString] = useState('');
|
|
41
|
+
const [identifierTypes, setIdentifierTypes] = useState<Array<PatientIdentifierType>>([]);
|
|
42
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
43
|
+
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
44
|
+
const [selectedIdentifiers, setSelectedIdentifiers] = useState<Set<string>>(
|
|
45
|
+
new Set(currentIdentifiers.length > 0 ? currentIdentifiers : defaultPatientIdentifierTypes),
|
|
46
|
+
);
|
|
47
|
+
const [hasChanges, setHasChanges] = useState(false);
|
|
48
|
+
|
|
49
|
+
useEffect(() => {
|
|
50
|
+
const loadIdentifierTypes = async () => {
|
|
51
|
+
try {
|
|
52
|
+
setIsLoading(true);
|
|
53
|
+
const types = await fetchPatientIdentifierTypesWithSources();
|
|
54
|
+
setIdentifierTypes(types);
|
|
55
|
+
} catch (error) {
|
|
56
|
+
console.error('Error fetching identifier types:', error);
|
|
57
|
+
showSnackbar({
|
|
58
|
+
title: t('errorLoadingIdentifiers', 'Error loading identifiers'),
|
|
59
|
+
kind: 'error',
|
|
60
|
+
isLowContrast: false,
|
|
61
|
+
});
|
|
62
|
+
} finally {
|
|
63
|
+
setIsLoading(false);
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
loadIdentifierTypes();
|
|
68
|
+
}, [t]);
|
|
69
|
+
|
|
70
|
+
useEffect(() => {
|
|
71
|
+
if (promptBeforeClosing) {
|
|
72
|
+
promptBeforeClosing(() => !hasChanges);
|
|
73
|
+
}
|
|
74
|
+
}, [hasChanges, promptBeforeClosing]);
|
|
75
|
+
|
|
76
|
+
const filteredIdentifierTypes = useMemo(() => {
|
|
77
|
+
if (!searchString.trim()) {
|
|
78
|
+
return identifierTypes;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const searchLower = searchString.toLowerCase();
|
|
82
|
+
return identifierTypes.filter(
|
|
83
|
+
(type) => type.name.toLowerCase().includes(searchLower) || type.fieldName.toLowerCase().includes(searchLower),
|
|
84
|
+
);
|
|
85
|
+
}, [identifierTypes, searchString]);
|
|
86
|
+
|
|
87
|
+
const handleSearchChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
|
|
88
|
+
setSearchString(event.target.value);
|
|
89
|
+
}, []);
|
|
90
|
+
|
|
91
|
+
const handleToggleIdentifier = useCallback((uuid: string) => {
|
|
92
|
+
setSelectedIdentifiers((prev) => {
|
|
93
|
+
const newSet = new Set(prev);
|
|
94
|
+
if (newSet.has(uuid)) {
|
|
95
|
+
newSet.delete(uuid);
|
|
96
|
+
} else {
|
|
97
|
+
newSet.add(uuid);
|
|
98
|
+
}
|
|
99
|
+
return newSet;
|
|
100
|
+
});
|
|
101
|
+
setHasChanges(true);
|
|
102
|
+
}, []);
|
|
103
|
+
|
|
104
|
+
const handleSelectAll = useCallback(() => {
|
|
105
|
+
const allUuids = new Set(filteredIdentifierTypes.map((type) => type.uuid));
|
|
106
|
+
setSelectedIdentifiers(allUuids);
|
|
107
|
+
setHasChanges(true);
|
|
108
|
+
}, [filteredIdentifierTypes]);
|
|
109
|
+
|
|
110
|
+
const handleDeselectAll = useCallback(() => {
|
|
111
|
+
setSelectedIdentifiers(new Set());
|
|
112
|
+
setHasChanges(true);
|
|
113
|
+
}, []);
|
|
114
|
+
|
|
115
|
+
const handleCancel = useCallback(() => {
|
|
116
|
+
closeWorkspace();
|
|
117
|
+
}, [closeWorkspace]);
|
|
118
|
+
|
|
119
|
+
const handleSave = useCallback(async () => {
|
|
120
|
+
const selectedArray = Array.from(selectedIdentifiers);
|
|
121
|
+
|
|
122
|
+
try {
|
|
123
|
+
setIsSubmitting(true);
|
|
124
|
+
|
|
125
|
+
if (onSave) {
|
|
126
|
+
await onSave(selectedArray);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
showSnackbar({
|
|
130
|
+
title: t('identifiersSaved', 'Identifiers saved'),
|
|
131
|
+
subtitle: t('identifiersSavedSubtitle', '{{count}} identifier(s) configured', {
|
|
132
|
+
count: selectedArray.length,
|
|
133
|
+
}),
|
|
134
|
+
kind: 'success',
|
|
135
|
+
isLowContrast: false,
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
setHasChanges(false);
|
|
139
|
+
closeWorkspace();
|
|
140
|
+
} catch (error) {
|
|
141
|
+
showSnackbar({
|
|
142
|
+
title: t('errorSavingIdentifiers', 'Error saving identifiers'),
|
|
143
|
+
subtitle: error?.message || t('unknownError', 'An unknown error occurred'),
|
|
144
|
+
kind: 'error',
|
|
145
|
+
isLowContrast: false,
|
|
146
|
+
});
|
|
147
|
+
} finally {
|
|
148
|
+
setIsSubmitting(false);
|
|
149
|
+
}
|
|
150
|
+
}, [selectedIdentifiers, onSave, closeWorkspace, t]);
|
|
151
|
+
|
|
152
|
+
if (isLoading) {
|
|
153
|
+
return (
|
|
154
|
+
<div className={styles.workspaceContainer}>
|
|
155
|
+
<div className={styles.workspaceHeader}>
|
|
156
|
+
<h2 className={styles.SectionTitle}>{t('configureIdentifiers', 'Configure Identifiers')}</h2>
|
|
157
|
+
</div>
|
|
158
|
+
<div className={styles.workspaceContent}>
|
|
159
|
+
<SkeletonText />
|
|
160
|
+
</div>
|
|
161
|
+
</div>
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (!identifierTypes || identifierTypes.length === 0) {
|
|
166
|
+
return (
|
|
167
|
+
<div className={styles.workspaceContainer}>
|
|
168
|
+
<div className={styles.workspaceHeader}>
|
|
169
|
+
<h3>{t('configureIdentifiers', 'Configure Identifiers')}</h3>
|
|
170
|
+
</div>
|
|
171
|
+
<div className={styles.workspaceContent}>
|
|
172
|
+
<p>{t('noIdentifierTypes', 'No identifier types available')}</p>
|
|
173
|
+
</div>
|
|
174
|
+
<div className={styles.workspaceFooter}>
|
|
175
|
+
<Button kind="secondary" onClick={handleCancel}>
|
|
176
|
+
{t('close', 'Close')}
|
|
177
|
+
</Button>
|
|
178
|
+
</div>
|
|
179
|
+
</div>
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return (
|
|
184
|
+
<div className={styles.workspaceContainer}>
|
|
185
|
+
<div className={styles.workspaceHeader}>
|
|
186
|
+
<p className={styles.label01}>
|
|
187
|
+
{t('identifierInstructions', "Select the identifiers you'd like to add for this patient:")}
|
|
188
|
+
</p>
|
|
189
|
+
<div className={styles.searchContainer}>
|
|
190
|
+
<Search
|
|
191
|
+
labelText={t('searchIdentifierPlaceholder', 'Search identifier')}
|
|
192
|
+
placeholder={t('searchIdentifierPlaceholder', 'Search identifier')}
|
|
193
|
+
onChange={handleSearchChange}
|
|
194
|
+
value={searchString}
|
|
195
|
+
size="lg"
|
|
196
|
+
/>
|
|
197
|
+
</div>
|
|
198
|
+
{filteredIdentifierTypes.length > 0 && (
|
|
199
|
+
<div className={styles.bulkActions}>
|
|
200
|
+
<Button kind="ghost" size="sm" onClick={handleSelectAll}>
|
|
201
|
+
{t('selectAll', 'Select all')}
|
|
202
|
+
</Button>
|
|
203
|
+
<Button kind="ghost" size="sm" onClick={handleDeselectAll}>
|
|
204
|
+
{t('deselectAll', 'Deselect all')}
|
|
205
|
+
</Button>
|
|
206
|
+
</div>
|
|
207
|
+
)}
|
|
208
|
+
</div>
|
|
209
|
+
<div className={styles.workspaceContent}>
|
|
210
|
+
<div className={styles.identifierList}>
|
|
211
|
+
{filteredIdentifierTypes.length === 0 ? (
|
|
212
|
+
<>
|
|
213
|
+
<EmptyState
|
|
214
|
+
displayText={t('noMatchingIdentifiers', 'identifiers match your search')}
|
|
215
|
+
headerTitle={t('noMatchingIdentifierHeader', 'No identifiers found')}
|
|
216
|
+
/>
|
|
217
|
+
</>
|
|
218
|
+
) : (
|
|
219
|
+
filteredIdentifierTypes.map((identifierType) => (
|
|
220
|
+
<Layer key={identifierType.uuid} className={styles.identifierItem}>
|
|
221
|
+
<Checkbox
|
|
222
|
+
id={identifierType.uuid}
|
|
223
|
+
labelText={
|
|
224
|
+
<div className={styles.identifierLabel}>
|
|
225
|
+
<span className={styles.identifierName}>{identifierType.name}</span>
|
|
226
|
+
</div>
|
|
227
|
+
}
|
|
228
|
+
checked={selectedIdentifiers.has(identifierType.uuid) || identifierType.required}
|
|
229
|
+
onChange={() => handleToggleIdentifier(identifierType.uuid)}
|
|
230
|
+
disabled={identifierType.required || selectedIdentifiers.has(identifierType.uuid)}
|
|
231
|
+
/>
|
|
232
|
+
{identifierType.identifierSources && identifierType.identifierSources.length > 0 && (
|
|
233
|
+
<div className={styles.identifierSources}>
|
|
234
|
+
<RadioButtonGroup
|
|
235
|
+
legendText={t('sources', 'Sources')}
|
|
236
|
+
name={`source-${identifierType.uuid}`}
|
|
237
|
+
valueSelected={identifierType.identifierSources[0]?.uuid || ''}>
|
|
238
|
+
{identifierType.identifierSources.map((source) => (
|
|
239
|
+
<RadioButton
|
|
240
|
+
key={source.uuid}
|
|
241
|
+
id={`${identifierType.uuid}-${source.uuid}`}
|
|
242
|
+
labelText={source.name}
|
|
243
|
+
value={source.uuid}
|
|
244
|
+
disabled
|
|
245
|
+
/>
|
|
246
|
+
))}
|
|
247
|
+
</RadioButtonGroup>
|
|
248
|
+
</div>
|
|
249
|
+
)}
|
|
250
|
+
</Layer>
|
|
251
|
+
))
|
|
252
|
+
)}
|
|
253
|
+
</div>
|
|
254
|
+
</div>
|
|
255
|
+
<ButtonSet
|
|
256
|
+
className={classNames(styles.buttonSet, {
|
|
257
|
+
[styles.tablet]: isTablet,
|
|
258
|
+
[styles.desktop]: !isTablet,
|
|
259
|
+
})}>
|
|
260
|
+
<Button className={styles.button} kind="secondary" onClick={handleCancel}>
|
|
261
|
+
{t('discard', 'Discard')}
|
|
262
|
+
</Button>
|
|
263
|
+
<Button
|
|
264
|
+
className={styles.button}
|
|
265
|
+
disabled={selectedIdentifiers.size === 0 || isSubmitting}
|
|
266
|
+
kind="primary"
|
|
267
|
+
onClick={handleSave}>
|
|
268
|
+
{isSubmitting ? (
|
|
269
|
+
<InlineLoading className={styles.spinner} description={t('saving', 'Saving') + '...'} />
|
|
270
|
+
) : (
|
|
271
|
+
<span> {t('save', 'Save')}</span>
|
|
272
|
+
)}
|
|
273
|
+
</Button>
|
|
274
|
+
</ButtonSet>
|
|
275
|
+
/{' '}
|
|
276
|
+
</div>
|
|
277
|
+
);
|
|
278
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
@use '@carbon/layout';
|
|
2
|
+
@use '@carbon/type';
|
|
3
|
+
@use '@carbon/colors';
|
|
4
|
+
|
|
5
|
+
.workspaceContainer {
|
|
6
|
+
display: flex;
|
|
7
|
+
flex-direction: column;
|
|
8
|
+
height: 100%;
|
|
9
|
+
padding: layout.$spacing-05;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
.workspaceHeader {
|
|
13
|
+
margin-bottom: layout.$spacing-05;
|
|
14
|
+
|
|
15
|
+
h3 {
|
|
16
|
+
@include type.type-style('heading-03');
|
|
17
|
+
margin-bottom: layout.$spacing-03;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.bodyLong02 {
|
|
22
|
+
@include type.type-style('body-long-02');
|
|
23
|
+
color: var(--cds-text-secondary);
|
|
24
|
+
margin-bottom: layout.$spacing-05;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.workspaceContent {
|
|
28
|
+
flex: 1;
|
|
29
|
+
overflow-y: auto;
|
|
30
|
+
margin-bottom: layout.$spacing-05;
|
|
31
|
+
|
|
32
|
+
/* Hide scrollbar for Chrome, Safari and Opera */
|
|
33
|
+
&::-webkit-scrollbar {
|
|
34
|
+
display: none;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/* Hide scrollbar for IE, Edge and Firefox */
|
|
38
|
+
-ms-overflow-style: none;
|
|
39
|
+
scrollbar-width: none;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.searchContainer {
|
|
43
|
+
margin-top: layout.$spacing-04;
|
|
44
|
+
margin-bottom: layout.$spacing-05;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.identifierList {
|
|
48
|
+
display: flex;
|
|
49
|
+
flex-direction: column;
|
|
50
|
+
gap: layout.$spacing-05;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.identifierItem {
|
|
54
|
+
padding: layout.$spacing-04;
|
|
55
|
+
border: 1px solid var(--cds-border-subtle);
|
|
56
|
+
border-radius: 4px;
|
|
57
|
+
background-color: var(--cds-layer-01);
|
|
58
|
+
|
|
59
|
+
&:hover {
|
|
60
|
+
background-color: var(--cds-layer-hover-01);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.sourceSelection {
|
|
65
|
+
margin-top: layout.$spacing-04;
|
|
66
|
+
margin-left: layout.$spacing-07;
|
|
67
|
+
padding-left: layout.$spacing-05;
|
|
68
|
+
border-left: 2px solid var(--cds-border-subtle);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.workspaceFooter {
|
|
72
|
+
display: flex;
|
|
73
|
+
flex-direction: row;
|
|
74
|
+
padding-top: layout.$spacing-05;
|
|
75
|
+
border-top: 1px solid var(--cds-border-subtle);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
:global(.omrs-breakpoint-lt-desktop) .workspaceFooter {
|
|
79
|
+
display: flex;
|
|
80
|
+
flex-direction: row;
|
|
81
|
+
justify-content: flex-end;
|
|
82
|
+
padding-top: layout.$spacing-05;
|
|
83
|
+
border-top: 1px solid var(--cds-border-subtle);
|
|
84
|
+
}
|
|
85
|
+
.identifierSources {
|
|
86
|
+
margin-top: layout.$spacing-07;
|
|
87
|
+
@include type.type-style('body-02');
|
|
88
|
+
}
|
|
89
|
+
.label01,
|
|
90
|
+
.bulkActions {
|
|
91
|
+
@include type.type-style('label-02');
|
|
92
|
+
color: colors.$gray-100;
|
|
93
|
+
font-weight: bold;
|
|
94
|
+
}
|
|
95
|
+
.buttonSet {
|
|
96
|
+
margin-top: auto;
|
|
97
|
+
justify-self: end;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
.button {
|
|
101
|
+
height: layout.$spacing-10;
|
|
102
|
+
display: flex;
|
|
103
|
+
align-content: flex-start;
|
|
104
|
+
align-items: baseline;
|
|
105
|
+
min-width: 50%;
|
|
106
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { messageOmrsServiceWorker, openmrsFetch, restBaseUrl } from '@openmrs/esm-framework';
|
|
2
|
+
import escapeRegExp from 'lodash-es/escapeRegExp';
|
|
3
|
+
import { cacheForOfflineHeaders } from '../constant';
|
|
4
|
+
import { FetchedPatientIdentifierType, PatientIdentifierType } from '../type';
|
|
5
|
+
import find from 'lodash-es/find';
|
|
6
|
+
import camelCase from 'lodash-es/camelCase';
|
|
7
|
+
|
|
8
|
+
async function cacheAndFetch<T = any>(url?: string) {
|
|
9
|
+
const abortController = new AbortController();
|
|
10
|
+
|
|
11
|
+
await messageOmrsServiceWorker({
|
|
12
|
+
type: 'registerDynamicRoute',
|
|
13
|
+
pattern: escapeRegExp(url),
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
return await openmrsFetch<T>(url, { headers: cacheForOfflineHeaders, signal: abortController?.signal });
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function fetchPatientIdentifierTypesWithSources(): Promise<Array<PatientIdentifierType>> {
|
|
20
|
+
const patientIdentifierTypes = await fetchPatientIdentifierTypes();
|
|
21
|
+
|
|
22
|
+
// @ts-ignore Reason: The required props of the type are generated below.
|
|
23
|
+
const identifierTypes: Array<PatientIdentifierType> = patientIdentifierTypes.filter(Boolean);
|
|
24
|
+
|
|
25
|
+
const [autoGenOptions, identifierSourcesResponse] = await Promise.all([
|
|
26
|
+
fetchAutoGenerationOptions(),
|
|
27
|
+
fetchIdentifierSources(),
|
|
28
|
+
]);
|
|
29
|
+
|
|
30
|
+
const allIdentifierSources = identifierSourcesResponse.data.results;
|
|
31
|
+
|
|
32
|
+
for (let i = 0; i < identifierTypes?.length; i++) {
|
|
33
|
+
identifierTypes[i].identifierSources = allIdentifierSources
|
|
34
|
+
.filter((source) => source.identifierType.uuid === identifierTypes[i].uuid)
|
|
35
|
+
.map((source) => {
|
|
36
|
+
const option = find(autoGenOptions.data.results, { source: { uuid: source.uuid } });
|
|
37
|
+
source.autoGenerationOption = option;
|
|
38
|
+
return source;
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return identifierTypes;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function fetchPatientIdentifierTypes(): Promise<Array<FetchedPatientIdentifierType>> {
|
|
46
|
+
const [patientIdentifierTypesResponse, primaryIdentifierTypeResponse] = await Promise.all([
|
|
47
|
+
cacheAndFetch(
|
|
48
|
+
`${restBaseUrl}/patientidentifiertype?v=custom:(display,uuid,name,format,required,uniquenessBehavior)`,
|
|
49
|
+
),
|
|
50
|
+
cacheAndFetch(`${restBaseUrl}/metadatamapping/termmapping?v=full&code=emr.primaryIdentifierType`),
|
|
51
|
+
]);
|
|
52
|
+
|
|
53
|
+
if (patientIdentifierTypesResponse.ok) {
|
|
54
|
+
// Primary identifier type is to be kept at the top of the list.
|
|
55
|
+
const patientIdentifierTypes = patientIdentifierTypesResponse?.data?.results;
|
|
56
|
+
|
|
57
|
+
const primaryIdentifierTypeUuid = primaryIdentifierTypeResponse?.data?.results?.[0]?.metadataUuid;
|
|
58
|
+
|
|
59
|
+
let identifierTypes = [];
|
|
60
|
+
|
|
61
|
+
patientIdentifierTypes.forEach((type) => {
|
|
62
|
+
if (type.uuid !== primaryIdentifierTypeUuid) {
|
|
63
|
+
identifierTypes.push(mapPatientIdentifierType(type, false));
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
return identifierTypes;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return [];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function fetchIdentifierSources() {
|
|
73
|
+
return await cacheAndFetch(`${restBaseUrl}/idgen/identifiersource?v=default`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function fetchAutoGenerationOptions(abortController?: AbortController) {
|
|
77
|
+
return await cacheAndFetch(`${restBaseUrl}/idgen/autogenerationoption?v=full`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function mapPatientIdentifierType(patientIdentifierType, isPrimary) {
|
|
81
|
+
return {
|
|
82
|
+
name: patientIdentifierType.display,
|
|
83
|
+
fieldName: camelCase(patientIdentifierType.name),
|
|
84
|
+
required: patientIdentifierType.required,
|
|
85
|
+
uuid: patientIdentifierType.uuid,
|
|
86
|
+
format: patientIdentifierType.format,
|
|
87
|
+
isPrimary,
|
|
88
|
+
uniquenessBehavior: patientIdentifierType.uniquenessBehavior,
|
|
89
|
+
};
|
|
90
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { defineConfigSchema, getSyncLifecycle } from '@openmrs/esm-framework';
|
|
2
|
+
import { configSchema } from './config-schema';
|
|
3
|
+
import { dashboardMeta } from './dashboard.meta';
|
|
4
|
+
import { createDashboardLink } from './left-panel/left-panel.component';
|
|
5
|
+
import SearchDashboard from './search-dashboard/search-dashboard.component';
|
|
6
|
+
import { RegistrationForm } from './registration-form/registration-form.component';
|
|
7
|
+
import IdentifierConfigureWorkspace from './identifier-workspace/identifier-configure-form.workspace';
|
|
8
|
+
import VisitTag from './patient-tags/visit-tag.extension';
|
|
9
|
+
import DeceasedPatientBannerTag from './patient-tags/deceased-patient-tag.extension';
|
|
10
|
+
import CheckInWorkspace from './checkin-form/checkin-form.workspace';
|
|
11
|
+
|
|
12
|
+
export const moduleName = '@digihmis/esm-patient-registration-app';
|
|
13
|
+
const pageName = 'registration';
|
|
14
|
+
|
|
15
|
+
const options = {
|
|
16
|
+
featureName: pageName,
|
|
17
|
+
moduleName,
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export const importTranslation = require.context('../translations', false, /.json$/, 'lazy');
|
|
21
|
+
export const leftPanelLink = getSyncLifecycle(createDashboardLink(dashboardMeta), options);
|
|
22
|
+
export const searchDashboard = getSyncLifecycle(SearchDashboard, options);
|
|
23
|
+
export const registrationForm = getSyncLifecycle(RegistrationForm, options);
|
|
24
|
+
export const identifierConfigureForm = getSyncLifecycle(IdentifierConfigureWorkspace, options);
|
|
25
|
+
|
|
26
|
+
// t('checkInFormTitle', 'Check In form')
|
|
27
|
+
export const checkInWorkspace = getSyncLifecycle(CheckInWorkspace, options);
|
|
28
|
+
export const visitTag = getSyncLifecycle(VisitTag, options);
|
|
29
|
+
export const deceasedPatientTag = getSyncLifecycle(DeceasedPatientBannerTag, options);
|
|
30
|
+
|
|
31
|
+
export function startupApp() {
|
|
32
|
+
defineConfigSchema(moduleName, configSchema);
|
|
33
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import React, { useMemo } from 'react';
|
|
2
|
+
import { ConfigurableLink, MaybeIcon } from '@openmrs/esm-framework';
|
|
3
|
+
import { useTranslation } from 'react-i18next';
|
|
4
|
+
import { type CarbonIconType } from '@carbon/react/icons';
|
|
5
|
+
import last from 'lodash-es/last';
|
|
6
|
+
import styles from './left-panel.scss';
|
|
7
|
+
|
|
8
|
+
export interface DashboardLinkConfig {
|
|
9
|
+
name: string;
|
|
10
|
+
title: string;
|
|
11
|
+
icon?: string | CarbonIconType;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const DashboardLink = ({ dashboardLinkConfig }: { dashboardLinkConfig: DashboardLinkConfig }) => {
|
|
15
|
+
const { t } = useTranslation();
|
|
16
|
+
const { name, title, icon } = dashboardLinkConfig;
|
|
17
|
+
const nameSegment = name.split('/').at(-1);
|
|
18
|
+
|
|
19
|
+
const currentPath = window.location.pathname;
|
|
20
|
+
const basePath = window.getOpenmrsSpaBase?.() || '';
|
|
21
|
+
const cleanBasePath = basePath.endsWith('/') ? basePath.slice(0, -1) : basePath;
|
|
22
|
+
const spaBasePath = `${cleanBasePath}/home`;
|
|
23
|
+
|
|
24
|
+
const urlSegment = useMemo(() => {
|
|
25
|
+
const pathSegments = currentPath.split('/');
|
|
26
|
+
const lastSegment = last(pathSegments) || '';
|
|
27
|
+
return decodeURIComponent(lastSegment);
|
|
28
|
+
}, [currentPath]);
|
|
29
|
+
|
|
30
|
+
const IconComp = typeof icon !== 'string' ? (icon as CarbonIconType) : null;
|
|
31
|
+
const isActive = currentPath.includes(`/${nameSegment}`) || nameSegment === urlSegment;
|
|
32
|
+
|
|
33
|
+
let linkPath = `${spaBasePath}/registration`;
|
|
34
|
+
if (name && name.trim() !== '') {
|
|
35
|
+
linkPath += name.startsWith('/') ? name : `/${name}`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<ConfigurableLink to={linkPath} className={`cds--side-nav__link ${isActive ? 'active-left-nav-link' : ''}`}>
|
|
40
|
+
{typeof icon === 'string' ? (
|
|
41
|
+
<MaybeIcon icon={icon} className={styles.icon} size={16} />
|
|
42
|
+
) : (
|
|
43
|
+
IconComp && <IconComp className={styles.icon} />
|
|
44
|
+
)}
|
|
45
|
+
{t(title, 'Search')}
|
|
46
|
+
</ConfigurableLink>
|
|
47
|
+
);
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export const createDashboardLink = (dashboardLinkConfig: DashboardLinkConfig) => () => {
|
|
51
|
+
return <DashboardLink dashboardLinkConfig={dashboardLinkConfig} />;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export default DashboardLink;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { useTranslation } from 'react-i18next';
|
|
3
|
+
import { Calendar } from '@carbon/react/icons';
|
|
4
|
+
import { useSession, formatDate, ExtensionSlot, useConfig } from '@openmrs/esm-framework';
|
|
5
|
+
import styles from './page-header.scss';
|
|
6
|
+
import { IconWrapper } from '../icon-wrapper/icon-wrapper.extension';
|
|
7
|
+
import { ConfigObject } from '../config-schema';
|
|
8
|
+
|
|
9
|
+
export const PageHeader: React.FC = () => {
|
|
10
|
+
const { t } = useTranslation();
|
|
11
|
+
const userSession = useSession();
|
|
12
|
+
const { registrationIcon } = useConfig<ConfigObject>();
|
|
13
|
+
const userLocation = userSession?.sessionLocation?.display;
|
|
14
|
+
|
|
15
|
+
return (
|
|
16
|
+
<div className={styles.header}>
|
|
17
|
+
<div className={styles['left-justified-items']}>
|
|
18
|
+
<IconWrapper src={registrationIcon.src} alt="Registration Dashboard Icon" />
|
|
19
|
+
<div className={styles['page-labels']}>
|
|
20
|
+
<p className={styles['page-name']}>{userLocation}</p>
|
|
21
|
+
<p className={styles['page-subtitle']}>{t('search', 'Search')}</p>
|
|
22
|
+
</div>
|
|
23
|
+
</div>
|
|
24
|
+
<div className={styles['right-justified-items']}>
|
|
25
|
+
<div className={styles.extensionSlot}>
|
|
26
|
+
<ExtensionSlot name={'provider-banner-slot'} />
|
|
27
|
+
<div className={styles['date-and-location']}>
|
|
28
|
+
<Calendar size={16} />
|
|
29
|
+
<span className={styles.value}>{formatDate(new Date(), { mode: 'wide', noToday: true })}</span>
|
|
30
|
+
</div>
|
|
31
|
+
</div>
|
|
32
|
+
</div>
|
|
33
|
+
</div>
|
|
34
|
+
);
|
|
35
|
+
};
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
@use '@carbon/layout';
|
|
2
|
+
@use '@carbon/type';
|
|
3
|
+
@use '@carbon/colors';
|
|
4
|
+
|
|
5
|
+
.header {
|
|
6
|
+
@include type.type-style('body-compact-02');
|
|
7
|
+
height: layout.$spacing-12;
|
|
8
|
+
border: 1px solid colors.$gray-20;
|
|
9
|
+
border-left: 0px;
|
|
10
|
+
display: flex;
|
|
11
|
+
background: var(--cds-field);
|
|
12
|
+
justify-content: space-between;
|
|
13
|
+
margin-top: 1.5rem;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
.left-justified-items {
|
|
17
|
+
display: flex;
|
|
18
|
+
flex-direction: row;
|
|
19
|
+
align-items: center;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
.right-justified-items {
|
|
23
|
+
@include type.type-style('body-01');
|
|
24
|
+
color: colors.$gray-80;
|
|
25
|
+
padding-top: layout.$spacing-05;
|
|
26
|
+
padding-bottom: layout.$spacing-07;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.page-subtitle {
|
|
30
|
+
@include type.type-style('caption-01');
|
|
31
|
+
color: colors.$gray-70;
|
|
32
|
+
font-size: layout.$spacing-06;
|
|
33
|
+
}
|
|
34
|
+
.page-name {
|
|
35
|
+
@include type.type-style('caption-01');
|
|
36
|
+
color: colors.$gray-80;
|
|
37
|
+
font-size: layout.$spacing-05;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.page-labels {
|
|
41
|
+
p:first-of-type {
|
|
42
|
+
margin-bottom: layout.$spacing-02;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.date-and-location {
|
|
47
|
+
margin-top: layout.$spacing-03;
|
|
48
|
+
display: flex;
|
|
49
|
+
justify-content: flex-end;
|
|
50
|
+
align-items: center;
|
|
51
|
+
margin-right: layout.$spacing-05;
|
|
52
|
+
@include type.type-style('caption-01');
|
|
53
|
+
font-size: 0.83rem;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.value {
|
|
57
|
+
margin-left: layout.$spacing-02;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.middot {
|
|
61
|
+
margin: 0 layout.$spacing-03;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.view {
|
|
65
|
+
@include type.type-style('caption-01');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
svg.iconOverrides {
|
|
69
|
+
width: 72 !important;
|
|
70
|
+
height: 72 !important;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.svgContainer svg {
|
|
74
|
+
width: 72px;
|
|
75
|
+
height: 72px;
|
|
76
|
+
}
|