@sasindi/radiologytestscheduling 0.0.0-stage → 1.0.1

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 (72) hide show
  1. package/LICENSE +354 -0
  2. package/README.md +166 -2
  3. package/dist/117.js +1 -0
  4. package/dist/117.js.map +1 -0
  5. package/dist/197.js +1 -0
  6. package/dist/197.js.map +1 -0
  7. package/dist/206.css +1 -0
  8. package/dist/206.css.map +1 -0
  9. package/dist/206.js +39 -0
  10. package/dist/206.js.map +1 -0
  11. package/dist/237.js +1 -0
  12. package/dist/237.js.map +1 -0
  13. package/dist/29.js +1 -0
  14. package/dist/29.js.map +1 -0
  15. package/dist/362.js +1 -0
  16. package/dist/362.js.map +1 -0
  17. package/dist/410.js +1 -0
  18. package/dist/410.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/538.js +1 -0
  24. package/dist/538.js.map +1 -0
  25. package/dist/540.css +1 -0
  26. package/dist/540.css.map +1 -0
  27. package/dist/559.js +1 -0
  28. package/dist/559.js.map +1 -0
  29. package/dist/570.js +60 -0
  30. package/dist/570.js.map +1 -0
  31. package/dist/578.js +1 -0
  32. package/dist/578.js.map +1 -0
  33. package/dist/647.js +1 -0
  34. package/dist/647.js.map +1 -0
  35. package/dist/652.js +1 -0
  36. package/dist/652.js.map +1 -0
  37. package/dist/668.js +1 -0
  38. package/dist/668.js.map +1 -0
  39. package/dist/749.js +1 -0
  40. package/dist/749.js.map +1 -0
  41. package/dist/809.js +22 -0
  42. package/dist/809.js.map +1 -0
  43. package/dist/813.js +1 -0
  44. package/dist/813.js.map +1 -0
  45. package/dist/898.js +1 -0
  46. package/dist/898.js.map +1 -0
  47. package/dist/974.css +1 -0
  48. package/dist/974.css.map +1 -0
  49. package/dist/974.js +1 -0
  50. package/dist/974.js.map +1 -0
  51. package/dist/976.js +1 -0
  52. package/dist/976.js.map +1 -0
  53. package/dist/977.js +1 -0
  54. package/dist/977.js.map +1 -0
  55. package/dist/98.js +1 -0
  56. package/dist/98.js.map +1 -0
  57. package/dist/openmrs-esm-radiologytestscheduling.js +3 -0
  58. package/dist/openmrs-esm-radiologytestscheduling.js.map +1 -0
  59. package/dist/routes.json +1 -0
  60. package/package.json +101 -4
  61. package/src/config-schema.ts +21 -0
  62. package/src/constants.ts +1 -0
  63. package/src/declarations.d.ts +23 -0
  64. package/src/index.ts +28 -0
  65. package/src/radiology-test-scheduling.component.tsx +875 -0
  66. package/src/radiology-test-scheduling.resource.ts +321 -0
  67. package/src/radiology-test-scheduling.scss +478 -0
  68. package/src/root.component.test.tsx +12 -0
  69. package/src/root.component.tsx +13 -0
  70. package/src/root.scss +5 -0
  71. package/src/routes.json +12 -0
  72. package/src/types.ts +100 -0
@@ -0,0 +1,875 @@
1
+ import React, { useState, useEffect, useCallback, useMemo } from 'react';
2
+ import { useTranslation } from 'react-i18next';
3
+ import { Loading } from '@carbon/react';
4
+ import Swal from 'sweetalert2';
5
+ import {
6
+ fetchCurrentSessionAndInstitute,
7
+ fetchInstitutes,
8
+ fetchPendingSchedulingOrders,
9
+ fetchPatientDetails,
10
+ fetchLocationDisplay,
11
+ fetchUserDisplay,
12
+ fetchExamRooms,
13
+ fetchCalendarData,
14
+ saveScheduling,
15
+ SessionInfo,
16
+ } from './radiology-test-scheduling.resource';
17
+ import {
18
+ SchedulingOrderDTO,
19
+ PatientDemographics,
20
+ InstituteOption,
21
+ ExamRoom,
22
+ RadiologyOrder,
23
+ } from './types';
24
+ import styles from './radiology-test-scheduling.scss';
25
+
26
+ const DEFAULT_AVATAR =
27
+ 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100" viewBox="0 0 24 24" fill="%23b0bec5"><path d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z"/></svg>';
28
+
29
+ const RadiologyTestScheduling: React.FC = () => {
30
+ const { t } = useTranslation();
31
+
32
+ // Session & Institute states
33
+ const [sessionInfo, setSessionInfo] = useState<SessionInfo>({
34
+ userUuid: '',
35
+ personUuid: '',
36
+ locationUuid: '',
37
+ locationName: '',
38
+ });
39
+ const [institutes, setInstitutes] = useState<InstituteOption[]>([]);
40
+ const [selectedInstitute, setSelectedInstitute] = useState<string>('all');
41
+
42
+ // Pending Tests & Table states
43
+ const [pendingOrders, setPendingOrders] = useState<SchedulingOrderDTO[]>([]);
44
+ const [loadingOrders, setLoadingOrders] = useState<boolean>(true);
45
+ const [searchTerm, setSearchTerm] = useState<string>('');
46
+
47
+ // Selected Order & Patient state
48
+ const [selectedOrder, setSelectedOrder] = useState<SchedulingOrderDTO | null>(null);
49
+ const [patientDetails, setPatientDetails] = useState<PatientDemographics | null>(null);
50
+ const [loadingPatient, setLoadingPatient] = useState<boolean>(false);
51
+
52
+ // Scheduling inputs
53
+ const [scheduledDateTime, setScheduledDateTime] = useState<string>('');
54
+ const [remarks, setRemarks] = useState<string>('');
55
+ const [isSaving, setIsSaving] = useState<boolean>(false);
56
+
57
+ // Calendar filter states
58
+ const [selectedModality, setSelectedModality] = useState<string>('all');
59
+ const [examRooms, setExamRooms] = useState<ExamRoom[]>([]);
60
+ const [selectedExamRoom, setSelectedExamRoom] = useState<string>('all');
61
+ const [calendarOrders, setCalendarOrders] = useState<RadiologyOrder[]>([]);
62
+ const [calendarDate, setCalendarDate] = useState<Date>(new Date());
63
+ const [loadingCalendar, setLoadingCalendar] = useState<boolean>(false);
64
+
65
+ // Initialize Session and Institutes
66
+ useEffect(() => {
67
+ let isMounted = true;
68
+ (async () => {
69
+ try {
70
+ const [session, instList] = await Promise.all([
71
+ fetchCurrentSessionAndInstitute(),
72
+ fetchInstitutes(),
73
+ ]);
74
+ if (!isMounted) return;
75
+ setSessionInfo(session);
76
+ setInstitutes(instList);
77
+ if (session.locationUuid) {
78
+ setSelectedInstitute(session.locationUuid);
79
+ }
80
+ } catch (err) {
81
+ console.error('Initialization failed', err);
82
+ }
83
+ })();
84
+ return () => {
85
+ isMounted = false;
86
+ };
87
+ }, []);
88
+
89
+ // Load Pending Orders whenever selected institute changes
90
+ const loadPendingOrders = useCallback(async (instUuid: string) => {
91
+ setLoadingOrders(true);
92
+ try {
93
+ const orders = await fetchPendingSchedulingOrders(instUuid);
94
+ // Enhance with async location / user names if available
95
+ const enhanced = await Promise.all(
96
+ orders.map(async (dto) => {
97
+ const loc = dto.radiology.location ? await fetchLocationDisplay(dto.radiology.location) : '';
98
+ const ordBy = dto.radiology.requesterReference
99
+ ? await fetchUserDisplay(dto.radiology.requesterReference)
100
+ : '';
101
+ return {
102
+ ...dto,
103
+ locationName: loc,
104
+ orderedByName: ordBy,
105
+ };
106
+ }),
107
+ );
108
+ setPendingOrders(enhanced);
109
+ } catch (err) {
110
+ console.error('Failed to load pending scheduling orders', err);
111
+ Swal.fire({
112
+ icon: 'error',
113
+ title: 'Error Loading Orders',
114
+ text: 'Unable to fetch pending radiology test orders.',
115
+ });
116
+ } finally {
117
+ setLoadingOrders(false);
118
+ }
119
+ }, []);
120
+
121
+ useEffect(() => {
122
+ loadPendingOrders(selectedInstitute);
123
+ }, [selectedInstitute, loadPendingOrders]);
124
+
125
+ // Load Exam Rooms when modality changes
126
+ useEffect(() => {
127
+ (async () => {
128
+ if (selectedModality === 'all' || !selectedModality) {
129
+ setExamRooms([]);
130
+ setSelectedExamRoom('all');
131
+ } else {
132
+ const rooms = await fetchExamRooms(selectedModality);
133
+ setExamRooms(rooms);
134
+ setSelectedExamRoom('all');
135
+ }
136
+ })();
137
+ }, [selectedModality]);
138
+
139
+ // Load Calendar Data
140
+ const loadCalendar = useCallback(async () => {
141
+ setLoadingCalendar(true);
142
+ try {
143
+ const data = await fetchCalendarData(selectedModality, selectedExamRoom, selectedInstitute);
144
+ setCalendarOrders(data);
145
+ } catch (err) {
146
+ console.error('Failed to load calendar data', err);
147
+ } finally {
148
+ setLoadingCalendar(false);
149
+ }
150
+ }, [selectedModality, selectedExamRoom, selectedInstitute]);
151
+
152
+ useEffect(() => {
153
+ loadCalendar();
154
+ }, [loadCalendar]);
155
+
156
+ // Handle row selection
157
+ const handleSelectRow = async (dto: SchedulingOrderDTO) => {
158
+ setSelectedOrder(dto);
159
+ setScheduledDateTime('');
160
+ setRemarks('');
161
+ setLoadingPatient(true);
162
+
163
+ try {
164
+ const details = await fetchPatientDetails(dto.radiology.patientPhn);
165
+ setPatientDetails(details);
166
+ } catch (e) {
167
+ console.error('Failed to load patient details', e);
168
+ } finally {
169
+ setLoadingPatient(false);
170
+ }
171
+ };
172
+
173
+ // Reset selection
174
+ const handleClear = () => {
175
+ setSelectedOrder(null);
176
+ setPatientDetails(null);
177
+ setScheduledDateTime('');
178
+ setRemarks('');
179
+ };
180
+
181
+ // Save Schedule Handler
182
+ const handleSave = async () => {
183
+ if (!selectedOrder) {
184
+ Swal.fire({
185
+ icon: 'warning',
186
+ title: 'No Order Selected',
187
+ text: 'Please select a pending test order from the table to schedule.',
188
+ confirmButtonColor: '#0f62fe',
189
+ });
190
+ return;
191
+ }
192
+
193
+ if (!scheduledDateTime) {
194
+ Swal.fire({
195
+ icon: 'warning',
196
+ title: 'Date & Time Required',
197
+ text: 'Please choose the Scheduled Date and Time before saving.',
198
+ confirmButtonColor: '#0f62fe',
199
+ });
200
+ return;
201
+ }
202
+
203
+ // Format datetime string: "YYYY-MM-DD HH:mm:ss"
204
+ const formattedDateTime = scheduledDateTime.replace('T', ' ') + ':00';
205
+
206
+ const result = await Swal.fire({
207
+ title: 'Confirm Scheduling',
208
+ text: `Schedule ${selectedOrder.radiology.codeDisplay || 'test'} on ${formattedDateTime}?`,
209
+ icon: 'question',
210
+ showCancelButton: true,
211
+ confirmButtonColor: '#0f62fe',
212
+ cancelButtonColor: '#6c757d',
213
+ confirmButtonText: 'Yes, Schedule',
214
+ cancelButtonText: 'Cancel',
215
+ });
216
+
217
+ if (!result.isConfirmed) {
218
+ return;
219
+ }
220
+
221
+ setIsSaving(true);
222
+ try {
223
+ await saveScheduling(selectedOrder.type, {
224
+ id: selectedOrder.radiology.id,
225
+ scheduledDateTime: formattedDateTime,
226
+ remarks: remarks || '',
227
+ cby: sessionInfo.userUuid || '1',
228
+ });
229
+
230
+ await Swal.fire({
231
+ icon: 'success',
232
+ title: 'Saved Successfully!',
233
+ text: 'The radiology test has been scheduled.',
234
+ timer: 2000,
235
+ showConfirmButton: false,
236
+ });
237
+
238
+ handleClear();
239
+ loadPendingOrders(selectedInstitute);
240
+ loadCalendar();
241
+ } catch (err: any) {
242
+ console.error('Failed to save schedule', err);
243
+ Swal.fire({
244
+ icon: 'error',
245
+ title: 'Save Failed',
246
+ text: err?.message || 'Could not schedule radiology test. Please check server logs.',
247
+ confirmButtonColor: '#da1e28',
248
+ });
249
+ } finally {
250
+ setIsSaving(false);
251
+ }
252
+ };
253
+
254
+ // Filtered Orders for search input
255
+ const filteredOrders = useMemo(() => {
256
+ if (!searchTerm.trim()) return pendingOrders;
257
+ const term = searchTerm.toLowerCase();
258
+ return pendingOrders.filter((dto) => {
259
+ const phn = dto.radiology.patientPhn?.toLowerCase() || '';
260
+ const test = dto.radiology.codeDisplay?.toLowerCase() || '';
261
+ const section = dto.type?.toLowerCase() || '';
262
+ const priority = dto.radiology.priority?.toLowerCase() || '';
263
+ const location = dto.locationName?.toLowerCase() || '';
264
+ return (
265
+ phn.includes(term) ||
266
+ test.includes(term) ||
267
+ section.includes(term) ||
268
+ priority.includes(term) ||
269
+ location.includes(term)
270
+ );
271
+ });
272
+ }, [pendingOrders, searchTerm]);
273
+
274
+ // Calendar grouping helper
275
+ const calendarDays = useMemo(() => {
276
+ const year = calendarDate.getFullYear();
277
+ const month = calendarDate.getMonth();
278
+ const firstDayIndex = new Date(year, month, 1).getDay();
279
+ const totalDays = new Date(year, month + 1, 0).getDate();
280
+ const prevMonthDays = new Date(year, month, 0).getDate();
281
+
282
+ // Map scheduled tests by date string "YYYY-MM-DD"
283
+ const dateMap = new Map<string, RadiologyOrder[]>();
284
+ for (const ord of calendarOrders) {
285
+ if (ord.scheduledDateTime) {
286
+ const dStr = ord.scheduledDateTime.split(' ')[0];
287
+ if (!dateMap.has(dStr)) {
288
+ dateMap.set(dStr, []);
289
+ }
290
+ dateMap.get(dStr)!.push(ord);
291
+ }
292
+ }
293
+
294
+ const cells = [];
295
+
296
+ // Previous month filler days
297
+ for (let i = firstDayIndex - 1; i >= 0; i--) {
298
+ const dayNum = prevMonthDays - i;
299
+ cells.push({
300
+ dayNum,
301
+ isCurrentMonth: false,
302
+ dateStr: '',
303
+ orders: [],
304
+ });
305
+ }
306
+
307
+ // Current month days
308
+ const todayStr = new Date().toISOString().split('T')[0];
309
+ for (let day = 1; day <= totalDays; day++) {
310
+ const monthStr = String(month + 1).padStart(2, '0');
311
+ const dayStr = String(day).padStart(2, '0');
312
+ const dateStr = `${year}-${monthStr}-${dayStr}`;
313
+ cells.push({
314
+ dayNum: day,
315
+ isCurrentMonth: true,
316
+ isToday: dateStr === todayStr,
317
+ dateStr,
318
+ orders: dateMap.get(dateStr) || [],
319
+ });
320
+ }
321
+
322
+ // Next month filler days to complete grid (multiples of 7)
323
+ const remaining = 7 - (cells.length % 7);
324
+ if (remaining < 7) {
325
+ for (let i = 1; i <= remaining; i++) {
326
+ cells.push({
327
+ dayNum: i,
328
+ isCurrentMonth: false,
329
+ dateStr: '',
330
+ orders: [],
331
+ });
332
+ }
333
+ }
334
+
335
+ return cells;
336
+ }, [calendarDate, calendarOrders]);
337
+
338
+ // Handle clicking calendar event or day
339
+ const handleCalendarEventClick = (dateStr: string, orders: RadiologyOrder[]) => {
340
+ if (!orders || orders.length === 0) return;
341
+
342
+ // Group orders by modality and exam room
343
+ const breakdown: Record<string, Record<string, number>> = {};
344
+ for (const ord of orders) {
345
+ const mod = ord.categoryDisplay || 'Other';
346
+ const room = ord.supportingInfo || 'Default Room';
347
+ if (!breakdown[mod]) {
348
+ breakdown[mod] = {};
349
+ }
350
+ breakdown[mod][room] = (breakdown[mod][room] || 0) + 1;
351
+ }
352
+
353
+ let html = `<div style="text-align: left; font-size: 0.92rem;">`;
354
+ for (const [mod, rooms] of Object.entries(breakdown)) {
355
+ html += `<div style="margin-bottom: 0.75rem;"><strong style="color: #009384;">${mod}</strong><ul style="margin: 0.25rem 0 0 1.25rem; padding: 0;">`;
356
+ for (const [room, count] of Object.entries(rooms)) {
357
+ html += `<li><strong>${room}:</strong> ${count} scheduled</li>`;
358
+ }
359
+ html += `</ul></div>`;
360
+ }
361
+ html += `</div>`;
362
+
363
+ Swal.fire({
364
+ title: `Scheduled Tests (${dateStr})`,
365
+ html,
366
+ icon: 'info',
367
+ confirmButtonColor: '#0f62fe',
368
+ confirmButtonText: 'Close',
369
+ });
370
+ };
371
+
372
+ const monthNames = [
373
+ 'January', 'February', 'March', 'April', 'May', 'June',
374
+ 'July', 'August', 'September', 'October', 'November', 'December',
375
+ ];
376
+
377
+ return (
378
+ <div className={styles.bodyWrapper}>
379
+ {/* Main Unified Card: Heading inside the form container as required */}
380
+ <div className={styles.card}>
381
+ <div className={styles.cardHeader}>
382
+ <h2 className={styles.formHeading}>
383
+ {t('radiologyTestSchedulingHeading', 'Radiology Test Scheduling')}
384
+ </h2>
385
+ </div>
386
+
387
+ {/* Institute and Search Filter Box */}
388
+ <div className={styles.filterBox}>
389
+ <div className={styles.row}>
390
+ <div className={`${styles.col4} ${styles.formGroup}`}>
391
+ <label className={styles.label}>
392
+ <span>{t('institute', 'Institute')}</span>
393
+ </label>
394
+ <select
395
+ className={styles.formControl}
396
+ value={selectedInstitute}
397
+ onChange={(e) => setSelectedInstitute(e.target.value)}
398
+ >
399
+ <option value="all">{t('allInstitutes', '-- All Institutes --')}</option>
400
+ {institutes.map((inst) => (
401
+ <option key={inst.uuid} value={inst.uuid}>
402
+ {inst.name}
403
+ </option>
404
+ ))}
405
+ </select>
406
+ </div>
407
+
408
+ <div className={`${styles.col8} ${styles.formGroup}`}>
409
+ <label className={styles.label}>
410
+ <span>{t('searchPendingTests', 'Search Pending Tests')}</span>
411
+ </label>
412
+ <input
413
+ type="text"
414
+ className={styles.formControl}
415
+ placeholder={t('searchPlaceholder', 'Type PHN, Test Name, Section, or Location...')}
416
+ value={searchTerm}
417
+ onChange={(e) => setSearchTerm(e.target.value)}
418
+ />
419
+ </div>
420
+ </div>
421
+ </div>
422
+
423
+ {/* Pending Tests Table */}
424
+ <div className={styles.sectionHeader}>
425
+ <span>{t('pendingSchedulingRequests', 'Pending Scheduling Requests')}</span>
426
+ <span className={`${styles.badge} ${styles.badgeType}`} style={{ marginLeft: 'auto' }}>
427
+ {filteredOrders.length} {t('requests', 'Requests')}
428
+ </span>
429
+ </div>
430
+
431
+ <div className={styles.tableContainer}>
432
+ {loadingOrders ? (
433
+ <div style={{ padding: '2rem' }}>
434
+ <Loading description="Loading pending tests..." withOverlay={false} small />
435
+ </div>
436
+ ) : filteredOrders.length === 0 ? (
437
+ <div className={styles.emptyState}>
438
+ {t('noPendingOrders', 'No pending radiology test scheduling requests found.')}
439
+ </div>
440
+ ) : (
441
+ <table className={styles.table}>
442
+ <thead>
443
+ <tr>
444
+ <th>#</th>
445
+ <th>{t('institute', 'Institute')}</th>
446
+ <th>{t('section', 'Section')}</th>
447
+ <th>{t('phn', 'PHN')}</th>
448
+ <th>{t('admittedDate', 'Admitted Date')}</th>
449
+ <th>{t('orderedBy', 'Ordered By')}</th>
450
+ <th>{t('orderedDatetime', 'Ordered Datetime')}</th>
451
+ <th>{t('testName', 'Test Name')}</th>
452
+ <th>{t('region', 'Region')}</th>
453
+ <th>{t('examRoom', 'Exam Room')}</th>
454
+ <th>{t('comment', 'Comment')}</th>
455
+ <th>{t('priority', 'Priority')}</th>
456
+ <th>{t('consultant', 'Consultant')}</th>
457
+ <th style={{ textAlign: 'center' }}>{t('action', 'Action')}</th>
458
+ </tr>
459
+ </thead>
460
+ <tbody>
461
+ {filteredOrders.map((dto, idx) => {
462
+ const isUrgent = dto.radiology.priority === 'Urgent';
463
+ const isSelected = selectedOrder?.radiology.id === dto.radiology.id;
464
+ const rowClass = `${isUrgent ? styles.urgentRow : ''} ${
465
+ isSelected ? styles.selectedRow : ''
466
+ }`.trim();
467
+
468
+ return (
469
+ <tr key={`${dto.type}-${dto.radiology.id}`} className={rowClass}>
470
+ <td>{idx + 1}</td>
471
+ <td>{dto.locationName || dto.radiology.location}</td>
472
+ <td>
473
+ <span className={`${styles.badge} ${styles.badgeType}`}>
474
+ {dto.type}
475
+ </span>
476
+ </td>
477
+ <td>
478
+ <strong>{dto.radiology.patientPhn}</strong>
479
+ </td>
480
+ <td>
481
+ {dto.radiology.occurrenceDateTime
482
+ ? dto.radiology.occurrenceDateTime.split(' ')[0]
483
+ : '-'}
484
+ </td>
485
+ <td>{dto.orderedByName || dto.radiology.requesterReference || '-'}</td>
486
+ <td>{dto.radiology.occurrenceDateTime || '-'}</td>
487
+ <td>
488
+ <strong>{dto.radiology.codeDisplay || '-'}</strong>
489
+ </td>
490
+ <td>{dto.radiology.bodySiteText || '-'}</td>
491
+ <td>
492
+ {dto.radiology.supportingInfo === '--Select--'
493
+ ? '-'
494
+ : dto.radiology.supportingInfo || '-'}
495
+ </td>
496
+ <td>{dto.radiology.note || '-'}</td>
497
+ <td>
498
+ <span
499
+ className={`${styles.badge} ${
500
+ isUrgent ? styles.badgeUrgent : styles.badgeRoutine
501
+ }`}
502
+ >
503
+ {dto.radiology.priority || 'Routine'}
504
+ </span>
505
+ </td>
506
+ <td>
507
+ {dto.radiology.requesterDisplay === '--Select--'
508
+ ? '-'
509
+ : dto.radiology.requesterDisplay || '-'}
510
+ </td>
511
+ <td style={{ textAlign: 'center' }}>
512
+ <button
513
+ type="button"
514
+ className={styles.btnSelect}
515
+ onClick={() => handleSelectRow(dto)}
516
+ >
517
+ {isSelected ? t('selected', 'Selected') : t('select', 'Select')}
518
+ </button>
519
+ </td>
520
+ </tr>
521
+ );
522
+ })}
523
+ </tbody>
524
+ </table>
525
+ )}
526
+ </div>
527
+
528
+ {/* Details and Scheduling Form */}
529
+ {selectedOrder && (
530
+ <div className={styles.detailsPanel}>
531
+ <div className={styles.row}>
532
+ {/* Patient Demographics */}
533
+ <div className={`${styles.col6} ${styles.formGroup}`}>
534
+ <div className={styles.sectionHeader}>
535
+ <span>{t('patientDemographics', 'Patient Demographics')}</span>
536
+ {selectedOrder.radiology.patientPhn && (
537
+ <span style={{ fontSize: '0.85rem', color: '#6c757d', marginLeft: 'auto' }}>
538
+ PHN: ({selectedOrder.radiology.patientPhn})
539
+ </span>
540
+ )}
541
+ </div>
542
+
543
+ {loadingPatient ? (
544
+ <Loading description="Loading patient demographics..." withOverlay={false} small />
545
+ ) : (
546
+ <div className={styles.row}>
547
+ <div className={styles.col8}>
548
+ <div className={styles.infoField}>
549
+ <span className={styles.infoLabel}>{t('fullName', 'Full Name')}:</span>
550
+ <span className={styles.infoValue}>
551
+ {patientDetails?.fullName || '-'}
552
+ </span>
553
+ </div>
554
+ <div className={styles.infoField}>
555
+ <span className={styles.infoLabel}>{t('gender', 'Gender')}:</span>
556
+ <span className={styles.infoValue}>{patientDetails?.gender || '-'}</span>
557
+ </div>
558
+ <div className={styles.infoField}>
559
+ <span className={styles.infoLabel}>{t('nicNo', 'NIC No')}:</span>
560
+ <span className={styles.infoValue}>{patientDetails?.nicNo || '-'}</span>
561
+ </div>
562
+ <div className={styles.infoField}>
563
+ <span className={styles.infoLabel}>{t('guardianInfo', 'Guardian Info')}:</span>
564
+ <span className={styles.infoValue}>
565
+ {patientDetails?.guardianInfo || '-'}
566
+ </span>
567
+ </div>
568
+ <div className={styles.infoField}>
569
+ <span className={styles.infoLabel}>{t('address', 'Address')}:</span>
570
+ <span className={styles.infoValue}>{patientDetails?.address || '-'}</span>
571
+ </div>
572
+ <div className={styles.infoField}>
573
+ <span className={styles.infoLabel}>{t('age', 'Age')}:</span>
574
+ <span className={styles.infoValue}>
575
+ Y: {patientDetails?.years || 0} | M: {patientDetails?.months || 0} | D:{' '}
576
+ {patientDetails?.days || 0}
577
+ </span>
578
+ </div>
579
+ <div className={styles.infoField}>
580
+ <span className={styles.infoLabel}>{t('dob', 'Date Of Birth')}:</span>
581
+ <span className={styles.infoValue}>
582
+ {patientDetails?.dateOfBirth || '-'}
583
+ </span>
584
+ </div>
585
+ <div className={styles.infoField}>
586
+ <span className={styles.infoLabel}>{t('telephone', 'Telephone')}:</span>
587
+ <span className={styles.infoValue}>
588
+ {[patientDetails?.telephoneHome, patientDetails?.telephoneMobile]
589
+ .filter(Boolean)
590
+ .join(', ') || '-'}
591
+ </span>
592
+ </div>
593
+ </div>
594
+
595
+ <div className={`${styles.col4} ${styles.avatarContainer}`}>
596
+ <img
597
+ className={styles.avatarImg}
598
+ src={patientDetails?.profileImage || DEFAULT_AVATAR}
599
+ alt="Patient"
600
+ onError={(e) => {
601
+ (e.currentTarget as HTMLImageElement).src = DEFAULT_AVATAR;
602
+ }}
603
+ />
604
+ </div>
605
+ </div>
606
+ )}
607
+ </div>
608
+
609
+ {/* Investigation Details */}
610
+ <div className={`${styles.col6} ${styles.formGroup}`}>
611
+ <div className={styles.sectionHeader}>
612
+ <span>{t('investigationDetails', 'Investigation Details')}</span>
613
+ </div>
614
+
615
+ <div className={styles.infoField}>
616
+ <span className={styles.infoLabel}>{t('testName', 'Test Name')}:</span>
617
+ <span className={styles.infoValue}>
618
+ {selectedOrder.radiology.codeDisplay || '-'}
619
+ </span>
620
+ </div>
621
+ <div className={styles.infoField}>
622
+ <span className={styles.infoLabel}>{t('region', 'Region')}:</span>
623
+ <span className={styles.infoValue}>
624
+ {selectedOrder.radiology.bodySiteText || '-'}
625
+ </span>
626
+ </div>
627
+ <div className={styles.infoField}>
628
+ <span className={styles.infoLabel}>{t('localization', 'Localization')}:</span>
629
+ <span className={styles.infoValue}>
630
+ {selectedOrder.radiology.specimen || '-'}
631
+ </span>
632
+ </div>
633
+ <div className={styles.infoField}>
634
+ <span className={styles.infoLabel}>{t('position', 'Position')}:</span>
635
+ <span className={styles.infoValue}>
636
+ {selectedOrder.radiology.bodySiteDisplay || '-'}
637
+ </span>
638
+ </div>
639
+ <div className={styles.infoField}>
640
+ <span className={styles.infoLabel}>{t('comment', 'Comment')}:</span>
641
+ <span className={styles.infoValue}>{selectedOrder.radiology.note || '-'}</span>
642
+ </div>
643
+ <div className={styles.infoField}>
644
+ <span className={styles.infoLabel}>{t('priority', 'Priority')}:</span>
645
+ <span
646
+ className={`${styles.badge} ${
647
+ selectedOrder.radiology.priority === 'Urgent'
648
+ ? styles.badgeUrgent
649
+ : styles.badgeRoutine
650
+ }`}
651
+ >
652
+ {selectedOrder.radiology.priority || 'Routine'}
653
+ </span>
654
+ </div>
655
+ <div className={styles.infoField}>
656
+ <span className={styles.infoLabel}>{t('consultant', 'Consultant')}:</span>
657
+ <span className={styles.infoValue}>
658
+ {selectedOrder.radiology.requesterDisplay === '--Select--'
659
+ ? '-'
660
+ : selectedOrder.radiology.requesterDisplay || '-'}
661
+ </span>
662
+ </div>
663
+ <div className={styles.infoField}>
664
+ <span className={styles.infoLabel}>{t('orderedDateTime', 'Ordered Date & Time')}:</span>
665
+ <span className={styles.infoValue}>
666
+ {selectedOrder.radiology.occurrenceDateTime || '-'}
667
+ </span>
668
+ </div>
669
+ </div>
670
+ </div>
671
+
672
+ {/* Scheduling Form Inputs */}
673
+ <div className={styles.row} style={{ marginTop: '1.25rem' }}>
674
+ <div className={`${styles.col6} ${styles.formGroup}`}>
675
+ <label className={styles.label}>
676
+ <span>{t('scheduledDateTime', 'Scheduled Date Time')} *</span>
677
+ </label>
678
+ <input
679
+ type="datetime-local"
680
+ className={styles.formControl}
681
+ value={scheduledDateTime}
682
+ onChange={(e) => setScheduledDateTime(e.target.value)}
683
+ required
684
+ />
685
+ </div>
686
+
687
+ <div className={`${styles.col6} ${styles.formGroup}`}>
688
+ <label className={styles.label}>
689
+ <span>{t('remarks', 'Remarks')}</span>
690
+ </label>
691
+ <textarea
692
+ className={styles.formControl}
693
+ rows={2}
694
+ placeholder={t('enterRemarks', 'Add scheduling remarks or instructions...')}
695
+ value={remarks}
696
+ onChange={(e) => setRemarks(e.target.value)}
697
+ />
698
+ </div>
699
+ </div>
700
+
701
+ {/* Action Buttons: Save button is prominent blue as requested */}
702
+ <div className={styles.buttonGroup}>
703
+ <button
704
+ type="button"
705
+ className={styles.btnPrimary}
706
+ onClick={handleSave}
707
+ disabled={isSaving}
708
+ >
709
+ {isSaving ? t('saving', 'Saving...') : t('saveSchedule', 'Save Schedule')}
710
+ </button>
711
+ <button
712
+ type="button"
713
+ className={styles.btnCancel}
714
+ onClick={handleClear}
715
+ disabled={isSaving}
716
+ >
717
+ {t('cancel', 'Cancel')}
718
+ </button>
719
+ </div>
720
+ </div>
721
+ )}
722
+
723
+ {/* Calendar Overview Section */}
724
+ <div style={{ marginTop: '2.5rem' }}>
725
+ <div className={styles.sectionHeader}>
726
+ <span>{t('scheduledCalendarOverview', 'Scheduled Calendar Overview')}</span>
727
+ </div>
728
+
729
+ <div className={styles.row} style={{ marginBottom: '1rem' }}>
730
+ <div className={`${styles.col4} ${styles.formGroup}`}>
731
+ <label className={styles.label}>
732
+ <span>{t('modality', 'Modality')}</span>
733
+ </label>
734
+ <select
735
+ className={styles.formControl}
736
+ value={selectedModality}
737
+ onChange={(e) => setSelectedModality(e.target.value)}
738
+ >
739
+ <option value="all">{t('allModalities', '-- All Modalities --')}</option>
740
+ <option value="Xray">Xray</option>
741
+ <option value="CT">CT</option>
742
+ <option value="MRI">MRI</option>
743
+ </select>
744
+ </div>
745
+
746
+ <div className={`${styles.col4} ${styles.formGroup}`}>
747
+ <label className={styles.label}>
748
+ <span>{t('examRoom', 'Exam Room')}</span>
749
+ </label>
750
+ <select
751
+ className={styles.formControl}
752
+ value={selectedExamRoom}
753
+ onChange={(e) => setSelectedExamRoom(e.target.value)}
754
+ disabled={selectedModality === 'all'}
755
+ >
756
+ <option value="all">{t('allExamRooms', '-- All Exam Rooms --')}</option>
757
+ {examRooms.map((room, i) => (
758
+ <option key={`${room.name}-${i}`} value={room.name}>
759
+ {room.name}
760
+ </option>
761
+ ))}
762
+ </select>
763
+ </div>
764
+ </div>
765
+
766
+ {/* Interactive Calendar Grid */}
767
+ <div className={styles.calendarWrapper}>
768
+ <div className={styles.calendarNav}>
769
+ <button
770
+ type="button"
771
+ className={styles.calendarNavBtn}
772
+ onClick={() =>
773
+ setCalendarDate(
774
+ new Date(calendarDate.getFullYear(), calendarDate.getMonth() - 1, 1),
775
+ )
776
+ }
777
+ >
778
+ &larr; {t('previous', 'Previous')}
779
+ </button>
780
+ <span className={styles.calendarMonthTitle}>
781
+ {monthNames[calendarDate.getMonth()]} {calendarDate.getFullYear()}
782
+ </span>
783
+ <div style={{ display: 'flex', gap: '0.5rem' }}>
784
+ <button
785
+ type="button"
786
+ className={styles.calendarNavBtn}
787
+ onClick={() => setCalendarDate(new Date())}
788
+ >
789
+ {t('today', 'Today')}
790
+ </button>
791
+ <button
792
+ type="button"
793
+ className={styles.calendarNavBtn}
794
+ onClick={() =>
795
+ setCalendarDate(
796
+ new Date(calendarDate.getFullYear(), calendarDate.getMonth() + 1, 1),
797
+ )
798
+ }
799
+ >
800
+ {t('next', 'Next')} &rarr;
801
+ </button>
802
+ </div>
803
+ </div>
804
+
805
+ {loadingCalendar ? (
806
+ <div style={{ padding: '2rem' }}>
807
+ <Loading description="Loading calendar appointments..." withOverlay={false} small />
808
+ </div>
809
+ ) : (
810
+ <div className={styles.calendarGrid}>
811
+ {['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'].map((d) => (
812
+ <div key={d} className={styles.calendarDayHeader}>
813
+ {d}
814
+ </div>
815
+ ))}
816
+
817
+ {calendarDays.map((cell, idx) => {
818
+ const cellClass = `${styles.calendarCell} ${
819
+ !cell.isCurrentMonth ? styles.calendarCellInactive : ''
820
+ }`.trim();
821
+
822
+ // Group cell orders by category
823
+ const categoryCounts: Record<string, number> = {};
824
+ for (const ord of cell.orders) {
825
+ const cat = ord.categoryDisplay || 'Other';
826
+ categoryCounts[cat] = (categoryCounts[cat] || 0) + 1;
827
+ }
828
+
829
+ return (
830
+ <div
831
+ key={idx}
832
+ className={cellClass}
833
+ onClick={() =>
834
+ cell.orders.length > 0 &&
835
+ handleCalendarEventClick(cell.dateStr, cell.orders)
836
+ }
837
+ style={{ cursor: cell.orders.length > 0 ? 'pointer' : 'default' }}
838
+ >
839
+ <span
840
+ className={`${styles.calendarDayNumber} ${
841
+ cell.isToday ? styles.calendarDayToday : ''
842
+ }`}
843
+ >
844
+ {cell.dayNum}
845
+ </span>
846
+
847
+ {Object.entries(categoryCounts).map(([cat, count]) => {
848
+ let badgeCls = styles.badgeOther;
849
+ if (cat === 'Xray') badgeCls = styles.badgeXray;
850
+ else if (cat === 'CT') badgeCls = styles.badgeCT;
851
+ else if (cat === 'MRI') badgeCls = styles.badgeMRI;
852
+
853
+ return (
854
+ <span
855
+ key={cat}
856
+ className={`${styles.calendarBadge} ${badgeCls}`}
857
+ title={`${cat}: ${count} scheduled. Click to see details.`}
858
+ >
859
+ {cat}: {count}
860
+ </span>
861
+ );
862
+ })}
863
+ </div>
864
+ );
865
+ })}
866
+ </div>
867
+ )}
868
+ </div>
869
+ </div>
870
+ </div>
871
+ </div>
872
+ );
873
+ };
874
+
875
+ export default RadiologyTestScheduling;