source: frontend/src/pages/lab-tests/LabTestList.js@ f18b11b

Last change on this file since f18b11b was f05de05, checked in by MBK <marija.karapandzova@…>, 4 months ago

Add pages and routing for lab tests, procedures and billing with api services

  • Property mode set to 100644
File size: 20.0 KB
Line 
1import React, { useState, useEffect } from 'react';
2import { patientService } from '../../services/patientService';
3import { labService } from '../../services/labService';
4import ErrorAlert from '../../components/ErrorAlert';
5
6function LabTestList() {
7 const user = JSON.parse(localStorage.getItem('user') || '{}');
8 const isLabTechnician = user.role === 'LAB_TECHNICIAN';
9
10 const [patient, setPatient] = useState(null);
11 const [embg, setEmbg] = useState('');
12 const [searched, setSearched] = useState(false);
13 const [loading, setLoading] = useState(false);
14 const [error, setError] = useState(null);
15
16 const [availableTests, setAvailableTests] = useState([]);
17 const [labTestRequests, setLabTestRequests] = useState([]);
18 const [labResults, setLabResults] = useState([]);
19 const [pendingTests, setPendingTests] = useState([]);
20 const [selectedTest, setSelectedTest] = useState(null);
21
22 const [showRequestForm, setShowRequestForm] = useState(false);
23 const [requestData, setRequestData] = useState({
24 testId: '',
25 testDate: new Date().toISOString().split('T')[0],
26 notes: '',
27 });
28
29 const [submitFormData, setSubmitFormData] = useState({
30 results: '',
31 resultDate: new Date().toISOString().split('T')[0],
32 });
33
34 useEffect(() => {
35 if (isLabTechnician) {
36 loadPendingTests();
37 }
38 }, [isLabTechnician]);
39
40 const loadPendingTests = async () => {
41 try {
42 setLoading(true);
43 const response = await labService.getPendingLabTests();
44 setPendingTests(response.data || []);
45 } catch (err) {
46 setError('Failed to load pending lab tests');
47 } finally {
48 setLoading(false);
49 }
50 };
51
52 const handleSearch = async (e) => {
53 e.preventDefault();
54 setError(null);
55 setLoading(true);
56
57 try {
58 if (!embg.trim()) {
59 setError('Please enter an EMBG');
60 setLoading(false);
61 return;
62 }
63
64 const patientResponse = await patientService.getPatientByEmbg(embg);
65 setPatient(patientResponse.data);
66 setSearched(true);
67
68 // Fetch available tests
69 const testsResponse = await labService.getAllLabTests();
70 setAvailableTests(testsResponse.data);
71
72 // Fetch medical record for patient
73 const { medicalRecordService } = await import('../../services/medicalRecordService');
74 const medicalRecordRes = await medicalRecordService.getMedicalRecordByPatientId(patientResponse.data.patientId);
75
76 // Fetch existing test requests for patient
77 const requestsResponse = await labService.getLabTestRequestsForPatient(patientResponse.data.patientId);
78 setLabTestRequests(requestsResponse.data);
79
80 // Fetch lab results for medical record
81 const resultsResponse = await labService.getLabResultsForMedicalRecord(medicalRecordRes.data.recordId);
82 setLabResults(resultsResponse.data);
83 } catch (err) {
84 setError(`Patient with EMBG ${embg} not found`);
85 setPatient(null);
86 } finally {
87 setLoading(false);
88 }
89 };
90
91 const handleRequestTest = async (e) => {
92 e.preventDefault();
93 setError(null);
94
95 if (!requestData.testId) {
96 setError('Please select a test');
97 return;
98 }
99
100 if (!patient) {
101 setError('Patient not found');
102 return;
103 }
104
105 try {
106 setLoading(true);
107
108 // Get doctor ID from localStorage (set during login)
109 const doctorId = localStorage.getItem('doctorId') || 1;
110
111 const request = {
112 patientId: patient.patientId,
113 medicalRecordId: patient.patientId, // Assuming medical record ID matches patient ID
114 doctorId: parseInt(doctorId),
115 testId: parseInt(requestData.testId),
116 testDate: requestData.testDate,
117 notes: requestData.notes,
118 };
119
120 await labService.requestLabTest(request);
121
122 // Refresh the test requests
123 const requestsResponse = await labService.getLabTestRequestsForPatient(patient.patientId);
124 setLabTestRequests(requestsResponse.data);
125
126 // Reset form
127 setRequestData({
128 testId: '',
129 testDate: new Date().toISOString().split('T')[0],
130 notes: '',
131 });
132 setShowRequestForm(false);
133 setError(null);
134 } catch (err) {
135 setError('Failed to request lab test: ' + err.response?.data?.error || err.message);
136 } finally {
137 setLoading(false);
138 }
139 };
140
141 const handleSubmitResult = async (e) => {
142 e.preventDefault();
143 setError(null);
144
145 if (!submitFormData.results.trim()) {
146 setError('Please enter test results');
147 return;
148 }
149
150 if (!selectedTest) {
151 setError('No test selected');
152 return;
153 }
154
155 try {
156 setLoading(true);
157
158 const { medicalRecordService } = await import('../../services/medicalRecordService');
159 const medicalRecordRes = await medicalRecordService.getMedicalRecordByPatientId(selectedTest.patientId);
160
161 const resultData = {
162 medicalRecordId: medicalRecordRes.data.recordId,
163 testId: selectedTest.testId,
164 results: submitFormData.results,
165 resultDate: submitFormData.resultDate,
166 };
167
168 await labService.submitLabResult(resultData);
169
170 // Refresh pending tests
171 await loadPendingTests();
172
173 // Reset form
174 setSelectedTest(null);
175 setSubmitFormData({
176 results: '',
177 resultDate: new Date().toISOString().split('T')[0],
178 });
179
180 // Show success message
181 setError(null);
182 alert('Lab result submitted successfully!');
183 } catch (err) {
184 setError('Failed to submit lab result: ' + (err.response?.data?.error || err.message));
185 } finally {
186 setLoading(false);
187 }
188 };
189
190 if (isLabTechnician) {
191 return (
192 <div>
193 <h1 className="text-3xl font-bold mb-6" style={{ color: '#7c3aed' }}>Lab Tests - Submit Results</h1>
194
195 {error && <ErrorAlert message={error} onClose={() => setError(null)} />}
196
197 {selectedTest ? (
198 // Submit Result Form
199 <div className="bg-white rounded-lg shadow p-6 mb-6">
200 <h2 className="text-xl font-bold mb-4">Submit Lab Result</h2>
201 <div className="mb-4 p-4 bg-gray-50 rounded-lg">
202 <p className="mb-2"><strong>Test:</strong> {selectedTest.testName}</p>
203 <p className="mb-2"><strong>Patient:</strong> {selectedTest.patientName}</p>
204 <p className="mb-2"><strong>Doctor:</strong> {selectedTest.doctorName}</p>
205 <p className="mb-2"><strong>Test Date:</strong> {selectedTest.testDate}</p>
206 {selectedTest.notes && <p><strong>Notes:</strong> {selectedTest.notes}</p>}
207 </div>
208
209 <form onSubmit={handleSubmitResult} className="space-y-4">
210 <div>
211 <label className="block text-sm font-semibold mb-2">Test Results *</label>
212 <textarea
213 value={submitFormData.results}
214 onChange={(e) => setSubmitFormData({ ...submitFormData, results: e.target.value })}
215 placeholder="Enter detailed test results"
216 className="w-full px-4 py-2 border rounded-lg"
217 rows="4"
218 required
219 />
220 </div>
221
222 <div>
223 <label className="block text-sm font-semibold mb-2">Result Date *</label>
224 <input
225 type="date"
226 value={submitFormData.resultDate}
227 onChange={(e) => setSubmitFormData({ ...submitFormData, resultDate: e.target.value })}
228 className="w-full px-4 py-2 border rounded-lg"
229 required
230 />
231 </div>
232
233 <div className="flex gap-4">
234 <button
235 type="submit"
236 disabled={loading}
237 style={{
238 background: loading ? '#d1d5db' : '#bfdbfe',
239 color: '#1e1035',
240 padding: '8px 24px',
241 borderRadius: '6px',
242 border: 'none',
243 cursor: loading ? 'not-allowed' : 'pointer',
244 fontSize: '14px',
245 fontWeight: '400'
246 }}
247 onMouseEnter={(e) => !loading && (e.currentTarget.style.background = '#93c5fd')}
248 onMouseLeave={(e) => !loading && (e.currentTarget.style.background = '#bfdbfe')}
249 >
250 {loading ? 'Submitting...' : 'Submit Result'}
251 </button>
252 <button
253 type="button"
254 onClick={() => {
255 setSelectedTest(null);
256 setSubmitFormData({
257 results: '',
258 resultDate: new Date().toISOString().split('T')[0],
259 });
260 }}
261 className="bg-gray-400 text-white px-6 py-2 rounded hover:bg-gray-500"
262 >
263 Cancel
264 </button>
265 </div>
266 </form>
267 </div>
268 ) : (
269 // Pending Tests List
270 <div className="bg-white rounded-lg shadow overflow-hidden">
271 <div className="p-6 border-b">
272 <h2 className="text-xl font-bold">Pending Lab Tests ({pendingTests.length})</h2>
273 </div>
274
275 {pendingTests.length === 0 ? (
276 <div className="p-6 text-center text-gray-600">
277 No pending lab tests
278 </div>
279 ) : (
280 <table className="w-full">
281 <thead className="bg-gray-100">
282 <tr>
283 <th className="px-6 py-3 text-left text-sm font-semibold">Test</th>
284 <th className="px-6 py-3 text-left text-sm font-semibold">Patient</th>
285 <th className="px-6 py-3 text-left text-sm font-semibold">Doctor</th>
286 <th className="px-6 py-3 text-left text-sm font-semibold">Requested</th>
287 <th className="px-6 py-3 text-left text-sm font-semibold">Test Date</th>
288 <th className="px-6 py-3 text-left text-sm font-semibold">Notes</th>
289 <th className="px-6 py-3 text-left text-sm font-semibold">Action</th>
290 </tr>
291 </thead>
292 <tbody>
293 {pendingTests.map((test, index) => (
294 <tr key={index} className="border-t hover:bg-gray-50">
295 <td className="px-6 py-3 font-medium">{test.testName}</td>
296 <td className="px-6 py-3">{test.patientName}</td>
297 <td className="px-6 py-3">{test.doctorName}</td>
298 <td className="px-6 py-3 text-green-600">{test.requestDate}</td>
299 <td className="px-6 py-3 text-purple-600">{test.testDate}</td>
300 <td className="px-6 py-3 text-gray-600 text-sm">{test.notes || '-'}</td>
301 <td className="px-6 py-3">
302 <button
303 onClick={() => setSelectedTest(test)}
304 style={{
305 background: '#bfdbfe',
306 color: '#1e1035',
307 padding: '6px 12px',
308 borderRadius: '4px',
309 border: 'none',
310 cursor: 'pointer',
311 fontSize: '12px',
312 fontWeight: '400'
313 }}
314 onMouseEnter={(e) => e.currentTarget.style.background = '#93c5fd'}
315 onMouseLeave={(e) => e.currentTarget.style.background = '#bfdbfe'}
316 >
317 Submit Result
318 </button>
319 </td>
320 </tr>
321 ))}
322 </tbody>
323 </table>
324 )}
325 </div>
326 )}
327 </div>
328 );
329 }
330
331 return (
332 <div>
333 <h1 className="text-3xl font-bold mb-6" style={{ color: '#7c3aed' }}>Lab Tests</h1>
334
335 {error && <ErrorAlert message={error} onClose={() => setError(null)} />}
336
337 {/* Search Form */}
338 <div className="bg-white rounded-lg shadow p-6 mb-6">
339 <h2 className="text-xl font-bold mb-4">Search Patient</h2>
340 <form onSubmit={handleSearch} className="space-y-4">
341 <div className="flex gap-4">
342 <div className="flex-1">
343 <label className="block text-sm font-semibold mb-2">Patient EMBG</label>
344 <input
345 type="text"
346 value={embg}
347 onChange={(e) => setEmbg(e.target.value)}
348 placeholder="e.g., 1402994123456"
349 className="w-full px-4 py-2 border rounded-lg"
350 />
351 </div>
352 <div className="flex items-end">
353 <button
354 type="submit"
355 disabled={loading}
356 className="bg-purple-600 text-white px-8 py-2 rounded-full hover:bg-purple-700 disabled:bg-gray-400"
357 >
358 {loading ? 'Searching...' : 'Search'}
359 </button>
360 </div>
361 </div>
362 </form>
363 </div>
364
365 {/* Patient Lab Tests */}
366 {searched && patient && (
367 <div className="space-y-6">
368 {/* Patient Info */}
369 <div className="bg-white rounded-lg shadow p-6">
370 <div className="flex justify-between items-start mb-4">
371 <h2 className="text-2xl font-bold">{patient.firstName} {patient.lastName}</h2>
372 {!showRequestForm && (
373 <button
374 onClick={() => setShowRequestForm(true)}
375 className="bg-green-600 text-white px-4 py-2 rounded hover:bg-green-700"
376 >
377 Request Lab Test
378 </button>
379 )}
380 </div>
381 <div className="grid grid-cols-4 gap-4">
382 <div>
383 <p className="text-sm text-gray-600">EMBG</p>
384 <p className="font-semibold">{patient.embg}</p>
385 </div>
386 <div>
387 <p className="text-sm text-gray-600">Blood Type</p>
388 <p className="font-semibold">{patient.bloodType || 'N/A'}</p>
389 </div>
390 <div>
391 <p className="text-sm text-gray-600">Date of Birth</p>
392 <p className="font-semibold">{patient.dateOfBirth}</p>
393 </div>
394 </div>
395 </div>
396
397 {/* Request Lab Test Form */}
398 {showRequestForm && (
399 <div className="bg-white rounded-lg shadow p-6">
400 <h3 className="text-xl font-bold mb-4">Request Lab Test</h3>
401 <form onSubmit={handleRequestTest} className="space-y-4">
402 <div className="grid grid-cols-2 gap-4">
403 <div>
404 <label className="block text-sm font-semibold mb-2">Test</label>
405 <select
406 value={requestData.testId}
407 onChange={(e) => setRequestData({ ...requestData, testId: e.target.value })}
408 className="w-full px-4 py-2 border rounded-lg"
409 required
410 >
411 <option value="">Select a test</option>
412 {availableTests.map((test) => (
413 <option key={test.testId} value={test.testId}>
414 {test.testName} (${test.cost})
415 </option>
416 ))}
417 </select>
418 </div>
419 <div>
420 <label className="block text-sm font-semibold mb-2">Test Date</label>
421 <input
422 type="date"
423 value={requestData.testDate}
424 onChange={(e) => setRequestData({ ...requestData, testDate: e.target.value })}
425 className="w-full px-4 py-2 border rounded-lg"
426 />
427 </div>
428 </div>
429 <div>
430 <label className="block text-sm font-semibold mb-2">Notes</label>
431 <textarea
432 value={requestData.notes}
433 onChange={(e) => setRequestData({ ...requestData, notes: e.target.value })}
434 placeholder="Additional notes for the lab technician"
435 className="w-full px-4 py-2 border rounded-lg"
436 rows="3"
437 />
438 </div>
439 <div className="flex gap-4">
440 <button
441 type="submit"
442 disabled={loading}
443 className="bg-green-600 text-white px-6 py-2 rounded hover:bg-green-700 disabled:bg-gray-400"
444 >
445 {loading ? 'Requesting...' : 'Request Test'}
446 </button>
447 <button
448 type="button"
449 onClick={() => setShowRequestForm(false)}
450 className="bg-gray-400 text-white px-6 py-2 rounded hover:bg-gray-500"
451 >
452 Cancel
453 </button>
454 </div>
455 </form>
456 </div>
457 )}
458
459 {/* Test Requests */}
460 {labTestRequests && labTestRequests.length > 0 && (
461 <div className="bg-white rounded-lg shadow p-6">
462 <h3 className="text-xl font-bold mb-4">Lab Test Requests</h3>
463 <div className="space-y-3">
464 {labTestRequests.map((request) => (
465 <div key={request.testId} className="border-l-4 border-purple-500 pl-4 py-2">
466 <p className="font-semibold text-lg">{request.testName}</p>
467 <p className="text-sm text-gray-600">Requested by: {request.doctorName}</p>
468 <p className="text-sm text-gray-600">Test Date: {request.requestDate}</p>
469 {request.notes && (
470 <p className="text-sm text-gray-600">Notes: {request.notes}</p>
471 )}
472 </div>
473 ))}
474 </div>
475 </div>
476 )}
477
478 {/* Lab Test Results */}
479 {labResults && labResults.length > 0 && (
480 <div className="bg-white rounded-lg shadow p-6">
481 <h3 className="text-xl font-bold mb-4">Lab Test Results</h3>
482 <div className="space-y-4">
483 {labResults.map((result) => (
484 <div key={result.resultId} className="border-l-4 border-green-500 pl-4 py-3 bg-green-50 rounded">
485 <p className="font-semibold text-lg text-green-700">{result.testName}</p>
486 <p className="text-sm text-gray-700 mt-2"><strong>Results:</strong> {result.results}</p>
487 <p className="text-sm text-gray-600">Result Date: {result.resultDate}</p>
488 </div>
489 ))}
490 </div>
491 </div>
492 )}
493
494 {/* Link to submit results */}
495 {labTestRequests && labTestRequests.length > 0 && (
496 <div className="bg-yellow-50 rounded-lg p-6">
497 <h3 className="text-lg font-semibold text-yellow-800 mb-3">Lab Technician: Submit Test Results</h3>
498 <p className="text-sm text-gray-700 mb-4">
499 {labTestRequests.length} test{labTestRequests.length !== 1 ? 's' : ''} awaiting results
500 </p>
501 <a
502 href="/lab-tests/results"
503 className="inline-block bg-yellow-600 text-white px-6 py-2 rounded hover:bg-yellow-700"
504 >
505 Submit Lab Results
506 </a>
507 </div>
508 )}
509
510 {/* No requests message */}
511 {!labTestRequests || labTestRequests.length === 0 && !labResults?.length && (
512 <div className="bg-blue-50 rounded-lg p-6 text-center">
513 <p className="text-gray-600">No lab test requests for this patient</p>
514 </div>
515 )}
516 </div>
517 )}
518
519 {/* No search performed */}
520 {!searched && (
521 <div className="bg-gray-50 rounded-lg p-12 text-center">
522 <p className="text-gray-600 text-lg">Enter a patient EMBG to request lab tests</p>
523 </div>
524 )}
525 </div>
526 );
527}
528
529export default LabTestList;
Note: See TracBrowser for help on using the repository browser.