@sasindi/samplecollection 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 (64) hide show
  1. package/LICENSE +354 -0
  2. package/README.md +123 -0
  3. package/dist/117.js +1 -0
  4. package/dist/117.js.map +1 -0
  5. package/dist/197.js +1 -0
  6. package/dist/197.js.map +1 -0
  7. package/dist/237.js +1 -0
  8. package/dist/237.js.map +1 -0
  9. package/dist/30.js +1 -0
  10. package/dist/30.js.map +1 -0
  11. package/dist/362.js +1 -0
  12. package/dist/362.js.map +1 -0
  13. package/dist/410.js +1 -0
  14. package/dist/410.js.map +1 -0
  15. package/dist/435.js +1 -0
  16. package/dist/435.js.map +1 -0
  17. package/dist/480.js +1 -0
  18. package/dist/480.js.map +1 -0
  19. package/dist/517.js +1 -0
  20. package/dist/517.js.map +1 -0
  21. package/dist/529.js +1 -0
  22. package/dist/529.js.map +1 -0
  23. package/dist/538.js +1 -0
  24. package/dist/538.js.map +1 -0
  25. package/dist/578.js +1 -0
  26. package/dist/578.js.map +1 -0
  27. package/dist/584.js +22 -0
  28. package/dist/584.js.map +1 -0
  29. package/dist/652.js +1 -0
  30. package/dist/652.js.map +1 -0
  31. package/dist/668.js +1 -0
  32. package/dist/668.js.map +1 -0
  33. package/dist/749.js +1 -0
  34. package/dist/749.js.map +1 -0
  35. package/dist/898.js +1 -0
  36. package/dist/898.js.map +1 -0
  37. package/dist/964.js +1 -0
  38. package/dist/964.js.map +1 -0
  39. package/dist/976.js +1 -0
  40. package/dist/976.js.map +1 -0
  41. package/dist/98.js +1 -0
  42. package/dist/98.js.map +1 -0
  43. package/dist/981.js +43 -0
  44. package/dist/981.js.map +1 -0
  45. package/dist/997.js +1 -0
  46. package/dist/997.js.map +1 -0
  47. package/dist/main.js +6 -0
  48. package/dist/main.js.map +1 -0
  49. package/dist/openmrs-esm-samplecollection.js +6 -0
  50. package/dist/openmrs-esm-samplecollection.js.buildmanifest.json +748 -0
  51. package/dist/openmrs-esm-samplecollection.js.map +1 -0
  52. package/dist/routes.json +1 -0
  53. package/package.json +111 -0
  54. package/src/config-schema.ts +21 -0
  55. package/src/constants.ts +1 -0
  56. package/src/declarations.d.ts +23 -0
  57. package/src/index.ts +28 -0
  58. package/src/root.component.test.tsx +12 -0
  59. package/src/root.component.tsx +15 -0
  60. package/src/root.scss +5 -0
  61. package/src/routes.json +14 -0
  62. package/src/samplecollection.component.tsx +280 -0
  63. package/src/samplecollection.scss +163 -0
  64. package/src/samplecollectionroom.component.tsx +640 -0
@@ -0,0 +1,280 @@
1
+ import React, { useState, useEffect } from 'react';
2
+ import { useNavigate } from 'react-router-dom';
3
+ import { useTranslation } from 'react-i18next';
4
+ import {
5
+ Tile,
6
+ Select,
7
+ SelectItem,
8
+ TextInput,
9
+ Table,
10
+ TableHead,
11
+ TableRow,
12
+ TableHeader,
13
+ TableBody,
14
+ TableCell,
15
+ Button,
16
+ Pagination,
17
+ Loading
18
+ } from '@carbon/react';
19
+ import styles from './samplecollection.scss';
20
+
21
+ const SearchIcon = () => (
22
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
23
+ <path d="M15 14.3l-3.3-3.3c.9-1.1 1.4-2.5 1.4-4 0-3.3-2.7-6-6-6S1.1 3.7 1.1 7s2.7 6 6 6c1.5 0 2.9-.5 4-1.4l3.3 3.3.6-.6zM7.1 12c-2.8 0-5-2.2-5-5s2.2-5 5-5 5 2.2 5 5-2.2 5-5 5z" />
24
+ </svg>
25
+ );
26
+
27
+ const Samplecollection: React.FC = () => {
28
+ const { t } = useTranslation();
29
+ const navigate = useNavigate();
30
+
31
+ // State variables
32
+ const [locationUuid, setLocationUuid] = useState<string>('');
33
+ const [locationName, setLocationName] = useState<string>('Loading location...');
34
+ const [wardSections, setWardSections] = useState<string[]>([]);
35
+ const [sectionFilter, setSectionFilter] = useState<string>('ALL');
36
+ const [searchQuery, setSearchQuery] = useState<string>('');
37
+ const [data, setData] = useState<any[]>([]);
38
+ const [loading, setLoading] = useState<boolean>(true);
39
+
40
+ // Pagination state
41
+ const [page, setPage] = useState<number>(1);
42
+ const [pageSize, setPageSize] = useState<number>(50);
43
+ const [totalRecords, setTotalRecords] = useState<number>(0);
44
+ const [filteredRecords, setFilteredRecords] = useState<number>(0);
45
+
46
+ // Load Session and Location context
47
+ useEffect(() => {
48
+ async function initSession() {
49
+ try {
50
+ const sessionRes = await fetch('/openmrs/ws/rest/v1/session');
51
+ if (!sessionRes.ok) throw new Error('Session request failed');
52
+ const sessionData = await sessionRes.json();
53
+ const personUuid = sessionData.user.person.uuid;
54
+
55
+ const personRes = await fetch(`/openmrs/ws/rest/v1/person/${personUuid}`);
56
+ if (!personRes.ok) throw new Error('Person details request failed');
57
+ const personData = await personRes.json();
58
+
59
+ let locUuid = '';
60
+ if (personData.attributes) {
61
+ personData.attributes.forEach((attr: any) => {
62
+ if (attr.display) {
63
+ const [key, value] = attr.display.split('=');
64
+ if (key && value && key.trim() === 'Institute Id') {
65
+ locUuid = value.trim();
66
+ }
67
+ }
68
+ });
69
+ }
70
+
71
+ if (locUuid) {
72
+ setLocationUuid(locUuid);
73
+
74
+ // Get Location Details
75
+ const locRes = await fetch(`/openmrs/ws/rest/v1/location/${locUuid}`);
76
+ if (locRes.ok) {
77
+ const locData = await locRes.json();
78
+ setLocationName(locData.display || 'OpenMRS Clinic');
79
+ }
80
+
81
+ // Load Ward Sections
82
+ const wardRes = await fetch(`/openmrs/ws/rest/v1/samplecollection/getwardsections/${locUuid}`);
83
+ if (wardRes.ok) {
84
+ const wardData = await wardRes.json();
85
+ const sections = (wardData.wardSections || []).map((name: string) => name.split('@@')[0]);
86
+ setWardSections(sections);
87
+ }
88
+ } else {
89
+ setLocationName('No clinic location assigned');
90
+ setLoading(false);
91
+ }
92
+ } catch (err) {
93
+ console.error('Error during context initialization:', err);
94
+ setLocationName('Error loading location');
95
+ setLoading(false);
96
+ }
97
+ }
98
+
99
+ initSession();
100
+ }, []);
101
+
102
+ // Fetch serverside table data when location, filter, search, page or pageSize changes
103
+ useEffect(() => {
104
+ if (!locationUuid) return;
105
+
106
+ async function fetchTableData() {
107
+ setLoading(true);
108
+ try {
109
+ const start = (page - 1) * pageSize;
110
+ const searchVal = encodeURIComponent(searchQuery);
111
+ const sectionVal = encodeURIComponent(sectionFilter);
112
+
113
+ const url = `/openmrs/ws/rest/v1/samplecollection/loadtable/serverside/${locationUuid}?draw=${page}&start=${start}&length=${pageSize}&search[value]=${searchVal}&section=${sectionVal}`;
114
+ const response = await fetch(url);
115
+ if (!response.ok) throw new Error('Failed to load table data');
116
+ const resData = await response.json();
117
+
118
+ setData(resData.data || []);
119
+ setTotalRecords(resData.recordsTotal || 0);
120
+ setFilteredRecords(resData.recordsFiltered || 0);
121
+ } catch (err) {
122
+ console.error('Error fetching table data:', err);
123
+ } finally {
124
+ setLoading(false);
125
+ }
126
+ }
127
+
128
+ // Debounce data fetching slightly if search query is changing rapidly
129
+ const delayDebounceFn = setTimeout(() => {
130
+ fetchTableData();
131
+ }, 300);
132
+
133
+ return () => clearTimeout(delayDebounceFn);
134
+ }, [locationUuid, sectionFilter, searchQuery, page, pageSize]);
135
+
136
+ const handleSelectPatient = (phnNo: string, section: string, progress: string) => {
137
+ localStorage.setItem('phnNo', phnNo);
138
+ localStorage.setItem('section', section);
139
+ localStorage.setItem('progress', progress || '');
140
+ navigate('/samplecollection/room');
141
+ };
142
+
143
+ return (
144
+ <div className={styles.container}>
145
+ <Tile className={styles.tile}>
146
+ {/* Header Section */}
147
+ <div className={styles.headerRow}>
148
+ <h1 className={styles.heading}>
149
+ {t('samplecollectionHeading', 'Sample Collection List')}
150
+ </h1>
151
+ <div className={styles.locationBadge}>
152
+ <span className="icon-hospital" />
153
+ <span>{locationName}</span>
154
+ </div>
155
+ </div>
156
+
157
+ {/* Filter and Search controls */}
158
+ <div className={styles.filterRow}>
159
+ <Select
160
+ id="sectionFilter"
161
+ labelText={t('sectionLabel', 'Section')}
162
+ value={sectionFilter}
163
+ onChange={(e) => {
164
+ setSectionFilter(e.target.value);
165
+ setPage(1); // Reset page on filter change
166
+ }}
167
+ style={{ width: '200px' }}
168
+ >
169
+ <SelectItem value="ALL" text={t('allSections', 'All')} />
170
+ <SelectItem value="OPD" text="OPD" />
171
+ <SelectItem value="Clinic" text="Clinic" />
172
+ {wardSections.map((sect) => (
173
+ <SelectItem key={sect} value={sect} text={sect} />
174
+ ))}
175
+ </Select>
176
+
177
+ <TextInput
178
+ id="searchQuery"
179
+ labelText={t('searchLabel', 'Search (PHN, Name, Test)')}
180
+ placeholder={t('searchPlaceholder', 'Type to search...')}
181
+ value={searchQuery}
182
+ onChange={(e) => {
183
+ setSearchQuery(e.target.value);
184
+ setPage(1); // Reset page on search
185
+ }}
186
+ />
187
+ </div>
188
+
189
+ {/* Loading Indicator */}
190
+ {loading && <Loading description="Loading data..." withOverlay={false} />}
191
+
192
+ {/* Main Table */}
193
+ <div className={styles.tableContainer}>
194
+ <Table>
195
+ <TableHead>
196
+ <TableRow>
197
+ <TableHeader>No</TableHeader>
198
+ <TableHeader>Section</TableHeader>
199
+ <TableHeader>PHN No</TableHeader>
200
+ <TableHeader>Name</TableHeader>
201
+ <TableHeader>Gender</TableHeader>
202
+ <TableHeader>Age</TableHeader>
203
+ <TableHeader>Test Name</TableHeader>
204
+ <TableHeader />
205
+ </TableRow>
206
+ </TableHead>
207
+ <TableBody>
208
+ {data.length === 0 ? (
209
+ <TableRow>
210
+ <TableCell colSpan={8} style={{ textAlign: 'center', padding: '2rem' }}>
211
+ {loading ? 'Fetching data...' : 'No sample collections found.'}
212
+ </TableCell>
213
+ </TableRow>
214
+ ) : (
215
+ data.map((row, index) => {
216
+ const phnNo = row[0];
217
+ const patientName = row[1];
218
+ const testName = row[2];
219
+ const gender = row[3];
220
+ const age = row[4];
221
+ const section = row[7];
222
+ const specimenStatus = row[8]; // 'Inadequate', 'Error'
223
+ const progress = row[9];
224
+ const rowIndex = (page - 1) * pageSize + index + 1;
225
+
226
+ let trClass = '';
227
+ if (specimenStatus === 'Inadequate') {
228
+ trClass = styles.rowInadequate;
229
+ } else if (specimenStatus === 'Error') {
230
+ trClass = styles.rowError;
231
+ }
232
+
233
+ return (
234
+ <TableRow key={`${phnNo}-${section}-${index}`} className={trClass}>
235
+ <TableCell>{rowIndex}</TableCell>
236
+ <TableCell>{section}</TableCell>
237
+ <TableCell>{phnNo}</TableCell>
238
+ <TableCell>{patientName}</TableCell>
239
+ <TableCell>{gender}</TableCell>
240
+ <TableCell>{age}</TableCell>
241
+ <TableCell>{testName}</TableCell>
242
+ <TableCell>
243
+ <Button
244
+ kind="primary"
245
+ size="sm"
246
+ onClick={() => handleSelectPatient(phnNo, section, progress)}
247
+ >
248
+ {t('selectBtn', 'Select')}
249
+ </Button>
250
+ </TableCell>
251
+ </TableRow>
252
+ );
253
+ })
254
+ )}
255
+ </TableBody>
256
+ </Table>
257
+ </div>
258
+
259
+ {/* Pagination */}
260
+ {filteredRecords > 0 && (
261
+ <Pagination
262
+ backwardText="Previous page"
263
+ forwardText="Next page"
264
+ itemsPerPageText="Items per page:"
265
+ onChange={({ page, pageSize }) => {
266
+ setPage(page);
267
+ setPageSize(pageSize);
268
+ }}
269
+ page={page}
270
+ pageSize={pageSize}
271
+ pageSizes={[10, 25, 50, 100]}
272
+ totalItems={filteredRecords}
273
+ />
274
+ )}
275
+ </Tile>
276
+ </div>
277
+ );
278
+ };
279
+
280
+ export default Samplecollection;
@@ -0,0 +1,163 @@
1
+ @use '@carbon/layout';
2
+ @use '@carbon/type';
3
+
4
+ .container {
5
+ padding: layout.$spacing-06;
6
+ max-width: 1200px;
7
+ margin: 0 auto;
8
+ }
9
+
10
+ .tile {
11
+ padding: layout.$spacing-06;
12
+ margin-bottom: layout.$spacing-05;
13
+ background: var(--cds-layer-01, #ffffff);
14
+ border-radius: 8px;
15
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
16
+ border: 1px solid var(--cds-border-subtle-01, #e0e0e0);
17
+ }
18
+
19
+ .headerRow {
20
+ display: flex;
21
+ justify-content: space-between;
22
+ align-items: center;
23
+ margin-bottom: layout.$spacing-06;
24
+ border-bottom: 1px solid var(--cds-border-subtle-01, #e0e0e0);
25
+ padding-bottom: layout.$spacing-04;
26
+ }
27
+
28
+ .heading {
29
+ @include type.type-style('heading-04');
30
+ color: #009384;
31
+ margin: 0;
32
+ font-weight: 600;
33
+ }
34
+
35
+ .locationBadge {
36
+ display: flex;
37
+ align-items: center;
38
+ gap: layout.$spacing-03;
39
+ font-weight: 600;
40
+ color: var(--cds-text-secondary, #525252);
41
+ background: var(--cds-layer-02, #f4f4f4);
42
+ padding: layout.$spacing-02 layout.$spacing-04;
43
+ border-radius: 16px;
44
+ font-size: 14px;
45
+ }
46
+
47
+ .filterRow {
48
+ display: flex;
49
+ gap: layout.$spacing-05;
50
+ align-items: flex-end;
51
+ margin-bottom: layout.$spacing-05;
52
+ flex-wrap: wrap;
53
+ }
54
+
55
+ .tableContainer {
56
+ margin-top: layout.$spacing-05;
57
+ margin-bottom: layout.$spacing-05;
58
+ }
59
+
60
+ .rowInadequate {
61
+ background-color: #fff3cd !important;
62
+ transition: background-color 0.2s ease;
63
+ &:hover {
64
+ background-color: #ffeeb5 !important;
65
+ }
66
+ }
67
+
68
+ .rowError {
69
+ background-color: #f8d7da !important;
70
+ transition: background-color 0.2s ease;
71
+ &:hover {
72
+ background-color: #f5c2c7 !important;
73
+ }
74
+ }
75
+
76
+ /* Patient Card Info for Room view */
77
+ .patientCard {
78
+ display: grid;
79
+ grid-template-columns: 1fr 120px;
80
+ gap: layout.$spacing-05;
81
+ align-items: center;
82
+ background: var(--cds-layer-01, #ffffff);
83
+ padding: layout.$spacing-05;
84
+ border-radius: 8px;
85
+ border: 1px solid var(--cds-border-subtle-01, #e0e0e0);
86
+ margin-bottom: layout.$spacing-05;
87
+
88
+ @media (max-width: 768px) {
89
+ grid-template-columns: 1fr;
90
+ }
91
+ }
92
+
93
+ .patientGrid {
94
+ display: grid;
95
+ grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
96
+ gap: layout.$spacing-05;
97
+ }
98
+
99
+ .patientField {
100
+ display: flex;
101
+ flex-direction: column;
102
+ gap: layout.$spacing-02;
103
+ }
104
+
105
+ .fieldLabel {
106
+ font-size: 12px;
107
+ color: var(--cds-text-helper, #6f6f6f);
108
+ text-transform: uppercase;
109
+ font-weight: 600;
110
+ }
111
+
112
+ .fieldValue {
113
+ font-size: 16px;
114
+ color: var(--cds-text-primary, #161616);
115
+ font-weight: 500;
116
+ }
117
+
118
+ .avatarContainer {
119
+ display: flex;
120
+ justify-content: center;
121
+ align-items: center;
122
+ }
123
+
124
+ .avatarImg {
125
+ width: 100px;
126
+ height: 100px;
127
+ border-radius: 50%;
128
+ object-fit: cover;
129
+ border: 3px solid #009384;
130
+ }
131
+
132
+ .roomFormSection {
133
+ margin-top: layout.$spacing-06;
134
+ border-top: 1px solid var(--cds-border-subtle-01, #e0e0e0);
135
+ padding-top: layout.$spacing-06;
136
+ }
137
+
138
+ .formGrid {
139
+ display: grid;
140
+ grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
141
+ gap: layout.$spacing-06;
142
+ margin-bottom: layout.$spacing-06;
143
+ }
144
+
145
+ .actionsContainer {
146
+ display: flex;
147
+ justify-content: flex-end;
148
+ gap: layout.$spacing-04;
149
+ margin-top: layout.$spacing-06;
150
+ }
151
+
152
+ .paginationContainer {
153
+ display: flex;
154
+ justify-content: flex-end;
155
+ align-items: center;
156
+ gap: layout.$spacing-05;
157
+ margin-top: layout.$spacing-04;
158
+ }
159
+
160
+ .checkboxCell {
161
+ width: 48px;
162
+ text-align: center;
163
+ }