@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.
- package/LICENSE +354 -0
- package/README.md +123 -0
- package/dist/117.js +1 -0
- package/dist/117.js.map +1 -0
- package/dist/197.js +1 -0
- package/dist/197.js.map +1 -0
- package/dist/237.js +1 -0
- package/dist/237.js.map +1 -0
- package/dist/30.js +1 -0
- package/dist/30.js.map +1 -0
- package/dist/362.js +1 -0
- package/dist/362.js.map +1 -0
- package/dist/410.js +1 -0
- package/dist/410.js.map +1 -0
- package/dist/435.js +1 -0
- package/dist/435.js.map +1 -0
- package/dist/480.js +1 -0
- package/dist/480.js.map +1 -0
- package/dist/517.js +1 -0
- package/dist/517.js.map +1 -0
- package/dist/529.js +1 -0
- package/dist/529.js.map +1 -0
- package/dist/538.js +1 -0
- package/dist/538.js.map +1 -0
- package/dist/578.js +1 -0
- package/dist/578.js.map +1 -0
- package/dist/584.js +22 -0
- package/dist/584.js.map +1 -0
- package/dist/652.js +1 -0
- package/dist/652.js.map +1 -0
- package/dist/668.js +1 -0
- package/dist/668.js.map +1 -0
- package/dist/749.js +1 -0
- package/dist/749.js.map +1 -0
- package/dist/898.js +1 -0
- package/dist/898.js.map +1 -0
- package/dist/964.js +1 -0
- package/dist/964.js.map +1 -0
- package/dist/976.js +1 -0
- package/dist/976.js.map +1 -0
- package/dist/98.js +1 -0
- package/dist/98.js.map +1 -0
- package/dist/981.js +43 -0
- package/dist/981.js.map +1 -0
- package/dist/997.js +1 -0
- package/dist/997.js.map +1 -0
- package/dist/main.js +6 -0
- package/dist/main.js.map +1 -0
- package/dist/openmrs-esm-samplecollection.js +6 -0
- package/dist/openmrs-esm-samplecollection.js.buildmanifest.json +748 -0
- package/dist/openmrs-esm-samplecollection.js.map +1 -0
- package/dist/routes.json +1 -0
- package/package.json +111 -0
- package/src/config-schema.ts +21 -0
- package/src/constants.ts +1 -0
- package/src/declarations.d.ts +23 -0
- package/src/index.ts +28 -0
- package/src/root.component.test.tsx +12 -0
- package/src/root.component.tsx +15 -0
- package/src/root.scss +5 -0
- package/src/routes.json +14 -0
- package/src/samplecollection.component.tsx +280 -0
- package/src/samplecollection.scss +163 -0
- package/src/samplecollectionroom.component.tsx +640 -0
|
@@ -0,0 +1,640 @@
|
|
|
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
|
+
TextInput,
|
|
7
|
+
Select,
|
|
8
|
+
SelectItem,
|
|
9
|
+
TextArea,
|
|
10
|
+
Button,
|
|
11
|
+
Table,
|
|
12
|
+
TableHead,
|
|
13
|
+
TableRow,
|
|
14
|
+
TableHeader,
|
|
15
|
+
TableBody,
|
|
16
|
+
TableCell,
|
|
17
|
+
Checkbox,
|
|
18
|
+
Loading,
|
|
19
|
+
InlineNotification
|
|
20
|
+
} from '@carbon/react';
|
|
21
|
+
import styles from './samplecollection.scss';
|
|
22
|
+
|
|
23
|
+
const UserAvatar = () => (
|
|
24
|
+
<svg width="100" height="100" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg" style={{ borderRadius: '50%', border: '3px solid #009384' }}>
|
|
25
|
+
<circle cx="50" cy="50" r="50" fill="#E0F2F1"/>
|
|
26
|
+
<circle cx="50" cy="40" r="18" fill="#009384"/>
|
|
27
|
+
<path d="M18 78C18 62.536 32.3269 50 50 50C67.6731 50 82 62.536 82 78" stroke="#009384" strokeWidth="8" strokeLinecap="round"/>
|
|
28
|
+
</svg>
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
const SamplecollectionRoom: React.FC = () => {
|
|
32
|
+
const { t } = useTranslation();
|
|
33
|
+
const navigate = useNavigate();
|
|
34
|
+
|
|
35
|
+
// Route state read from localstorage
|
|
36
|
+
const phnNo = localStorage.getItem('phnNo') || '';
|
|
37
|
+
const initialSection = localStorage.getItem('section') || '';
|
|
38
|
+
|
|
39
|
+
// Patient Info States
|
|
40
|
+
const [patientName, setPatientName] = useState<string>('');
|
|
41
|
+
const [patientGender, setPatientGender] = useState<string>('');
|
|
42
|
+
const [patientAge, setPatientAge] = useState<string>('');
|
|
43
|
+
const [profileImgName, setProfileImgName] = useState<string>('');
|
|
44
|
+
|
|
45
|
+
// Context & IDs
|
|
46
|
+
const [userUuid, setUserUuid] = useState<string>('');
|
|
47
|
+
const [locationUuid, setLocationUuid] = useState<string>('');
|
|
48
|
+
const [locationName, setLocationName] = useState<string>('');
|
|
49
|
+
const [poi, setPoi] = useState<string>('');
|
|
50
|
+
|
|
51
|
+
// Table Data
|
|
52
|
+
const [testList, setTestList] = useState<any[]>([]);
|
|
53
|
+
const [loading, setLoading] = useState<boolean>(true);
|
|
54
|
+
const [errorMsg, setErrorMsg] = useState<string>('');
|
|
55
|
+
const [successMsg, setSuccessMsg] = useState<string>('');
|
|
56
|
+
|
|
57
|
+
// Form selections state
|
|
58
|
+
const [selectedIds, setSelectedIds] = useState<string[]>([]);
|
|
59
|
+
const [selectedUrgents, setSelectedUrgents] = useState<string[]>([]);
|
|
60
|
+
const [selectedTestnames, setSelectedTestnames] = useState<string[]>([]);
|
|
61
|
+
|
|
62
|
+
const [displayTestNames, setDisplayTestNames] = useState<string>('');
|
|
63
|
+
const [displayOrderBy, setDisplayOrderBy] = useState<string>('');
|
|
64
|
+
const [displayOrderDate, setDisplayOrderDate] = useState<string>('');
|
|
65
|
+
const [displayOrderTime, setDisplayOrderTime] = useState<string>('');
|
|
66
|
+
|
|
67
|
+
// Save fields state
|
|
68
|
+
const [saveDate, setSaveDate] = useState<string>('');
|
|
69
|
+
const [saveTime, setSaveTime] = useState<string>('');
|
|
70
|
+
const [saveStatus, setSaveStatus] = useState<string>('Done');
|
|
71
|
+
const [saveRemark, setSaveRemark] = useState<string>('');
|
|
72
|
+
const [saving, setSaving] = useState<boolean>(false);
|
|
73
|
+
|
|
74
|
+
// Pad number helper
|
|
75
|
+
const padNumber = (number: number, length: number) => {
|
|
76
|
+
return number.toString().padStart(length, '0');
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
// Get current date string formatted as YYMMDDHHmmss
|
|
80
|
+
const getCurrentDateFormatted = () => {
|
|
81
|
+
const now = new Date();
|
|
82
|
+
const year = now.getFullYear().toString().substr(-2);
|
|
83
|
+
const month = padNumber(now.getMonth() + 1, 2);
|
|
84
|
+
const day = padNumber(now.getDate(), 2);
|
|
85
|
+
const hours = padNumber(now.getHours(), 2);
|
|
86
|
+
const minutes = padNumber(now.getMinutes(), 2);
|
|
87
|
+
const seconds = padNumber(now.getSeconds(), 2);
|
|
88
|
+
return year + month + day + hours + minutes + seconds;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
// Age calculation helper
|
|
92
|
+
const calculateAge = (birthdateString: string) => {
|
|
93
|
+
if (!birthdateString) return '';
|
|
94
|
+
const now = new Date();
|
|
95
|
+
const dob = new Date(birthdateString);
|
|
96
|
+
let age = now.getFullYear() - dob.getFullYear();
|
|
97
|
+
const monthDiff = now.getMonth() - dob.getMonth();
|
|
98
|
+
if (monthDiff < 0 || (monthDiff === 0 && now.getDate() < dob.getDate())) {
|
|
99
|
+
age--;
|
|
100
|
+
}
|
|
101
|
+
return age.toString();
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
// Set default values for Date and Time
|
|
105
|
+
useEffect(() => {
|
|
106
|
+
const today = new Date();
|
|
107
|
+
const formattedDate = `${today.getFullYear()}-${padNumber(today.getMonth() + 1, 2)}-${padNumber(today.getDate(), 2)}`;
|
|
108
|
+
const formattedTime = `${padNumber(today.getHours(), 2)}:${padNumber(today.getMinutes(), 2)}`;
|
|
109
|
+
|
|
110
|
+
setSaveDate(formattedDate);
|
|
111
|
+
setSaveTime(formattedTime);
|
|
112
|
+
}, []);
|
|
113
|
+
|
|
114
|
+
// Fetch session, location info and patient profile info
|
|
115
|
+
useEffect(() => {
|
|
116
|
+
if (!phnNo) {
|
|
117
|
+
setErrorMsg('No patient PHN provided');
|
|
118
|
+
setLoading(false);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async function initContextAndPatient() {
|
|
123
|
+
try {
|
|
124
|
+
// Load session
|
|
125
|
+
const sessionRes = await fetch('/openmrs/ws/rest/v1/session');
|
|
126
|
+
if (!sessionRes.ok) throw new Error('Session request failed');
|
|
127
|
+
const sessionData = await sessionRes.json();
|
|
128
|
+
setUserUuid(sessionData.user.uuid);
|
|
129
|
+
const personUuid = sessionData.user.person.uuid;
|
|
130
|
+
|
|
131
|
+
// Load user person attributes to get clinic location
|
|
132
|
+
const personAttrRes = await fetch(`/openmrs/ws/rest/v1/person/${personUuid}`);
|
|
133
|
+
if (!personAttrRes.ok) throw new Error('User person details failed');
|
|
134
|
+
const personAttrData = await personAttrRes.json();
|
|
135
|
+
|
|
136
|
+
let locUuid = '';
|
|
137
|
+
if (personAttrData.attributes) {
|
|
138
|
+
personAttrData.attributes.forEach((attr: any) => {
|
|
139
|
+
if (attr.display) {
|
|
140
|
+
const [key, value] = attr.display.split('=');
|
|
141
|
+
if (key && value && key.trim() === 'Institute Id') {
|
|
142
|
+
locUuid = value.trim();
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (locUuid) {
|
|
149
|
+
setLocationUuid(locUuid);
|
|
150
|
+
|
|
151
|
+
// Get Location Details (for Name & POI)
|
|
152
|
+
const locRes = await fetch(`/openmrs/ws/rest/v1/location/${locUuid}`);
|
|
153
|
+
if (locRes.ok) {
|
|
154
|
+
const locData = await locRes.json();
|
|
155
|
+
setLocationName(locData.display || '');
|
|
156
|
+
|
|
157
|
+
if (locData.attributes) {
|
|
158
|
+
locData.attributes.forEach((attr: any) => {
|
|
159
|
+
if (attr.display && attr.display.includes('POI')) {
|
|
160
|
+
const val = attr.display.split(':')[1];
|
|
161
|
+
if (val) setPoi(val.split(',')[0].trim());
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Fetch patient profile details
|
|
169
|
+
const patientRes = await fetch(`/openmrs/ws/rest/v1/patientexamination/phn/${phnNo}`);
|
|
170
|
+
if (!patientRes.ok) throw new Error('Patient not found');
|
|
171
|
+
const patientData = await patientRes.json();
|
|
172
|
+
|
|
173
|
+
setPatientName(patientData.person_id?.person?.display || '');
|
|
174
|
+
setPatientGender(patientData.person_id?.person?.gender || '');
|
|
175
|
+
if (patientData.person_id?.person?.birthdate) {
|
|
176
|
+
setPatientAge(calculateAge(patientData.person_id.person.birthdate));
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Check for ProfileImage attribute
|
|
180
|
+
if (patientData.person_id?.person?.attributes) {
|
|
181
|
+
patientData.person_id.person.attributes.forEach((attr: any) => {
|
|
182
|
+
if (attr.display && attr.display.split('=')[0].trim() === 'ProfileImage') {
|
|
183
|
+
setProfileImgName(attr.display.split('=')[1].trim());
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Fetch active test details for checkoff
|
|
189
|
+
if (locUuid) {
|
|
190
|
+
const loadViewRes = await fetch(`/openmrs/ws/rest/v1/samplecollection/loadview/${phnNo}/${initialSection}/${locUuid}`);
|
|
191
|
+
if (loadViewRes.ok) {
|
|
192
|
+
const loadViewData = await loadViewRes.json();
|
|
193
|
+
setTestList(loadViewData || []);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
} catch (err: any) {
|
|
197
|
+
console.error('Error loading page context details:', err);
|
|
198
|
+
setErrorMsg(err.message || 'Error occurred during data load');
|
|
199
|
+
} finally {
|
|
200
|
+
setLoading(false);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
initContextAndPatient();
|
|
205
|
+
}, [phnNo, initialSection]);
|
|
206
|
+
|
|
207
|
+
// Handle row checkoff selections
|
|
208
|
+
const handleCheckboxChange = (checked: boolean, id: string, testName: string, urgent: string, row: any) => {
|
|
209
|
+
let nextIds = [...selectedIds];
|
|
210
|
+
let nextTestnames = [...selectedTestnames];
|
|
211
|
+
let nextUrgents = [...selectedUrgents];
|
|
212
|
+
|
|
213
|
+
if (checked) {
|
|
214
|
+
nextIds.push(id);
|
|
215
|
+
nextTestnames.push(testName);
|
|
216
|
+
nextUrgents.push(urgent);
|
|
217
|
+
} else {
|
|
218
|
+
nextIds = nextIds.filter((item) => item !== id);
|
|
219
|
+
nextTestnames = nextTestnames.filter((item) => item !== testName);
|
|
220
|
+
nextUrgents = nextUrgents.filter((item) => item !== urgent);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
setSelectedIds(nextIds);
|
|
224
|
+
setSelectedTestnames(nextTestnames);
|
|
225
|
+
setSelectedUrgents(nextUrgents);
|
|
226
|
+
|
|
227
|
+
// Update form view fields
|
|
228
|
+
setDisplayTestNames(nextTestnames.join(', '));
|
|
229
|
+
|
|
230
|
+
if (nextIds.length === 0) {
|
|
231
|
+
setDisplayOrderBy('');
|
|
232
|
+
setDisplayOrderDate('');
|
|
233
|
+
setDisplayOrderTime('');
|
|
234
|
+
} else {
|
|
235
|
+
// Set to the values of the last checked row
|
|
236
|
+
const orderby = row[5] || '';
|
|
237
|
+
const timestamp = row[6] || '';
|
|
238
|
+
const [date, time] = timestamp.split(' ');
|
|
239
|
+
setDisplayOrderBy(orderby);
|
|
240
|
+
setDisplayOrderDate(date || '');
|
|
241
|
+
setDisplayOrderTime(time || '');
|
|
242
|
+
}
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
const handleSave = async () => {
|
|
246
|
+
if (saveStatus === '0') {
|
|
247
|
+
setErrorMsg('Please select a valid Collection Status');
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
if (selectedIds.length === 0) {
|
|
251
|
+
setErrorMsg('Please select at least one test to process');
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
if (!saveDate || !saveTime) {
|
|
255
|
+
setErrorMsg('Please specify both Date and Time of collection');
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
if (!userUuid) {
|
|
259
|
+
setErrorMsg('Invalid session. Please login again.');
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
setSaving(true);
|
|
264
|
+
setErrorMsg('');
|
|
265
|
+
setSuccessMsg('');
|
|
266
|
+
|
|
267
|
+
try {
|
|
268
|
+
if (saveStatus === 'Not Done') {
|
|
269
|
+
// Ported JSP behavior: Just show success and redirect/reload
|
|
270
|
+
setSuccessMsg('Status saved successfully as Not Done!');
|
|
271
|
+
setTimeout(() => {
|
|
272
|
+
navigate('/samplecollection');
|
|
273
|
+
}, 1500);
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Generate Barcode
|
|
278
|
+
const currentDate = getCurrentDateFormatted();
|
|
279
|
+
let hasUrgent = selectedUrgents.some((u) => u === 'true');
|
|
280
|
+
const generatedBarcode = hasUrgent ? `*${currentDate}${poi}` : `${currentDate}${poi}`;
|
|
281
|
+
|
|
282
|
+
// Progress string
|
|
283
|
+
const progress = saveStatus === 'Done' ? 'samplecollected' : 'samplenotcollected';
|
|
284
|
+
|
|
285
|
+
// Save Endpoint depending on section
|
|
286
|
+
const sectionType = initialSection === 'OPD' ? 'OPD' : initialSection === 'Clinic' ? 'Clinic' : 'Ward';
|
|
287
|
+
const saveEndpoint = {
|
|
288
|
+
OPD: '/openmrs/ws/rest/v1/samplecollection/labtestssave',
|
|
289
|
+
Clinic: '/openmrs/ws/rest/v1/samplecollection/labtestssaveclinic',
|
|
290
|
+
Ward: '/openmrs/ws/rest/v1/samplecollection/labtestssaveward',
|
|
291
|
+
}[sectionType];
|
|
292
|
+
|
|
293
|
+
// Send requests sequentially/parallelly for each selected ID
|
|
294
|
+
for (const id of selectedIds) {
|
|
295
|
+
const payload = {
|
|
296
|
+
id: id,
|
|
297
|
+
collectionstatus: saveStatus,
|
|
298
|
+
remark: saveRemark,
|
|
299
|
+
barcodenumber: generatedBarcode,
|
|
300
|
+
collectiondate: saveDate,
|
|
301
|
+
collectiontime: saveTime,
|
|
302
|
+
collectioncby: userUuid,
|
|
303
|
+
status: 'processing',
|
|
304
|
+
progress: progress,
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
const response = await fetch(saveEndpoint, {
|
|
308
|
+
method: 'POST',
|
|
309
|
+
body: JSON.stringify(payload),
|
|
310
|
+
headers: { 'Content-Type': 'application/json' },
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
if (!response.ok) {
|
|
314
|
+
throw new Error(`Failed to save test record for ID: ${id}`);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
setSuccessMsg('Sample status updated successfully! Preparing barcode print...');
|
|
319
|
+
|
|
320
|
+
// Fetch shortcodes for print labels
|
|
321
|
+
const shortcodes: string[] = [];
|
|
322
|
+
for (const test of selectedTestnames) {
|
|
323
|
+
const res = await fetch(`/openmrs/ws/rest/v1/samplecollection/getshortcode/${encodeURIComponent(test)}`);
|
|
324
|
+
if (res.ok) {
|
|
325
|
+
const shortcodeData = await res.json();
|
|
326
|
+
if (shortcodeData?.shortcode) {
|
|
327
|
+
shortcodes.push(shortcodeData.shortcode);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const combinedShortcodes = shortcodes.join(', ');
|
|
333
|
+
|
|
334
|
+
// Launch Barcode Print popup matching the team's dimensions
|
|
335
|
+
const printWindow = window.open('', '_blank');
|
|
336
|
+
if (printWindow) {
|
|
337
|
+
printWindow.document.write(`
|
|
338
|
+
<!DOCTYPE html>
|
|
339
|
+
<html>
|
|
340
|
+
<head>
|
|
341
|
+
<title>Barcode Print</title>
|
|
342
|
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/jsbarcode/3.11.5/JsBarcode.all.min.js"></script>
|
|
343
|
+
<style>
|
|
344
|
+
body {
|
|
345
|
+
margin: 0;
|
|
346
|
+
padding: 0;
|
|
347
|
+
text-align: center;
|
|
348
|
+
}
|
|
349
|
+
.container {
|
|
350
|
+
width: 1.96in; /* 5cm */
|
|
351
|
+
height: 0.98in; /* 2.5cm */
|
|
352
|
+
margin: 0 auto;
|
|
353
|
+
position: relative;
|
|
354
|
+
}
|
|
355
|
+
svg {
|
|
356
|
+
width: 100%;
|
|
357
|
+
height: 80%;
|
|
358
|
+
}
|
|
359
|
+
.shortcode {
|
|
360
|
+
font-family: Arial, sans-serif;
|
|
361
|
+
font-size: 10px;
|
|
362
|
+
margin-top: -14px;
|
|
363
|
+
}
|
|
364
|
+
@media print {
|
|
365
|
+
body { margin: 0; padding: 0; }
|
|
366
|
+
}
|
|
367
|
+
</style>
|
|
368
|
+
</head>
|
|
369
|
+
<body>
|
|
370
|
+
<div class="container">
|
|
371
|
+
<svg id="barcode"></svg>
|
|
372
|
+
<div class="shortcode">${patientName} (${patientAge}Y)</div>
|
|
373
|
+
<div style="font-size: 10px; font-family: Arial, sans-serif;">${initialSection}/${combinedShortcodes}</div>
|
|
374
|
+
</div>
|
|
375
|
+
<script>
|
|
376
|
+
document.addEventListener("DOMContentLoaded", function() {
|
|
377
|
+
JsBarcode("#barcode", "${generatedBarcode}", {
|
|
378
|
+
width: 2,
|
|
379
|
+
height: 50,
|
|
380
|
+
displayValue: true,
|
|
381
|
+
fontSize: 14,
|
|
382
|
+
margin: 5
|
|
383
|
+
});
|
|
384
|
+
setTimeout(function() {
|
|
385
|
+
window.print();
|
|
386
|
+
window.close();
|
|
387
|
+
}, 800);
|
|
388
|
+
});
|
|
389
|
+
</script>
|
|
390
|
+
</body>
|
|
391
|
+
</html>
|
|
392
|
+
`);
|
|
393
|
+
printWindow.document.close();
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// Clear selections and reload table data
|
|
397
|
+
setSelectedIds([]);
|
|
398
|
+
setSelectedTestnames([]);
|
|
399
|
+
setSelectedUrgents([]);
|
|
400
|
+
setDisplayTestNames('');
|
|
401
|
+
setDisplayOrderBy('');
|
|
402
|
+
setDisplayOrderDate('');
|
|
403
|
+
setDisplayOrderTime('');
|
|
404
|
+
|
|
405
|
+
// Refetch tests
|
|
406
|
+
const loadViewRes = await fetch(`/openmrs/ws/rest/v1/samplecollection/loadview/${phnNo}/${initialSection}/${locationUuid}`);
|
|
407
|
+
if (loadViewRes.ok) {
|
|
408
|
+
const loadViewData = await loadViewRes.json();
|
|
409
|
+
setTestList(loadViewData || []);
|
|
410
|
+
}
|
|
411
|
+
} catch (err: any) {
|
|
412
|
+
console.error('Error during save process:', err);
|
|
413
|
+
setErrorMsg(err.message || 'Error occurred saving collection data');
|
|
414
|
+
} finally {
|
|
415
|
+
setSaving(false);
|
|
416
|
+
}
|
|
417
|
+
};
|
|
418
|
+
|
|
419
|
+
if (loading) return <Loading description="Loading page details..." withOverlay={true} />;
|
|
420
|
+
|
|
421
|
+
return (
|
|
422
|
+
<div className={styles.container}>
|
|
423
|
+
<Tile className={styles.tile}>
|
|
424
|
+
{/* Header */}
|
|
425
|
+
<div className={styles.headerRow}>
|
|
426
|
+
<h1 className={styles.heading}>
|
|
427
|
+
{t('samplecollectionRoomHeading', 'Sample Collection Room')}
|
|
428
|
+
</h1>
|
|
429
|
+
<div className={styles.locationBadge}>
|
|
430
|
+
<span className="icon-hospital" />
|
|
431
|
+
<span>{locationName}</span>
|
|
432
|
+
</div>
|
|
433
|
+
</div>
|
|
434
|
+
|
|
435
|
+
{/* Notifications */}
|
|
436
|
+
{errorMsg && (
|
|
437
|
+
<InlineNotification
|
|
438
|
+
kind="error"
|
|
439
|
+
title="Error"
|
|
440
|
+
subtitle={errorMsg}
|
|
441
|
+
onClose={() => setErrorMsg('')}
|
|
442
|
+
style={{ marginBottom: '1rem' }}
|
|
443
|
+
/>
|
|
444
|
+
)}
|
|
445
|
+
{successMsg && (
|
|
446
|
+
<InlineNotification
|
|
447
|
+
kind="success"
|
|
448
|
+
title="Success"
|
|
449
|
+
subtitle={successMsg}
|
|
450
|
+
onClose={() => setSuccessMsg('')}
|
|
451
|
+
style={{ marginBottom: '1rem' }}
|
|
452
|
+
/>
|
|
453
|
+
)}
|
|
454
|
+
|
|
455
|
+
{/* Patient Profile Card */}
|
|
456
|
+
<div className={styles.patientCard}>
|
|
457
|
+
<div className={styles.patientGrid}>
|
|
458
|
+
<div className={styles.patientField}>
|
|
459
|
+
<span className={styles.fieldLabel}>Full Name</span>
|
|
460
|
+
<span className={styles.fieldValue}>{patientName || 'N/A'}</span>
|
|
461
|
+
</div>
|
|
462
|
+
<div className={styles.patientField}>
|
|
463
|
+
<span className={styles.fieldLabel}>Section</span>
|
|
464
|
+
<span className={styles.fieldValue}>{initialSection || 'N/A'}</span>
|
|
465
|
+
</div>
|
|
466
|
+
<div className={styles.patientField}>
|
|
467
|
+
<span className={styles.fieldLabel}>PHN No</span>
|
|
468
|
+
<span className={styles.fieldValue}>{phnNo || 'N/A'}</span>
|
|
469
|
+
</div>
|
|
470
|
+
<div className={styles.patientField}>
|
|
471
|
+
<span className={styles.fieldLabel}>Age</span>
|
|
472
|
+
<span className={styles.fieldValue}>{patientAge ? `${patientAge} years` : 'N/A'}</span>
|
|
473
|
+
</div>
|
|
474
|
+
<div className={styles.patientField}>
|
|
475
|
+
<span className={styles.fieldLabel}>Gender</span>
|
|
476
|
+
<span className={styles.fieldValue}>{patientGender || 'N/A'}</span>
|
|
477
|
+
</div>
|
|
478
|
+
</div>
|
|
479
|
+
<div className={styles.avatarContainer}>
|
|
480
|
+
{profileImgName ? (
|
|
481
|
+
<img
|
|
482
|
+
src={`${window.location.origin}/openmrs/owa/PatientImagesUploaded/${profileImgName}`}
|
|
483
|
+
alt="Patient profile"
|
|
484
|
+
className={styles.avatarImg}
|
|
485
|
+
onError={(e) => {
|
|
486
|
+
// Fallback if image not found
|
|
487
|
+
(e.target as HTMLElement).style.display = 'none';
|
|
488
|
+
}}
|
|
489
|
+
/>
|
|
490
|
+
) : (
|
|
491
|
+
<UserAvatar />
|
|
492
|
+
)}
|
|
493
|
+
</div>
|
|
494
|
+
</div>
|
|
495
|
+
|
|
496
|
+
{/* Tests List Table */}
|
|
497
|
+
<div style={{ maxHeight: '250px', overflowY: 'auto', marginBottom: '2rem' }}>
|
|
498
|
+
<Table>
|
|
499
|
+
<TableHead>
|
|
500
|
+
<TableRow>
|
|
501
|
+
<TableHeader>No</TableHeader>
|
|
502
|
+
<TableHeader>Test Name</TableHeader>
|
|
503
|
+
<TableHeader>Order By</TableHeader>
|
|
504
|
+
<TableHeader>Order Date/Time</TableHeader>
|
|
505
|
+
<TableHeader className={styles.checkboxCell} />
|
|
506
|
+
</TableRow>
|
|
507
|
+
</TableHead>
|
|
508
|
+
<TableBody>
|
|
509
|
+
{testList.length === 0 ? (
|
|
510
|
+
<TableRow>
|
|
511
|
+
<TableCell colSpan={5} style={{ textAlign: 'center' }}>
|
|
512
|
+
No active tests pending collection.
|
|
513
|
+
</TableCell>
|
|
514
|
+
</TableRow>
|
|
515
|
+
) : (
|
|
516
|
+
testList.map((row, index) => {
|
|
517
|
+
const id = row[8];
|
|
518
|
+
const testName = row[2];
|
|
519
|
+
const orderBy = row[5];
|
|
520
|
+
const orderDateTime = row[6];
|
|
521
|
+
const urgent = row[9]; // 'true' or 'false'
|
|
522
|
+
const isChecked = selectedIds.includes(String(id));
|
|
523
|
+
|
|
524
|
+
return (
|
|
525
|
+
<TableRow key={id || index}>
|
|
526
|
+
<TableCell>{index + 1}</TableCell>
|
|
527
|
+
<TableCell>{testName}</TableCell>
|
|
528
|
+
<TableCell>{orderBy}</TableCell>
|
|
529
|
+
<TableCell>{orderDateTime}</TableCell>
|
|
530
|
+
<TableCell className={styles.checkboxCell}>
|
|
531
|
+
<Checkbox
|
|
532
|
+
id={`chk-${id}`}
|
|
533
|
+
labelText=""
|
|
534
|
+
checked={isChecked}
|
|
535
|
+
onChange={(e, { checked }) =>
|
|
536
|
+
handleCheckboxChange(checked, String(id), testName, String(urgent), row)
|
|
537
|
+
}
|
|
538
|
+
/>
|
|
539
|
+
</TableCell>
|
|
540
|
+
</TableRow>
|
|
541
|
+
);
|
|
542
|
+
})
|
|
543
|
+
)}
|
|
544
|
+
</TableBody>
|
|
545
|
+
</Table>
|
|
546
|
+
</div>
|
|
547
|
+
|
|
548
|
+
{/* Detail Panel */}
|
|
549
|
+
<div className={styles.roomFormSection}>
|
|
550
|
+
<div className={styles.formGrid}>
|
|
551
|
+
<TextInput
|
|
552
|
+
id="txttestname"
|
|
553
|
+
labelText="Test Name"
|
|
554
|
+
value={displayTestNames}
|
|
555
|
+
readOnly
|
|
556
|
+
/>
|
|
557
|
+
<TextInput
|
|
558
|
+
id="txtorderby"
|
|
559
|
+
labelText="Order By"
|
|
560
|
+
value={displayOrderBy}
|
|
561
|
+
readOnly
|
|
562
|
+
/>
|
|
563
|
+
<TextInput
|
|
564
|
+
id="txtorderdate"
|
|
565
|
+
labelText="Order Date"
|
|
566
|
+
value={displayOrderDate}
|
|
567
|
+
readOnly
|
|
568
|
+
/>
|
|
569
|
+
<TextInput
|
|
570
|
+
id="txtordertime"
|
|
571
|
+
labelText="Order Time"
|
|
572
|
+
value={displayOrderTime}
|
|
573
|
+
readOnly
|
|
574
|
+
/>
|
|
575
|
+
</div>
|
|
576
|
+
|
|
577
|
+
<hr style={{ border: 'none', height: '1px', backgroundColor: '#e0e0e0', margin: '20px 0' }} />
|
|
578
|
+
|
|
579
|
+
{/* User inputs */}
|
|
580
|
+
<div className={styles.formGrid}>
|
|
581
|
+
<TextInput
|
|
582
|
+
id="txtdate"
|
|
583
|
+
labelText="Date"
|
|
584
|
+
type="date"
|
|
585
|
+
value={saveDate}
|
|
586
|
+
onChange={(e) => setSaveDate(e.target.value)}
|
|
587
|
+
/>
|
|
588
|
+
<TextInput
|
|
589
|
+
id="txttime"
|
|
590
|
+
labelText="Time"
|
|
591
|
+
type="time"
|
|
592
|
+
value={saveTime}
|
|
593
|
+
onChange={(e) => setSaveTime(e.target.value)}
|
|
594
|
+
/>
|
|
595
|
+
<Select
|
|
596
|
+
id="drpstatus"
|
|
597
|
+
labelText="Collection Status"
|
|
598
|
+
value={saveStatus}
|
|
599
|
+
onChange={(e) => setSaveStatus(e.target.value)}
|
|
600
|
+
>
|
|
601
|
+
<SelectItem value="0" text="--Select--" />
|
|
602
|
+
<SelectItem value="Done" text="Done" />
|
|
603
|
+
<SelectItem value="Not Done" text="Not Done" />
|
|
604
|
+
</Select>
|
|
605
|
+
</div>
|
|
606
|
+
|
|
607
|
+
<div style={{ marginBottom: '2rem' }}>
|
|
608
|
+
<TextArea
|
|
609
|
+
id="txtnote"
|
|
610
|
+
labelText="Remark"
|
|
611
|
+
rows={3}
|
|
612
|
+
value={saveRemark}
|
|
613
|
+
onChange={(e) => setSaveRemark(e.target.value)}
|
|
614
|
+
/>
|
|
615
|
+
</div>
|
|
616
|
+
|
|
617
|
+
{/* Action buttons */}
|
|
618
|
+
<div className={styles.actionsContainer}>
|
|
619
|
+
<Button
|
|
620
|
+
kind="danger"
|
|
621
|
+
onClick={() => navigate('/samplecollection')}
|
|
622
|
+
disabled={saving}
|
|
623
|
+
>
|
|
624
|
+
Back
|
|
625
|
+
</Button>
|
|
626
|
+
<Button
|
|
627
|
+
kind="primary"
|
|
628
|
+
onClick={handleSave}
|
|
629
|
+
disabled={saving || testList.length === 0}
|
|
630
|
+
>
|
|
631
|
+
{saving ? 'Saving...' : 'Save'}
|
|
632
|
+
</Button>
|
|
633
|
+
</div>
|
|
634
|
+
</div>
|
|
635
|
+
</Tile>
|
|
636
|
+
</div>
|
|
637
|
+
);
|
|
638
|
+
};
|
|
639
|
+
|
|
640
|
+
export default SamplecollectionRoom;
|