@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.
- package/LICENSE +354 -0
- package/README.md +167 -0
- package/dist/117.js +1 -0
- package/dist/117.js.map +1 -0
- package/dist/128.js +1 -0
- package/dist/128.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/267.js +1 -0
- package/dist/267.js.map +1 -0
- package/dist/277.js +1 -0
- package/dist/277.js.map +1 -0
- package/dist/362.js +1 -0
- package/dist/362.js.map +1 -0
- package/dist/372.js +43 -0
- package/dist/372.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/933.js +1 -0
- package/dist/933.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/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-addresults.js +6 -0
- package/dist/openmrs-esm-addresults.js.buildmanifest.json +796 -0
- package/dist/openmrs-esm-addresults.js.map +1 -0
- package/dist/routes.json +1 -0
- package/package.json +103 -0
- package/src/add-results-form.component.tsx +831 -0
- package/src/add-results.component.tsx +258 -0
- package/src/add-results.scss +448 -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 +20 -0
- package/src/root.component.test.tsx +12 -0
- package/src/root.component.tsx +13 -0
- package/src/root.scss +5 -0
- 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;
|