@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,831 @@
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 AddResultsForm: React.FC = () => {
11
+ const { t } = useTranslation();
12
+
13
+ // Route Parameters from localStorage
14
+ const phn = localStorage.getItem('phnNo') || '';
15
+ const section = localStorage.getItem('section') || '';
16
+ const barcode = localStorage.getItem('barcode') || '';
17
+ const serial = localStorage.getItem('id') || '';
18
+
19
+ // State Variables
20
+ const [patientName, setPatientName] = useState<string>('');
21
+ const [patientGender, setPatientGender] = useState<string>('');
22
+ const [patientAge, setPatientAge] = useState<string>('');
23
+ const [testName, setTestName] = useState<string>('');
24
+ const [testRemark, setTestRemark] = useState<string>('');
25
+ const [testDate, setTestDate] = useState<string>('');
26
+ const [testTime, setTestTime] = useState<string>('');
27
+
28
+ // Locations context
29
+ const [locationUuid, setLocationUuid] = useState<string>('');
30
+ const [instituteName, setInstituteName] = useState<string>('');
31
+ const [currentUserUuid, setCurrentUserUuid] = useState<string>('');
32
+
33
+ // Parameters list
34
+ const [parameters, setParameters] = useState<any[]>([]);
35
+ const [loading, setLoading] = useState<boolean>(true);
36
+ const [showSpinner, setShowSpinner] = useState<boolean>(false);
37
+ const [spinnerText, setSpinnerText] = useState<string>('Processing report...');
38
+
39
+ // Form submission and report flags
40
+ const [sendToApproval, setSendToApproval] = useState<boolean>(false);
41
+ const [flagGenerate, setFlagGenerate] = useState<boolean>(false);
42
+ const [uploadedFile, setUploadedFile] = useState<File | null>(null);
43
+
44
+ // Toast status
45
+ const [toast, setToast] = useState<{ type: 'success' | 'error'; text: string } | null>(null);
46
+
47
+ // Show toast utility
48
+ const showToast = (type: 'success' | 'error', text: string) => {
49
+ setToast({ type, text });
50
+ setTimeout(() => setToast(null), 3000);
51
+ };
52
+
53
+ // Helper date formatting
54
+ const getCurrentDateFormatted = () => {
55
+ const now = new Date();
56
+ const year = now.getFullYear();
57
+ const month = String(now.getMonth() + 1).padStart(2, '0');
58
+ const day = String(now.getDate()).padStart(2, '0');
59
+ return `${year}-${month}-${day}`;
60
+ };
61
+
62
+ const getCurrentTimeFormatted = () => {
63
+ const now = new Date();
64
+ const hours = String(now.getHours()).padStart(2, '0');
65
+ const minutes = String(now.getMinutes()).padStart(2, '0');
66
+ return `${hours}:${minutes}`;
67
+ };
68
+
69
+ // Age calculation helper
70
+ const getAge = (dateString: string) => {
71
+ if (!dateString) return '';
72
+ const today = new Date();
73
+ const birthDate = new Date(dateString);
74
+ let age = today.getFullYear() - birthDate.getFullYear();
75
+ const m = today.getMonth() - birthDate.getMonth();
76
+ if (m < 0 || (m === 0 && today.getDate() < birthDate.getDate())) {
77
+ age--;
78
+ }
79
+ return String(age);
80
+ };
81
+
82
+ // Map gender characters to full words
83
+ const formatGender = (gender: string) => {
84
+ if (!gender) return '';
85
+ const upper = gender.trim().toUpperCase();
86
+ if (upper === 'M' || upper === 'MALE') return 'Male';
87
+ if (upper === 'F' || upper === 'FEMALE') return 'Female';
88
+ return gender;
89
+ };
90
+
91
+ // 1. Initial Load: Context and Demographics
92
+ useEffect(() => {
93
+ async function loadData() {
94
+ try {
95
+ setLoading(true);
96
+
97
+ // a. Load Session/User Info
98
+ const sessionRes = await fetch('/openmrs/ws/rest/v1/session');
99
+ if (!sessionRes.ok) throw new Error('Failed to load session');
100
+ const sessionData = await sessionRes.json();
101
+ const personUuid = sessionData.user.person.uuid;
102
+ setCurrentUserUuid(sessionData.user.uuid);
103
+
104
+ // b. Load Person Attributes for Location
105
+ const personRes = await fetch(`/openmrs/ws/rest/v1/person/${personUuid}`);
106
+ if (!personRes.ok) throw new Error('Failed to load person');
107
+ const personData = await personRes.json();
108
+
109
+ let initialLocUuid = '';
110
+ if (personData.attributes) {
111
+ personData.attributes.forEach((attr: any) => {
112
+ if (attr.display) {
113
+ const [key, value] = attr.display.split('=');
114
+ if (key && value && (key.trim() === 'Institute Id' || key.trim() === 'Institute')) {
115
+ initialLocUuid = value.trim();
116
+ }
117
+ }
118
+ });
119
+ }
120
+
121
+ if (!initialLocUuid && sessionData.sessionLocation && sessionData.sessionLocation.uuid) {
122
+ initialLocUuid = sessionData.sessionLocation.uuid;
123
+ }
124
+
125
+ setLocationUuid(initialLocUuid);
126
+
127
+ // c. Load Location Display Name
128
+ if (initialLocUuid) {
129
+ const locRes = await fetch(`/openmrs/ws/rest/v1/location/${initialLocUuid}`);
130
+ if (locRes.ok) {
131
+ const locData = await locRes.json();
132
+ setInstituteName(locData.display || '');
133
+ }
134
+ }
135
+
136
+ // d. Load Patient Demographics from PHN
137
+ const phnRes = await fetch(`/openmrs/ws/rest/v1/phn/${phn}`);
138
+ if (!phnRes.ok) throw new Error('PHN not found!');
139
+ const phnData = await phnRes.json();
140
+
141
+ setPatientName(phnData.person_id.person.display || '');
142
+ setPatientGender(formatGender(phnData.person_id.person.gender || ''));
143
+ if (phnData.person_id.person.birthdate) {
144
+ setPatientAge(getAge(phnData.person_id.person.birthdate));
145
+ }
146
+
147
+ // e. Load Test Details from Barcode
148
+ const testRes = await fetch(`/openmrs/rest/v1/addresult/loadview/${barcode}`);
149
+ if (!testRes.ok) throw new Error('Barcode details not found!');
150
+ const testDataArray = await testRes.json();
151
+ if (testDataArray && testDataArray.length > 0) {
152
+ const mainTestData = testDataArray[0];
153
+ setTestName(mainTestData[2] || '');
154
+ setTestRemark(mainTestData[9] || '');
155
+ }
156
+
157
+ // Set default date/time
158
+ setTestDate(getCurrentDateFormatted());
159
+ setTestTime(getCurrentTimeFormatted());
160
+
161
+ } catch (err: any) {
162
+ console.error(err);
163
+ showToast('error', err.message || 'Error initializing page data');
164
+ } finally {
165
+ setLoading(false);
166
+ }
167
+ }
168
+
169
+ loadData();
170
+ }, [phn, barcode]);
171
+
172
+ // 2. Fetch Parameters once Test Name and Location are set
173
+ useEffect(() => {
174
+ if (!testName || !locationUuid) return;
175
+
176
+ async function loadParamsAndValues() {
177
+ try {
178
+ // a. Load raw parameters for this test
179
+ const paramsRes = await fetch(`/openmrs/rest/v1/addresult/loadparameter/${testName}/${locationUuid}`);
180
+ if (!paramsRes.ok) throw new Error('Failed to load parameters');
181
+ const paramsData = await paramsRes.json();
182
+
183
+ // b. Load pre-saved parameter values depending on the patient's section
184
+ let valuesRes;
185
+ if (section === 'OPD') {
186
+ valuesRes = await fetch(`/openmrs/rest/v1/addresult/getvalueopd/${serial}/${testName}`);
187
+ } else if (section === 'Clinic') {
188
+ valuesRes = await fetch(`/openmrs/rest/v1/addresult/getvalueclinic/${serial}/${testName}`);
189
+ } else if (section === 'Ward') {
190
+ valuesRes = await fetch(`/openmrs/rest/v1/addresult/getvalueward/${serial}/${testName}`);
191
+ }
192
+
193
+ let savedValues: any[] = [];
194
+ if (valuesRes && valuesRes.ok) {
195
+ savedValues = await valuesRes.json();
196
+ }
197
+
198
+ // c. Map parameters and bind pre-saved values
199
+ const mappedParams = (paramsData || []).map((p: any) => {
200
+ const paramId = p[3]; // Parameter ID/Serial
201
+ const paramName = p[1]; // Parameter Name
202
+ const paramOptionsStr = p[2]; // Parameters options JSON string (options dropdown list)
203
+
204
+ let options: string[] = [];
205
+ try {
206
+ if (paramOptionsStr && typeof paramOptionsStr === 'string') {
207
+ options = JSON.parse(paramOptionsStr);
208
+ }
209
+ } catch (e) {
210
+ options = [];
211
+ }
212
+
213
+ // Match saved value row
214
+ const matchedSaved = savedValues.find((sv: any) => sv[2] === paramId);
215
+ const initialValue = matchedSaved && matchedSaved[0] !== null ? matchedSaved[0] : (options.length > 0 ? options[0] : '');
216
+ const initialUnits = matchedSaved && matchedSaved[5] ? matchedSaved[5] : '';
217
+ const initialFlag = matchedSaved && matchedSaved[6] ? matchedSaved[6] : '';
218
+ const initialRanges = matchedSaved && matchedSaved[7] ? matchedSaved[7] : '';
219
+ const savedId = matchedSaved ? matchedSaved[3] : 0; // Existing record primary key (ID)
220
+
221
+ return {
222
+ id: paramId,
223
+ name: paramName,
224
+ options,
225
+ value: initialValue,
226
+ units: initialUnits,
227
+ flag: initialFlag,
228
+ ranges: initialRanges,
229
+ savedId // tracks update vs insert
230
+ };
231
+ });
232
+
233
+ setParameters(mappedParams);
234
+
235
+ } catch (err: any) {
236
+ console.error(err);
237
+ showToast('error', 'Error loading test parameters');
238
+ }
239
+ }
240
+
241
+ loadParamsAndValues();
242
+ }, [testName, locationUuid, section, serial]);
243
+
244
+ // Concept Fetchers for external JSReport & DocToPdf conversion URLs
245
+ const getConceptReferenceTerm = async (codeName: string): Promise<string> => {
246
+ const res = await fetch(`/openmrs/ws/rest/v1/conceptreferenceterm?codeOrName=${codeName}`);
247
+ if (!res.ok) throw new Error(`Reference concept ${codeName} not found`);
248
+ const data = await res.json();
249
+ if (data.results && data.results.length > 0) {
250
+ const str = data.results[0].display;
251
+ const start = str.lastIndexOf("(") + 1;
252
+ const end = str.lastIndexOf(")");
253
+ return str.substring(start, end);
254
+ }
255
+ return '';
256
+ };
257
+
258
+ // Parameter input changes
259
+ const handleParameterChange = (paramId: number, field: string, value: string) => {
260
+ setParameters(prev =>
261
+ prev.map(p => (p.id === paramId ? { ...p, [field]: value } : p))
262
+ );
263
+ };
264
+
265
+ // Generate Report & Upload Flow
266
+ const handleGenerateReport = async () => {
267
+ // Validate parameter values are not empty
268
+ const hasEmpty = parameters.some(p => String(p.value).trim() === '');
269
+ if (hasEmpty) {
270
+ showToast('error', 'Please fill all parameter values before generating report.');
271
+ return;
272
+ }
273
+
274
+ try {
275
+ setShowSpinner(true);
276
+ setSpinnerText('Uploading file template...');
277
+
278
+ let finalTemplateFileName = `${testName}.docx`;
279
+
280
+ // a. Upload a new template DOCX if provided
281
+ if (uploadedFile) {
282
+ finalTemplateFileName = uploadedFile.name;
283
+ const uploadFormData = new FormData();
284
+ uploadFormData.append('file', uploadedFile);
285
+
286
+ const uploadRes = await fetch('/openmrs/rest/v1/addresult/fileupload', {
287
+ method: 'POST',
288
+ body: uploadFormData
289
+ });
290
+ if (!uploadRes.ok) throw new Error('File template upload failed');
291
+ const uploadResult = await uploadRes.json();
292
+
293
+ // Save doc template record
294
+ const rawTemplateRecord = {
295
+ templateName: uploadedFile.name,
296
+ content: '',
297
+ location: locationUuid,
298
+ filename: uploadResult.data,
299
+ createdBy: currentUserUuid
300
+ };
301
+
302
+ const saveTemplateRes = await fetch('/openmrs/rest/v1/addresult/savedoctemplatedata', {
303
+ method: 'POST',
304
+ body: JSON.stringify(rawTemplateRecord),
305
+ headers: { 'Content-Type': 'application/json' }
306
+ });
307
+ if (!saveTemplateRes.ok) throw new Error('Failed to register template record on backend');
308
+ }
309
+
310
+ setSpinnerText('Reading report template...');
311
+ // b. Fetch report template as Base64 (from server OWA directory)
312
+ const fileUrl = `${window.location.origin}/openmrs/owa/LabReportTemplateFileUploaded/${finalTemplateFileName}`;
313
+ const templateRes = await fetch(fileUrl);
314
+ if (!templateRes.ok) throw new Error('Failed to locate template DOCX file on server');
315
+ const templateBlob = await templateRes.blob();
316
+
317
+ // Convert to Base64
318
+ const base64String: string = await new Promise((resolve, reject) => {
319
+ const reader = new FileReader();
320
+ reader.onloadend = () => {
321
+ const res = reader.result as string;
322
+ resolve(res.replace(/^data:.+;base64,/, ""));
323
+ };
324
+ reader.onerror = reject;
325
+ reader.readAsDataURL(templateBlob);
326
+ });
327
+
328
+ // c. Resolve JSReport Server URL
329
+ setSpinnerText('Connecting to JSReport server...');
330
+ const jsReportUrl = await getConceptReferenceTerm('JSReportURLManual');
331
+ if (!jsReportUrl) throw new Error('JSReportURLManual concept is not defined in OpenMRS');
332
+
333
+ // Create JSReport folder
334
+ const folderRes = await fetch(`${jsReportUrl}odata/folders`, {
335
+ method: 'POST',
336
+ headers: { 'Content-Type': 'application/json' },
337
+ body: JSON.stringify({ name: `${testName} folder` })
338
+ });
339
+ if (!folderRes.ok) throw new Error('Failed to create folder on JSReport server');
340
+ const folderData = await folderRes.json();
341
+ const folderShortId = folderData.shortid;
342
+ const folderId = folderData._id;
343
+
344
+ // Upload template asset to JSReport folder
345
+ const assetRes = await fetch(`${jsReportUrl}odata/assets`, {
346
+ method: 'POST',
347
+ headers: { 'Content-Type': 'application/json' },
348
+ body: JSON.stringify({
349
+ content: base64String,
350
+ folder: { shortid: folderShortId },
351
+ name: testName
352
+ })
353
+ });
354
+ if (!assetRes.ok) throw new Error('Failed to upload asset to JSReport');
355
+ const assetData = await assetRes.json();
356
+ const assetShortId = assetData.shortid;
357
+
358
+ // Construct payload data
359
+ const paramMap: { [key: string]: any } = {};
360
+ parameters.forEach(p => {
361
+ paramMap[p.name] = {
362
+ value: p.value,
363
+ units: p.units,
364
+ ranges: p.ranges,
365
+ flag: p.flag
366
+ };
367
+ });
368
+
369
+ const reportJsonData = {
370
+ hospital: instituteName,
371
+ patientname: patientName,
372
+ phn: phn,
373
+ docname: '', // loaded docname
374
+ testName: testName,
375
+ age: patientAge,
376
+ collectdate: `${testDate} ${testTime}`,
377
+ printdate: `${getCurrentDateFormatted()} ${getCurrentTimeFormatted()}`,
378
+ parameters: paramMap
379
+ };
380
+
381
+ // Create JSReport Data element
382
+ const jsreportDataRes = await fetch(`${jsReportUrl}odata/data`, {
383
+ method: 'POST',
384
+ headers: { 'Content-Type': 'application/json' },
385
+ body: JSON.stringify({
386
+ dataJson: JSON.stringify(reportJsonData),
387
+ folder: { shortid: folderShortId },
388
+ name: `${testName} data`
389
+ })
390
+ });
391
+ if (!jsreportDataRes.ok) throw new Error('Failed to register report data on JSReport');
392
+ const jsreportDataResult = await jsreportDataRes.json();
393
+ const dataShortId = jsreportDataResult.shortid;
394
+
395
+ // Create JSReport Template element
396
+ const jsreportTemplateRes = await fetch(`${jsReportUrl}odata/templates`, {
397
+ method: 'POST',
398
+ headers: { 'Content-Type': 'application/json' },
399
+ body: JSON.stringify({
400
+ data: { shortid: dataShortId },
401
+ docx: { templateAssetShortid: assetShortId },
402
+ engine: 'handlebars',
403
+ folder: { shortid: folderShortId },
404
+ name: `${testName} template`,
405
+ recipe: 'docx'
406
+ })
407
+ });
408
+ if (!jsreportTemplateRes.ok) throw new Error('Failed to construct template configuration on JSReport');
409
+ const jsreportTemplateResult = await jsreportTemplateRes.json();
410
+ const templateShortId = jsreportTemplateResult.shortid;
411
+
412
+ // Render DOCX report from template
413
+ setSpinnerText('Rendering report...');
414
+ const renderRes = await fetch(`${jsReportUrl}api/report`, {
415
+ method: 'POST',
416
+ headers: { 'Content-Type': 'application/json' },
417
+ body: JSON.stringify({
418
+ template: { shortid: templateShortId },
419
+ recipe: 'docx'
420
+ })
421
+ });
422
+ if (!renderRes.ok) throw new Error('Failed to compile report document on JSReport');
423
+ const renderedDocxBlob = await renderRes.blob();
424
+
425
+ // d. Convert Rendered DOCX to PDF using DocToPdfConverter
426
+ setSpinnerText('Converting report to PDF format...');
427
+ const pdfConverterUrl = await getConceptReferenceTerm('DocToPdfConverterURLManual');
428
+ if (!pdfConverterUrl) throw new Error('DocToPdfConverterURLManual concept is not defined in OpenMRS');
429
+
430
+ const conversionFormData = new FormData();
431
+ conversionFormData.append('file', renderedDocxBlob, 'document.docx');
432
+ const pdfRes = await fetch(`${pdfConverterUrl}convert-to-pdf`, {
433
+ method: 'POST',
434
+ body: conversionFormData
435
+ });
436
+ if (!pdfRes.ok) throw new Error('Failed to convert compiled report to PDF');
437
+ const pdfBlob = await pdfRes.blob();
438
+
439
+ // Open PDF in a new window
440
+ const pdfUrl = window.URL.createObjectURL(pdfBlob);
441
+ window.open(pdfUrl);
442
+
443
+ // e. Fetch laboratory details to retrieve encounter UUID
444
+ setSpinnerText('Saving document attachment...');
445
+ let labEncounterUuid = '';
446
+ let labDetailsRes;
447
+ if (section === 'OPD') {
448
+ labDetailsRes = await fetch(`/openmrs/rest/v1/addresult/labdetailsopd/${serial}`);
449
+ } else if (section === 'Clinic') {
450
+ labDetailsRes = await fetch(`/openmrs/rest/v1/addresult/labdetailsclinic/${serial}`);
451
+ } else if (section === 'Ward') {
452
+ labDetailsRes = await fetch(`/openmrs/rest/v1/addresult/labdetailsward/${serial}`);
453
+ }
454
+
455
+ if (labDetailsRes && labDetailsRes.ok) {
456
+ const labDetails = await labDetailsRes.json();
457
+ labEncounterUuid = labDetails.encounteruuid || '';
458
+ }
459
+
460
+ // Upload PDF attachment to OpenMRS OWA filesystem
461
+ const docName = `${phn}@@@${testName}@@@${labEncounterUuid}`;
462
+ const uploadPdfFormData = new FormData();
463
+ uploadPdfFormData.append('file', pdfBlob, `${docName}.pdf`);
464
+
465
+ const serverUploadRes = await fetch('/openmrs/rest/v1/addresult/upload', {
466
+ method: 'POST',
467
+ body: uploadPdfFormData
468
+ });
469
+ if (!serverUploadRes.ok) throw new Error('Failed to upload PDF attachment to OpenMRS OWA');
470
+ const serverUploadResult = await serverUploadRes.json();
471
+
472
+ // Save report document record
473
+ const rawDocRecord = {
474
+ labreportingid: section === 'OPD' ? serial : null,
475
+ labreportingclinicid: section === 'Clinic' ? serial : null,
476
+ labreportingwardid: section === 'Ward' ? serial : null,
477
+ reportname: `${docName}.pdf`,
478
+ file: serverUploadResult.data,
479
+ status: 'Approved',
480
+ cby: currentUserUuid
481
+ };
482
+
483
+ let saveDocUrl = '/openmrs/rest/v1/addresult/savedocument';
484
+ if (section === 'Clinic') saveDocUrl = '/openmrs/rest/v1/addresult/savedocumentclinic';
485
+ if (section === 'Ward') saveDocUrl = '/openmrs/rest/v1/addresult/savedocumentward';
486
+
487
+ const saveDocRecordRes = await fetch(saveDocUrl, {
488
+ method: 'POST',
489
+ headers: { 'Content-Type': 'application/json' },
490
+ body: JSON.stringify(rawDocRecord)
491
+ });
492
+ if (!saveDocRecordRes.ok) throw new Error('Failed to save document meta record to database');
493
+
494
+ // Cleanup JSReport folder
495
+ setSpinnerText('Cleaning up...');
496
+ await fetch(`${jsReportUrl}odata/folders('${folderId}')`, {
497
+ method: 'DELETE'
498
+ });
499
+
500
+ setFlagGenerate(true);
501
+ showToast('success', 'Report generated and converted successfully!');
502
+
503
+ } catch (err: any) {
504
+ console.error(err);
505
+ showToast('error', err.message || 'Error occurred during report generation');
506
+ } finally {
507
+ setShowSpinner(false);
508
+ }
509
+ };
510
+
511
+ // Save Event handler
512
+ const handleSave = async () => {
513
+ if (!flagGenerate) {
514
+ showToast('error', 'Please generate Report first before saving.');
515
+ return;
516
+ }
517
+
518
+ if (!testDate || !testTime) {
519
+ showToast('error', 'Please fill all the date and time fields.');
520
+ return;
521
+ }
522
+
523
+ try {
524
+ setShowSpinner(true);
525
+ setSpinnerText('Saving parameter values...');
526
+
527
+ // 1. Save parameters list
528
+ for (const p of parameters) {
529
+ const paramRecord: any = {
530
+ id: p.savedId, // 0 for insert, >0 for update
531
+ parameterid: p.id,
532
+ value: p.value,
533
+ cby: currentUserUuid,
534
+ units: p.units,
535
+ flag: p.flag,
536
+ ranges: p.ranges
537
+ };
538
+
539
+ let saveParamUrl = '/openmrs/rest/v1/addresult/saveparametervalue';
540
+ if (section === 'OPD') {
541
+ paramRecord.labreportingid = serial;
542
+ } else if (section === 'Clinic') {
543
+ paramRecord.labreportingclinicid = serial;
544
+ saveParamUrl = '/openmrs/rest/v1/addresult/saveparametervalueclinic';
545
+ } else if (section === 'Ward') {
546
+ paramRecord.labreportingwardid = serial;
547
+ saveParamUrl = '/openmrs/rest/v1/addresult/saveparametervalueward';
548
+ }
549
+
550
+ const saveParamRes = await fetch(saveParamUrl, {
551
+ method: 'POST',
552
+ headers: { 'Content-Type': 'application/json' },
553
+ body: JSON.stringify(paramRecord)
554
+ });
555
+ if (!saveParamRes.ok) throw new Error(`Failed to save parameter ${p.name}`);
556
+ }
557
+
558
+ // 2. Save Overall Lab Test status
559
+ setSpinnerText('Updating laboratory status...');
560
+ const finalStatus = sendToApproval ? 'Send Approved' : 'Approved';
561
+ const statusPayload = {
562
+ id: serial,
563
+ status: finalStatus
564
+ };
565
+
566
+ let saveStatusUrl = '/openmrs/ws/rest/v1/samplecollection/labtestsstatus';
567
+ if (section === 'Clinic') {
568
+ saveStatusUrl = '/openmrs/ws/rest/v1/samplecollection/labtestsstatusclinic';
569
+ } else if (section === 'Ward') {
570
+ saveStatusUrl = '/openmrs/ws/rest/v1/samplecollection/labtestsstatusward';
571
+ }
572
+
573
+ const saveStatusRes = await fetch(saveStatusUrl, {
574
+ method: 'POST',
575
+ headers: { 'Content-Type': 'application/json' },
576
+ body: JSON.stringify(statusPayload)
577
+ });
578
+ if (!saveStatusRes.ok) throw new Error('Failed to update lab test status');
579
+
580
+ showToast('success', 'Data Saved Successfully!');
581
+ setTimeout(() => {
582
+ navigateTo(window.getOpenmrsSpaBase() + 'addresults');
583
+ }, 1500);
584
+
585
+ } catch (err: any) {
586
+ console.error(err);
587
+ showToast('error', err.message || 'Error saving results details');
588
+ } finally {
589
+ setShowSpinner(false);
590
+ }
591
+ };
592
+
593
+ return (
594
+ <div className={styles.bodyWrapper}>
595
+ {toast && (
596
+ <div className={`${styles.toast} ${toast.type === 'success' ? styles.toastSuccess : styles.toastError}`}>
597
+ {toast.text}
598
+ </div>
599
+ )}
600
+
601
+ {showSpinner && (
602
+ <div className={styles.overlay}>
603
+ <div className={styles.spinner}></div>
604
+ <p>{spinnerText}</p>
605
+ </div>
606
+ )}
607
+
608
+ <div className={styles.navigationCard}>
609
+ <div className={styles.navRow}>
610
+ <h2 className={styles.navTitle}>Enter Test Results</h2>
611
+ <div className={styles.instituteSelector}>
612
+ <span>Hospital:</span>
613
+ <span style={{ fontWeight: 600, color: 'var(--primary-color)' }}>{instituteName}</span>
614
+ </div>
615
+ </div>
616
+ </div>
617
+
618
+ {loading ? (
619
+ <div className={styles.card}>
620
+ <div className={styles.textCenter} style={{ padding: '2rem' }}>
621
+ <div className={styles.spinner} style={{ margin: '0 auto' }}></div>
622
+ <p style={{ marginTop: '1rem', color: 'var(--text-light)' }}>Loading profile data...</p>
623
+ </div>
624
+ </div>
625
+ ) : (
626
+ <>
627
+ <div className={styles.card}>
628
+ <div className={styles.row}>
629
+ <div className={`${styles.col} ${styles.my1}`}>
630
+ <label>Full Name</label>
631
+ <input type="text" className={styles.formControl} value={patientName} readOnly />
632
+ </div>
633
+ <div className={`${styles.col} ${styles.my1}`}>
634
+ <label>Section</label>
635
+ <input type="text" className={styles.formControl} value={section} readOnly />
636
+ </div>
637
+ <div className={`${styles.col} ${styles.my1}`}>
638
+ <label>PHN No</label>
639
+ <input type="text" className={styles.formControl} value={phn} readOnly />
640
+ </div>
641
+ <div className={`${styles.col} ${styles.my1}`}>
642
+ <label>Age</label>
643
+ <input type="text" className={styles.formControl} value={patientAge} readOnly />
644
+ </div>
645
+ <div className={`${styles.col} ${styles.my1}`}>
646
+ <label>Gender</label>
647
+ <input type="text" className={styles.formControl} value={patientGender} readOnly />
648
+ </div>
649
+ </div>
650
+
651
+ <hr style={{ margin: '1.5rem 0', borderTop: '1px solid var(--border-color)' }} />
652
+
653
+ <div className={styles.row}>
654
+ <div className={`${styles.col} ${styles.my1}`} style={{ flexGrow: 2 }}>
655
+ <label>Test Name</label>
656
+ <input type="text" className={styles.formControl} value={testName} readOnly />
657
+ </div>
658
+ <div className={`${styles.col} ${styles.my1}`} style={{ flexGrow: 3 }}>
659
+ <label>Remark</label>
660
+ <textarea className={styles.formControl} rows={2} value={testRemark} readOnly />
661
+ </div>
662
+ <div className={`${styles.col} ${styles.my1}`}>
663
+ <label>Date</label>
664
+ <input
665
+ type="date"
666
+ className={styles.formControl}
667
+ value={testDate}
668
+ onChange={(e) => setTestDate(e.target.value)}
669
+ />
670
+ </div>
671
+ <div className={`${styles.col} ${styles.my1}`}>
672
+ <label>Time</label>
673
+ <input
674
+ type="time"
675
+ className={styles.formControl}
676
+ value={testTime}
677
+ onChange={(e) => setTestTime(e.target.value)}
678
+ />
679
+ </div>
680
+ </div>
681
+ </div>
682
+
683
+ <div className={styles.card}>
684
+ <h3 style={{ fontSize: '1.1rem', fontWeight: 600, marginBottom: '1rem', color: 'var(--primary-color)' }}>
685
+ Test Parameters
686
+ </h3>
687
+ <div className={styles.showHideTableParameter}>
688
+ <table className={`${styles.table} ${styles.tableBordered} ${styles.tableStriped}`}>
689
+ <thead>
690
+ <tr>
691
+ <th>No</th>
692
+ <th>Parameter Name</th>
693
+ <th style={{ width: '220px' }}>Value</th>
694
+ <th>Units</th>
695
+ <th>Flag</th>
696
+ <th>Ranges</th>
697
+ </tr>
698
+ </thead>
699
+ <tbody>
700
+ {parameters.length > 0 ? (
701
+ parameters.map((p, idx) => (
702
+ <tr key={p.id}>
703
+ <td>{idx + 1}</td>
704
+ <td style={{ fontWeight: 500 }}>{p.name}</td>
705
+ <td>
706
+ {p.options.length > 0 ? (
707
+ <select
708
+ className={styles.formControl}
709
+ value={p.value}
710
+ onChange={(e) => handleParameterChange(p.id, 'value', e.target.value)}
711
+ style={{ width: '100%' }}
712
+ >
713
+ {p.options.map((opt: string) => (
714
+ <option key={opt} value={opt}>
715
+ {opt}
716
+ </option>
717
+ ))}
718
+ </select>
719
+ ) : (
720
+ <input
721
+ type="text"
722
+ className={styles.formControl}
723
+ value={p.value}
724
+ onChange={(e) => handleParameterChange(p.id, 'value', e.target.value)}
725
+ style={{ width: '100%' }}
726
+ />
727
+ )}
728
+ </td>
729
+ <td>
730
+ <input
731
+ type="text"
732
+ className={styles.formControl}
733
+ value={p.units}
734
+ onChange={(e) => handleParameterChange(p.id, 'units', e.target.value)}
735
+ />
736
+ </td>
737
+ <td>
738
+ <input
739
+ type="text"
740
+ className={styles.formControl}
741
+ value={p.flag}
742
+ onChange={(e) => handleParameterChange(p.id, 'flag', e.target.value)}
743
+ />
744
+ </td>
745
+ <td>
746
+ <input
747
+ type="text"
748
+ className={styles.formControl}
749
+ value={p.ranges}
750
+ onChange={(e) => handleParameterChange(p.id, 'ranges', e.target.value)}
751
+ />
752
+ </td>
753
+ </tr>
754
+ ))
755
+ ) : (
756
+ <tr>
757
+ <td colSpan={6} className={styles.textCenter} style={{ color: 'var(--text-light)' }}>
758
+ No parameters defined for this test.
759
+ </td>
760
+ </tr>
761
+ )}
762
+ </tbody>
763
+ </table>
764
+ </div>
765
+
766
+ <div className={styles.row} style={{ marginTop: '2rem', alignItems: 'center' }}>
767
+ <div className={`${styles.col} ${styles.my1}`} style={{ flexGrow: 2 }}>
768
+ <div className={styles.checkboxContainer} onClick={() => setSendToApproval(prev => !prev)}>
769
+ <input
770
+ type="checkbox"
771
+ checked={sendToApproval}
772
+ onChange={() => {}} // handled by click container
773
+ />
774
+ <span>Send to Approval</span>
775
+ </div>
776
+ </div>
777
+ <div className={`${styles.col} ${styles.my1}`} style={{ flexGrow: 3, alignItems: 'flex-end' }}>
778
+ <div style={{ display: 'flex', alignItems: 'center', gap: '1rem' }}>
779
+ <label htmlFor="upload-doc-input" className={`${styles.btn} ${styles.btnSecondary}`} style={{ margin: 0 }}>
780
+ {uploadedFile ? `Template: ${uploadedFile.name}` : 'Upload Template (.docx)'}
781
+ </label>
782
+ <input
783
+ id="upload-doc-input"
784
+ type="file"
785
+ accept=".docx"
786
+ style={{ display: 'none' }}
787
+ onChange={(e) => {
788
+ if (e.target.files && e.target.files.length > 0) {
789
+ setUploadedFile(e.target.files[0]);
790
+ }
791
+ }}
792
+ />
793
+ <button
794
+ type="button"
795
+ className={`${styles.btn} ${styles.btnWarning}`}
796
+ onClick={handleGenerateReport}
797
+ >
798
+ Generate Report
799
+ </button>
800
+ </div>
801
+ </div>
802
+ </div>
803
+
804
+ <hr style={{ margin: '1.5rem 0', borderTop: '1px solid var(--border-color)' }} />
805
+
806
+ <div className={styles.textRight}>
807
+ <button
808
+ type="button"
809
+ className={`${styles.btn} ${styles.btnSuccess} ${styles.mx2}`}
810
+ onClick={handleSave}
811
+ style={{ width: '120px', height: '40px' }}
812
+ >
813
+ Save
814
+ </button>
815
+ <button
816
+ type="button"
817
+ className={`${styles.btn} ${styles.btnDanger}`}
818
+ onClick={() => navigateTo(window.getOpenmrsSpaBase() + 'addresults')}
819
+ style={{ width: '120px', height: '40px' }}
820
+ >
821
+ Back
822
+ </button>
823
+ </div>
824
+ </div>
825
+ </>
826
+ )}
827
+ </div>
828
+ );
829
+ };
830
+
831
+ export default AddResultsForm;