@sasindi/addresults 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.
Files changed (66) hide show
  1. package/LICENSE +354 -0
  2. package/README.md +167 -0
  3. package/dist/117.js +1 -0
  4. package/dist/117.js.map +1 -0
  5. package/dist/128.js +1 -0
  6. package/dist/128.js.map +1 -0
  7. package/dist/197.js +1 -0
  8. package/dist/197.js.map +1 -0
  9. package/dist/237.js +1 -0
  10. package/dist/237.js.map +1 -0
  11. package/dist/267.js +1 -0
  12. package/dist/267.js.map +1 -0
  13. package/dist/277.js +1 -0
  14. package/dist/277.js.map +1 -0
  15. package/dist/362.js +1 -0
  16. package/dist/362.js.map +1 -0
  17. package/dist/372.js +43 -0
  18. package/dist/372.js.map +1 -0
  19. package/dist/480.js +1 -0
  20. package/dist/480.js.map +1 -0
  21. package/dist/517.js +1 -0
  22. package/dist/517.js.map +1 -0
  23. package/dist/529.js +1 -0
  24. package/dist/529.js.map +1 -0
  25. package/dist/538.js +1 -0
  26. package/dist/538.js.map +1 -0
  27. package/dist/578.js +1 -0
  28. package/dist/578.js.map +1 -0
  29. package/dist/584.js +22 -0
  30. package/dist/584.js.map +1 -0
  31. package/dist/652.js +1 -0
  32. package/dist/652.js.map +1 -0
  33. package/dist/668.js +1 -0
  34. package/dist/668.js.map +1 -0
  35. package/dist/749.js +1 -0
  36. package/dist/749.js.map +1 -0
  37. package/dist/898.js +1 -0
  38. package/dist/898.js.map +1 -0
  39. package/dist/933.js +1 -0
  40. package/dist/933.js.map +1 -0
  41. package/dist/964.js +1 -0
  42. package/dist/964.js.map +1 -0
  43. package/dist/976.js +1 -0
  44. package/dist/976.js.map +1 -0
  45. package/dist/98.js +1 -0
  46. package/dist/98.js.map +1 -0
  47. package/dist/997.js +1 -0
  48. package/dist/997.js.map +1 -0
  49. package/dist/main.js +6 -0
  50. package/dist/main.js.map +1 -0
  51. package/dist/openmrs-esm-addresults.js +6 -0
  52. package/dist/openmrs-esm-addresults.js.buildmanifest.json +796 -0
  53. package/dist/openmrs-esm-addresults.js.map +1 -0
  54. package/dist/routes.json +1 -0
  55. package/package.json +103 -0
  56. package/src/add-results-form.component.tsx +831 -0
  57. package/src/add-results.component.tsx +258 -0
  58. package/src/add-results.scss +448 -0
  59. package/src/config-schema.ts +21 -0
  60. package/src/constants.ts +1 -0
  61. package/src/declarations.d.ts +23 -0
  62. package/src/index.ts +20 -0
  63. package/src/root.component.test.tsx +12 -0
  64. package/src/root.component.tsx +13 -0
  65. package/src/root.scss +5 -0
  66. package/src/routes.json +19 -0
@@ -0,0 +1,258 @@
1
+ import React, { useState, useEffect } from 'react';
2
+ import { useTranslation } from 'react-i18next';
3
+ import styles from './add-results.scss';
4
+
5
+ const navigateTo = (url: string) => {
6
+ window.history.pushState({}, '', url);
7
+ window.dispatchEvent(new PopStateEvent('popstate'));
8
+ };
9
+
10
+ const AddResults: React.FC = () => {
11
+ const { t } = useTranslation();
12
+
13
+ // State variables
14
+ const [locations, setLocations] = useState<any[]>([]);
15
+ const [selectedLocation, setSelectedLocation] = useState<string>('');
16
+ const [currentUserUuid, setCurrentUserUuid] = useState<string>('');
17
+ const [locationName, setLocationName] = useState<string>('Loading location...');
18
+ const [data, setData] = useState<any[]>([]);
19
+ const [loading, setLoading] = useState<boolean>(true);
20
+ const [searchQuery, setSearchQuery] = useState<string>('');
21
+
22
+ // Load Session, Current Location and Hospital Locations
23
+ useEffect(() => {
24
+ async function initContext() {
25
+ try {
26
+ // 1. Get Session Data
27
+ const sessionRes = await fetch('/openmrs/ws/rest/v1/session');
28
+ if (!sessionRes.ok) throw new Error('Session request failed');
29
+ const sessionData = await sessionRes.json();
30
+ const personUuid = sessionData.user.person.uuid;
31
+ setCurrentUserUuid(sessionData.user.uuid);
32
+
33
+ // 2. Get Person Attributes for Institute Id
34
+ const personRes = await fetch(`/openmrs/ws/rest/v1/person/${personUuid}`);
35
+ if (!personRes.ok) throw new Error('Person details request failed');
36
+ const personData = await personRes.json();
37
+
38
+ let initialLocUuid = '';
39
+ if (personData.attributes) {
40
+ personData.attributes.forEach((attr: any) => {
41
+ if (attr.display) {
42
+ const [key, value] = attr.display.split('=');
43
+ if (key && value) {
44
+ const cleanKey = key.trim();
45
+ if (cleanKey === 'Institute Id' || cleanKey === 'Institute') {
46
+ initialLocUuid = value.trim();
47
+ }
48
+ }
49
+ }
50
+ });
51
+ }
52
+
53
+ if (!initialLocUuid && sessionData.sessionLocation && sessionData.sessionLocation.uuid) {
54
+ initialLocUuid = sessionData.sessionLocation.uuid;
55
+ }
56
+
57
+ setSelectedLocation(initialLocUuid);
58
+
59
+ // 3. Load Locations (Try standard location resource first, filtering address14 for Hospital)
60
+ let loaded = false;
61
+ try {
62
+ const locsRes = await fetch('/openmrs/ws/rest/v1/location?v=custom:(uuid,name,address14)');
63
+ if (locsRes.ok) {
64
+ const locsData = await locsRes.json();
65
+ const filteredLocs = (locsData.results || []).filter((loc: any) => loc.address14 === 'Hospital');
66
+ if (filteredLocs.length > 0) {
67
+ setLocations(filteredLocs);
68
+ loaded = true;
69
+ }
70
+ }
71
+ } catch (e) {
72
+ console.warn("Standard location API query failed, trying patientregister custom API:", e);
73
+ }
74
+
75
+ if (!loaded) {
76
+ try {
77
+ const fallbackRes = await fetch('/openmrs/ws/rest/v1/patientregister/patientregister/getSelectedTypeData/Hospital');
78
+ if (fallbackRes.ok) {
79
+ const fallbackData = await fallbackRes.json();
80
+ setLocations(fallbackData.data || []);
81
+ }
82
+ } catch (e) {
83
+ console.error("All location loading attempts failed:", e);
84
+ }
85
+ }
86
+ } catch (err) {
87
+ console.error('Error during context initialization:', err);
88
+ setLocationName('Error loading location context');
89
+ }
90
+ }
91
+
92
+ initContext();
93
+ }, []);
94
+
95
+ // Fetch Table Data when selectedLocation changes
96
+ const fetchTableData = async () => {
97
+ if (!selectedLocation) return;
98
+ setLoading(true);
99
+ try {
100
+ // NOTE: Custom REST Controller mapped under '/openmrs/rest/v1/addresult' without '/ws'
101
+ const url = `/openmrs/rest/v1/addresult/loaddetails/${selectedLocation}`;
102
+ const response = await fetch(url);
103
+ if (!response.ok) throw new Error('Failed to load table data');
104
+ const resData = await response.json();
105
+ setData(resData || []);
106
+ } catch (err) {
107
+ console.error('Error fetching table data:', err);
108
+ setData([]);
109
+ } finally {
110
+ setLoading(false);
111
+ }
112
+ };
113
+
114
+ useEffect(() => {
115
+ fetchTableData();
116
+ }, [selectedLocation]);
117
+
118
+ // Set location name for the selected location dropdown
119
+ useEffect(() => {
120
+ if (selectedLocation && locations.length > 0) {
121
+ const match = locations.find(l => l.uuid === selectedLocation);
122
+ if (match) setLocationName(match.name);
123
+ }
124
+ }, [selectedLocation, locations]);
125
+
126
+ // Map gender characters to full words
127
+ const formatGender = (gender: string) => {
128
+ if (!gender) return '';
129
+ const upper = gender.trim().toUpperCase();
130
+ if (upper === 'M' || upper === 'MALE') return 'Male';
131
+ if (upper === 'F' || upper === 'FEMALE') return 'Female';
132
+ return gender;
133
+ };
134
+
135
+ // Handle select button click
136
+ const handleSelect = (row: any) => {
137
+ const phnNo = row[0];
138
+ const section = row[7];
139
+ const barcode = row[8];
140
+ const id = row[11];
141
+
142
+ localStorage.setItem('phnNo', phnNo);
143
+ localStorage.setItem('section', section);
144
+ localStorage.setItem('barcode', barcode);
145
+ localStorage.setItem('id', String(id));
146
+
147
+ navigateTo(window.getOpenmrsSpaBase() + 'addresults-form');
148
+ };
149
+
150
+ // Filter data by search query (matching barcode)
151
+ const filteredData = data.filter((row: any) => {
152
+ const barcode = String(row[8] || '').toLowerCase();
153
+ const query = searchQuery.toLowerCase().trim();
154
+ return barcode.includes(query);
155
+ });
156
+
157
+ return (
158
+ <div className={styles.bodyWrapper}>
159
+ <div className={styles.navigationCard}>
160
+ <div className={styles.navRow}>
161
+ <h2 className={styles.navTitle}>Add Results Dashboard</h2>
162
+ <div className={styles.instituteSelector}>
163
+ <span>Hospital:</span>
164
+ <select
165
+ className={styles.formControl}
166
+ value={selectedLocation}
167
+ onChange={(e) => setSelectedLocation(e.target.value)}
168
+ style={{ minWidth: '220px' }}
169
+ >
170
+ <option value="">Select Hospital Location</option>
171
+ {locations.map((loc) => (
172
+ <option key={loc.uuid} value={loc.uuid}>
173
+ {loc.name}
174
+ </option>
175
+ ))}
176
+ </select>
177
+ </div>
178
+ </div>
179
+ </div>
180
+
181
+ <div className={styles.card}>
182
+ <div className={styles.searchBar}>
183
+ <span className={styles.searchLabel}>Search Barcode:</span>
184
+ <input
185
+ type="text"
186
+ className={styles.formControl}
187
+ placeholder="Type barcode number to filter..."
188
+ value={searchQuery}
189
+ onChange={(e) => setSearchQuery(e.target.value)}
190
+ style={{ flexGrow: 1 }}
191
+ />
192
+ </div>
193
+
194
+ {loading ? (
195
+ <div className={styles.textCenter} style={{ padding: '2rem' }}>
196
+ <div className={styles.spinner} style={{ margin: '0 auto' }}></div>
197
+ <p style={{ marginTop: '1rem', color: 'var(--text-light)' }}>Loading pending samples list...</p>
198
+ </div>
199
+ ) : (
200
+ <div className={styles.tableResponsive}>
201
+ <table className={`${styles.table} ${styles.tableHover} ${styles.tableStriped} ${styles.tableBordered}`}>
202
+ <thead>
203
+ <tr>
204
+ <th>No</th>
205
+ <th>Section</th>
206
+ <th>Barcode No</th>
207
+ <th>Gender</th>
208
+ <th>Age</th>
209
+ <th>Test Name</th>
210
+ <th>Order By</th>
211
+ <th>Order Date</th>
212
+ <th>Collect By</th>
213
+ <th>Collect Date</th>
214
+ <th style={{ textAlign: 'center' }}>Action</th>
215
+ </tr>
216
+ </thead>
217
+ <tbody>
218
+ {filteredData.length > 0 ? (
219
+ filteredData.map((row: any, idx: number) => (
220
+ <tr key={row[11] || idx}>
221
+ <td>{idx + 1}</td>
222
+ <td>{row[7]}</td>
223
+ <td style={{ fontWeight: 600 }}>{row[8]}</td>
224
+ <td>{formatGender(row[3])}</td>
225
+ <td>{row[4]}</td>
226
+ <td style={{ color: 'var(--primary-color)', fontWeight: 500 }}>{row[2]}</td>
227
+ <td>{row[5]}</td>
228
+ <td>{row[6]}</td>
229
+ <td>{row[9]}</td>
230
+ <td>{row[10]}</td>
231
+ <td style={{ textAlign: 'center' }}>
232
+ <button
233
+ type="button"
234
+ className={`${styles.btn} ${styles.btnPrimary}`}
235
+ onClick={() => handleSelect(row)}
236
+ >
237
+ Select
238
+ </button>
239
+ </td>
240
+ </tr>
241
+ ))
242
+ ) : (
243
+ <tr>
244
+ <td colSpan={11} className={styles.textCenter} style={{ padding: '2rem', color: 'var(--text-light)' }}>
245
+ No pending laboratory samples found.
246
+ </td>
247
+ </tr>
248
+ )}
249
+ </tbody>
250
+ </table>
251
+ </div>
252
+ )}
253
+ </div>
254
+ </div>
255
+ );
256
+ };
257
+
258
+ export default AddResults;
@@ -0,0 +1,448 @@
1
+ /* Styling system for Add Results module in OpenMRS 3 */
2
+
3
+ :root {
4
+ --primary-color: #009384;
5
+ --secondary-color: #00bfa5;
6
+ --bg-color: #f4f7f6;
7
+ --card-bg: #ffffff;
8
+ --text-dark: #333333;
9
+ --text-light: #666666;
10
+ --border-color: #e0e0e0;
11
+ --danger-color: #ff5252;
12
+ --success-color: #4caf50;
13
+ --info-color: #00b0ff;
14
+ --warning-color: #ffd740;
15
+ }
16
+
17
+ .bodyWrapper {
18
+ background-color: var(--bg-color);
19
+ padding: 1.5rem;
20
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
21
+ color: var(--text-dark);
22
+ min-height: 100vh;
23
+ }
24
+
25
+ .navigationCard {
26
+ background: var(--card-bg);
27
+ border-radius: 8px;
28
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
29
+ margin-bottom: 2rem;
30
+ padding: 1.5rem;
31
+ border-left: 4px solid var(--primary-color);
32
+ }
33
+
34
+ .navRow {
35
+ display: flex;
36
+ justify-content: space-between;
37
+ align-items: center;
38
+ flex-wrap: wrap;
39
+ }
40
+
41
+ .navTitle {
42
+ font-size: 1.4rem;
43
+ color: var(--primary-color);
44
+ font-weight: 600;
45
+ margin: 0;
46
+ }
47
+
48
+ .instituteSelector {
49
+ display: flex;
50
+ align-items: center;
51
+ gap: 0.5rem;
52
+ font-weight: 500;
53
+ }
54
+
55
+ .formControl {
56
+ padding: 0.375rem 0.75rem;
57
+ font-size: 0.875rem;
58
+ line-height: 1.5;
59
+ color: #495057;
60
+ background-color: #fff;
61
+ background-clip: padding-box;
62
+ border: 1px solid #ced4da;
63
+ border-radius: 0.25rem;
64
+ transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
65
+
66
+ &:focus {
67
+ border-color: var(--secondary-color);
68
+ outline: 0;
69
+ box-shadow: 0 0 0 0.2rem rgba(0, 191, 165, 0.25);
70
+ }
71
+ }
72
+
73
+ .card {
74
+ background: var(--card-bg);
75
+ border-radius: 8px;
76
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
77
+ padding: 2rem;
78
+ margin-bottom: 2rem;
79
+ }
80
+
81
+ .row {
82
+ display: flex;
83
+ flex-wrap: wrap;
84
+ margin-right: -15px;
85
+ margin-left: -15px;
86
+ }
87
+
88
+ .col12 {
89
+ flex: 0 0 100%;
90
+ max-width: 100%;
91
+ padding-right: 15px;
92
+ padding-left: 15px;
93
+ }
94
+
95
+ .colMd12 {
96
+ flex: 0 0 100%;
97
+ max-width: 100%;
98
+ padding-right: 15px;
99
+ padding-left: 15px;
100
+ }
101
+
102
+ .col {
103
+ flex-grow: 1;
104
+ max-width: 100%;
105
+ padding-right: 15px;
106
+ padding-left: 15px;
107
+ }
108
+
109
+ .my1 {
110
+ margin-top: 0.25rem;
111
+ margin-bottom: 0.25rem;
112
+ display: flex;
113
+ flex-direction: column;
114
+ }
115
+
116
+ .my1 label {
117
+ font-size: 0.8rem;
118
+ font-weight: 600;
119
+ margin-bottom: 0.25rem;
120
+ color: var(--text-light);
121
+ }
122
+
123
+ .tableResponsive {
124
+ display: block;
125
+ width: 100%;
126
+ overflow-x: auto;
127
+ margin-top: 1rem;
128
+ }
129
+
130
+ .table {
131
+ width: 100%;
132
+ margin-bottom: 1rem;
133
+ color: var(--text-dark);
134
+ border-collapse: collapse;
135
+
136
+ th, td {
137
+ padding: 0.75rem;
138
+ vertical-align: middle;
139
+ border-top: 1px solid var(--border-color);
140
+ font-size: 0.85rem;
141
+ }
142
+
143
+ th {
144
+ background-color: #f1f3f2;
145
+ color: #495057;
146
+ font-weight: 600;
147
+ text-align: left;
148
+ border-bottom: 2px solid var(--border-color);
149
+ }
150
+ }
151
+
152
+ .tableHover tbody tr:hover {
153
+ background-color: rgba(0, 147, 132, 0.05);
154
+ }
155
+
156
+ .tableStriped tbody tr:nth-of-type(odd) {
157
+ background-color: rgba(0, 0, 0, 0.02);
158
+ }
159
+
160
+ .tableBordered {
161
+ border: 1px solid var(--border-color);
162
+
163
+ th, td {
164
+ border: 1px solid var(--border-color);
165
+ }
166
+ }
167
+
168
+ .btn {
169
+ display: inline-block;
170
+ font-weight: 500;
171
+ color: #212529;
172
+ text-align: center;
173
+ vertical-align: middle;
174
+ cursor: pointer;
175
+ user-select: none;
176
+ background-color: transparent;
177
+ border: 1px solid transparent;
178
+ padding: 0.375rem 0.75rem;
179
+ font-size: 0.875rem;
180
+ line-height: 1.5;
181
+ border-radius: 0.25rem;
182
+ transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
183
+
184
+ &:focus {
185
+ outline: 0;
186
+ }
187
+ }
188
+
189
+ .btnPrimary {
190
+ color: #fff;
191
+ background-color: var(--primary-color);
192
+ border-color: var(--primary-color);
193
+
194
+ &:hover {
195
+ background-color: #00776b;
196
+ border-color: #00776b;
197
+ }
198
+ }
199
+
200
+ .btnSecondary {
201
+ color: #fff;
202
+ background-color: #6c757d;
203
+ border-color: #6c757d;
204
+
205
+ &:hover {
206
+ background-color: #5a6268;
207
+ border-color: #545b62;
208
+ }
209
+ }
210
+
211
+ .btnSuccess {
212
+ color: #fff;
213
+ background-color: var(--success-color);
214
+ border-color: var(--success-color);
215
+
216
+ &:hover {
217
+ background-color: #3d8b40;
218
+ border-color: #3d8b40;
219
+ }
220
+ }
221
+
222
+ .btnDanger {
223
+ color: #fff;
224
+ background-color: var(--danger-color);
225
+ border-color: var(--danger-color);
226
+
227
+ &:hover {
228
+ background-color: #e53935;
229
+ border-color: #e53935;
230
+ }
231
+ }
232
+
233
+ .btnWarning {
234
+ color: #212529;
235
+ background-color: var(--warning-color);
236
+ border-color: var(--warning-color);
237
+
238
+ &:hover {
239
+ background-color: #ffc107;
240
+ border-color: #ffc107;
241
+ }
242
+ }
243
+
244
+ .btnInfo {
245
+ color: #fff;
246
+ background-color: var(--info-color);
247
+ border-color: var(--info-color);
248
+
249
+ &:hover {
250
+ background-color: #0091ea;
251
+ border-color: #0091ea;
252
+ }
253
+ }
254
+
255
+ .mx2 { margin-left: 0.5rem; margin-right: 0.5rem; }
256
+ .mx5 { margin-left: 1.5rem; margin-right: 1.5rem; }
257
+ .my5 { margin-top: 1.5rem; margin-bottom: 1.5rem; }
258
+
259
+ /* Form entry parameter styles */
260
+ .showHideTableParameter {
261
+ max-height: 350px;
262
+ overflow-y: auto;
263
+ border: 1px solid var(--border-color);
264
+ border-radius: 4px;
265
+ margin-top: 1rem;
266
+ }
267
+
268
+ /* Spinner and overlay */
269
+ .overlay {
270
+ position: fixed;
271
+ top: 0;
272
+ left: 0;
273
+ width: 100%;
274
+ height: 100%;
275
+ background-color: rgba(0, 0, 0, 0.6);
276
+ justify-content: center;
277
+ align-items: center;
278
+ z-index: 9999;
279
+ display: flex;
280
+ flex-direction: column;
281
+ color: #ffffff;
282
+ font-weight: 500;
283
+ gap: 1rem;
284
+ }
285
+
286
+ .spinner {
287
+ border: 6px solid #f3f3f3;
288
+ border-top: 6px solid var(--secondary-color);
289
+ border-radius: 50%;
290
+ width: 70px;
291
+ height: 70px;
292
+ animation: spin 1s linear infinite;
293
+ }
294
+
295
+ @keyframes spin {
296
+ 0% { transform: rotate(0deg); }
297
+ 100% { transform: rotate(360deg); }
298
+ }
299
+
300
+ /* Modal styling */
301
+ .customModal {
302
+ position: fixed;
303
+ top: 0;
304
+ left: 0;
305
+ width: 100%;
306
+ height: 100%;
307
+ background: rgba(0, 0, 0, 0.5);
308
+ display: flex;
309
+ justify-content: center;
310
+ align-items: center;
311
+ z-index: 1050;
312
+ }
313
+
314
+ .modalDialog {
315
+ background: white;
316
+ border-radius: 8px;
317
+ width: 90%;
318
+ max-width: 600px;
319
+ box-shadow: 0 5px 15px rgba(0,0,0,0.3);
320
+ overflow: hidden;
321
+ }
322
+
323
+ .modalDialogLarge {
324
+ max-width: 80%;
325
+ }
326
+
327
+ .modalHeader {
328
+ padding: 1rem 1.5rem;
329
+ border-bottom: 1px solid var(--border-color);
330
+ display: flex;
331
+ justify-content: space-between;
332
+ align-items: center;
333
+ background: #f8f9fa;
334
+ }
335
+
336
+ .modalTitle {
337
+ font-weight: 600;
338
+ color: var(--primary-color);
339
+ margin: 0;
340
+ }
341
+
342
+ .closeButton {
343
+ background: none;
344
+ border: none;
345
+ font-size: 1.5rem;
346
+ cursor: pointer;
347
+ color: var(--text-light);
348
+ line-height: 1;
349
+ }
350
+
351
+ .modalBody {
352
+ padding: 1.5rem;
353
+ max-height: 70vh;
354
+ overflow-y: auto;
355
+ }
356
+
357
+ .modalFooter {
358
+ padding: 1rem 1.5rem;
359
+ border-top: 1px solid var(--border-color);
360
+ display: flex;
361
+ justify-content: flex-end;
362
+ gap: 0.5rem;
363
+ background: #f8f9fa;
364
+ }
365
+
366
+ .actionSelectBox {
367
+ display: flex;
368
+ flex-direction: column;
369
+ gap: 1rem;
370
+ padding: 1rem;
371
+ text-align: center;
372
+ }
373
+
374
+ .formGroup {
375
+ margin-bottom: 1rem;
376
+ }
377
+
378
+ .formLabel {
379
+ font-size: 0.875rem;
380
+ font-weight: 600;
381
+ color: var(--text-dark);
382
+ margin-bottom: 0.5rem;
383
+ display: block;
384
+ }
385
+
386
+ .textCenter {
387
+ text-align: center;
388
+ }
389
+
390
+ .textRight {
391
+ text-align: right;
392
+ }
393
+
394
+ /* Custom search bar */
395
+ .searchBar {
396
+ display: flex;
397
+ align-items: center;
398
+ margin-bottom: 1rem;
399
+ max-width: 400px;
400
+ }
401
+
402
+ .searchLabel {
403
+ font-weight: 600;
404
+ margin-right: 0.5rem;
405
+ color: var(--text-light);
406
+ }
407
+
408
+ .toast {
409
+ position: fixed;
410
+ top: 20px;
411
+ right: 20px;
412
+ padding: 1rem 1.5rem;
413
+ border-radius: 4px;
414
+ color: #fff;
415
+ z-index: 10000;
416
+ box-shadow: 0 4px 12px rgba(0,0,0,0.15);
417
+ font-weight: 500;
418
+ animation: fadeIn 0.3s;
419
+ }
420
+
421
+ .toastSuccess {
422
+ background-color: var(--success-color);
423
+ }
424
+
425
+ .toastError {
426
+ background-color: var(--danger-color);
427
+ }
428
+
429
+ @keyframes fadeIn {
430
+ from { opacity: 0; transform: translateY(-10px); }
431
+ to { opacity: 1; transform: translateY(0); }
432
+ }
433
+
434
+ .checkboxContainer {
435
+ display: flex;
436
+ align-items: center;
437
+ gap: 0.5rem;
438
+ font-size: 1.1rem;
439
+ font-weight: 500;
440
+ margin: 1.5rem 0;
441
+ cursor: pointer;
442
+
443
+ input {
444
+ width: 20px;
445
+ height: 20px;
446
+ cursor: pointer;
447
+ }
448
+ }
@@ -0,0 +1,21 @@
1
+ import { Type, validators } from '@openmrs/esm-framework';
2
+
3
+ export const configSchema = {
4
+ // Add your configuration schema here
5
+ // Example:
6
+ // logo: {
7
+ // src: {
8
+ // _type: Type.String,
9
+ // _default: '',
10
+ // _description: 'The path or URL to the logo image',
11
+ // _validators: [validators.isUrl],
12
+ // },
13
+ // alt: {
14
+ // _type: Type.String,
15
+ // _default: 'Logo',
16
+ // _description: 'The alternative text for the logo image',
17
+ // },
18
+ // },
19
+ };
20
+
21
+ export type ConfigSchema = Record<string, never>;