ui-soxo-bootstrap-core 2.6.40-dev.20 → 2.6.40-dev.21
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.
|
@@ -211,7 +211,9 @@ function FormCreator({
|
|
|
211
211
|
// Keep the same value preparation path for normal submit and search reset.
|
|
212
212
|
fields.forEach((field) => {
|
|
213
213
|
|
|
214
|
-
|
|
214
|
+
// Only actual date inputs are converted , a field can be named with date
|
|
215
|
+
// and still hold a plain value ( eg : date_filter_type select )
|
|
216
|
+
if (field.field && field.field.includes('date') && ['date', 'datetime'].indexOf(field.type) !== -1) {
|
|
215
217
|
|
|
216
218
|
nextValues[field.field] = moment(nextValues[field.field]).valueOf();
|
|
217
219
|
|
|
@@ -530,7 +532,7 @@ function UserInput({ field, onUpload, selectedInformation, onChange, onSearchRes
|
|
|
530
532
|
return (
|
|
531
533
|
<Select defaultValue={defaultValue}
|
|
532
534
|
required={field.required}
|
|
533
|
-
style={{
|
|
535
|
+
style={{ minWidth: '160px' }}
|
|
534
536
|
onChange={(value) => onChange(field, value)}
|
|
535
537
|
>
|
|
536
538
|
{field.options.map((option, key) => (
|
|
@@ -1,32 +1,22 @@
|
|
|
1
1
|
import React, { useState, useEffect, useContext, useRef } from 'react';
|
|
2
2
|
|
|
3
|
-
import { Table, Skeleton, Input, Modal, message,
|
|
3
|
+
import { Table, Skeleton, Input, Modal, message, Tag } from 'antd';
|
|
4
4
|
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
import { Location, FormCreator, GlobalContext, ExportReactCSV, getExportData, Card, TableComponent, QrScanner } from './../../../../lib/';
|
|
5
|
+
import { Location, FormCreator, GlobalContext, Card } from './../../../../lib/';
|
|
8
6
|
|
|
9
7
|
import { CoreScripts } from './../../../../models/';
|
|
10
8
|
|
|
11
9
|
import moment from 'moment-timezone';
|
|
12
10
|
|
|
13
|
-
import Button from '../../../../lib/elements/basic/button/button';
|
|
14
|
-
|
|
15
11
|
import './reporting-dashboard.scss';
|
|
16
12
|
|
|
17
13
|
// import MenuDashBoard from '../../../../pages/homepage-api/menu-dashboard';
|
|
18
14
|
import MenuDashBoardComponent from '../../../../lib/elements/basic/menu-dashboard/menu-dashboard';
|
|
19
|
-
import { useHistory } from 'react-router-dom';
|
|
20
|
-
import * as ReportingDashboardComp from '../index';
|
|
21
|
-
import buildDisplayColumns from './display-columns/build-display-columns';
|
|
22
|
-
import { getRedirectLink } from './display-columns/display-cell-renderer';
|
|
23
|
-
import AdvancedSearchSelect from './adavance-search/advance-search';
|
|
24
|
-
|
|
25
|
-
// import { isPdfFile } from 'pdfjs-dist';
|
|
26
15
|
|
|
27
|
-
|
|
16
|
+
import ReportingTable from './reporting-table';
|
|
28
17
|
|
|
29
|
-
|
|
18
|
+
// Input parameter that decides which date column the report is filtered by
|
|
19
|
+
const DATE_FILTER_FIELD = 'date_filter_type';
|
|
30
20
|
|
|
31
21
|
/**
|
|
32
22
|
* ReportingDashboard component renders the dashboard and handles patient details,
|
|
@@ -56,7 +46,7 @@ export default function ReportingDashboard({
|
|
|
56
46
|
const [config, setConfig] = useState({});
|
|
57
47
|
|
|
58
48
|
// State to manage the layout of the form
|
|
59
|
-
const [formLayout
|
|
49
|
+
const [formLayout] = useState('inline');
|
|
60
50
|
|
|
61
51
|
const [loading, setLoading] = useState(true);
|
|
62
52
|
|
|
@@ -79,7 +69,7 @@ export default function ReportingDashboard({
|
|
|
79
69
|
|
|
80
70
|
//In case of reports from core_script , there will be id from params
|
|
81
71
|
// In case of normal menu we need to take id from props
|
|
82
|
-
let id = reportId ? reportId : match
|
|
72
|
+
let id = reportId ? reportId : match?.params?.id;
|
|
83
73
|
|
|
84
74
|
const { CustomModels = {} } = useContext(GlobalContext);
|
|
85
75
|
|
|
@@ -87,9 +77,9 @@ export default function ReportingDashboard({
|
|
|
87
77
|
|
|
88
78
|
const [columns, setColumns] = useState([]); // To set columns
|
|
89
79
|
|
|
90
|
-
const [
|
|
80
|
+
const [, setSummaryColumns] = useState([]);
|
|
91
81
|
|
|
92
|
-
const [
|
|
82
|
+
const [reportRequestPayload, setReportRequestPayload] = useState(null);
|
|
93
83
|
|
|
94
84
|
const urlParams = Location.search();
|
|
95
85
|
|
|
@@ -110,7 +100,6 @@ export default function ReportingDashboard({
|
|
|
110
100
|
* @returns {Promise<void>} A promise that resolves when the patient details have been fetched and the state has been updated.
|
|
111
101
|
*/
|
|
112
102
|
async function getPatientDetails(idOverride) {
|
|
113
|
-
setPatients([]);
|
|
114
103
|
const fetchId = idOverride || id;
|
|
115
104
|
await CoreScripts.getRecord({ id: fetchId, dbPtr }).then(async ({ result }) => {
|
|
116
105
|
// Check if display columns are provided from backend
|
|
@@ -188,6 +177,9 @@ export default function ReportingDashboard({
|
|
|
188
177
|
if (urlParams[record.field]) {
|
|
189
178
|
if (record.type === 'date') {
|
|
190
179
|
formContent[record.field] = moment.utc(urlParams[record.field]);
|
|
180
|
+
} else if (record.type !== 'search') {
|
|
181
|
+
// Restore plain values ( eg : select filters ) back from the url
|
|
182
|
+
formContent[record.field] = urlParams[record.field];
|
|
191
183
|
}
|
|
192
184
|
|
|
193
185
|
// return formContent;
|
|
@@ -215,6 +207,10 @@ export default function ReportingDashboard({
|
|
|
215
207
|
break;
|
|
216
208
|
|
|
217
209
|
default:
|
|
210
|
+
// Any other default is taken as the value itself ( eg : select filters )
|
|
211
|
+
if (record.default !== undefined && record.default !== null && record.type !== 'date') {
|
|
212
|
+
formContent[record.field] = record.default;
|
|
213
|
+
}
|
|
218
214
|
break;
|
|
219
215
|
}
|
|
220
216
|
}
|
|
@@ -223,6 +219,11 @@ export default function ReportingDashboard({
|
|
|
223
219
|
if (record.type === 'date' && !formContent[record.field]) {
|
|
224
220
|
formContent[record.field] = moment().tz(process.env.REACT_APP_TIMEZONE);
|
|
225
221
|
}
|
|
222
|
+
|
|
223
|
+
// For a static select with no value yet , preselect the first option
|
|
224
|
+
if (record.type === 'select' && Array.isArray(record.options) && formContent[record.field] === undefined) {
|
|
225
|
+
formContent[record.field] = record.options[0]?.valueMember;
|
|
226
|
+
}
|
|
226
227
|
if (record.type === 'search') {
|
|
227
228
|
if (!formContent[record.field]) formContent[record.field] = [];
|
|
228
229
|
return {
|
|
@@ -262,95 +263,57 @@ export default function ReportingDashboard({
|
|
|
262
263
|
// If enabled, clear the details array
|
|
263
264
|
setDetails([]);
|
|
264
265
|
} else {
|
|
266
|
+
// The date filter input is shown only when the filter is enabled in otherDetails .
|
|
267
|
+
// Otherwise it stays hidden holding its default value , so the report keeps filtering
|
|
268
|
+
// on the default date
|
|
269
|
+
const filterKey = otherDetails ? Object.keys(otherDetails).find((key) => key.toLowerCase().trim() === 'enableuserfilter') : null;
|
|
270
|
+
|
|
271
|
+
const isFilterEnabled = filterKey ? otherDetails[filterKey] === true || otherDetails[filterKey] === 'true' : false;
|
|
272
|
+
|
|
265
273
|
// Keep all parameters with a type (including search) to render in FormCreator
|
|
266
|
-
setDetails([
|
|
274
|
+
setDetails([
|
|
275
|
+
...parameters
|
|
276
|
+
.filter((ele) => ele.type)
|
|
277
|
+
.map((ele) => (ele.field === DATE_FILTER_FIELD ? { ...ele, visible: isFilterEnabled } : ele)),
|
|
278
|
+
]);
|
|
267
279
|
}
|
|
268
280
|
}
|
|
269
281
|
|
|
270
282
|
// Refresh patient details.
|
|
271
283
|
|
|
272
284
|
function refresh() {
|
|
273
|
-
getPatientDetails();
|
|
285
|
+
getPatientDetails(scriptId.current || id);
|
|
274
286
|
}
|
|
275
287
|
|
|
276
|
-
const
|
|
277
|
-
const
|
|
278
|
-
|
|
279
|
-
const coreScriptId = scriptId.current ? scriptId.current : id;
|
|
280
|
-
const normalizedColumns = Array.isArray(parsedColumns) ? parsedColumns : Array.isArray(columns) ? columns : [];
|
|
288
|
+
const buildReportRequestPayload = (values = {}, paginationOverride) => {
|
|
289
|
+
const pager = paginationOverride || pagination;
|
|
290
|
+
const formattedValues = {};
|
|
281
291
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
};
|
|
295
|
-
// Combine form data + pagination
|
|
296
|
-
let formBody = {
|
|
292
|
+
Object.keys(values || {}).forEach((key) => {
|
|
293
|
+
const val = values[key];
|
|
294
|
+
formattedValues[key] = moment.isMoment(val) ? val.format('YYYY-MM-DD') : val;
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
const paginationData = {
|
|
298
|
+
page: pager.current || 1,
|
|
299
|
+
limit: pager.pageSize || 10,
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
if (scope) {
|
|
303
|
+
return {
|
|
297
304
|
body: {
|
|
298
|
-
...
|
|
305
|
+
...scope,
|
|
299
306
|
...paginationData,
|
|
300
307
|
},
|
|
301
308
|
};
|
|
302
|
-
// Optional override if `scope` exists
|
|
303
|
-
if (scope) {
|
|
304
|
-
formBody = { body: { ...scope, ...paginationData } };
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
setReportMailRequest({
|
|
308
|
-
scriptId: coreScriptId,
|
|
309
|
-
inputParameters: formBody,
|
|
310
|
-
});
|
|
311
|
-
|
|
312
|
-
// Fetch result
|
|
313
|
-
const result = await CoreScripts.getReportingLisitng(coreScriptId, formBody, dbPtr);
|
|
314
|
-
|
|
315
|
-
const apiData = Array.isArray(result) ? result : Array.isArray(result?.result) ? result.result : [];
|
|
316
|
-
|
|
317
|
-
// Handle both result formats
|
|
318
|
-
let resultDetails = apiData[0] || [];
|
|
319
|
-
if (result?.result && result?.result[0]) {
|
|
320
|
-
resultDetails = result.result[0];
|
|
321
|
-
}
|
|
322
|
-
// Update patients
|
|
323
|
-
setPatients(resultDetails || []);
|
|
324
|
-
|
|
325
|
-
// When display_columns is missing, build columns from the response keys.
|
|
326
|
-
if (normalizedColumns.length === 0 && resultDetails.length > 0) {
|
|
327
|
-
// Create columns dynamically from resultDetails keys
|
|
328
|
-
setColumns((prev) => {
|
|
329
|
-
if (prev.length > 0) return prev;
|
|
330
|
-
return Object.keys(resultDetails[0]).map((key) => ({
|
|
331
|
-
title: key,
|
|
332
|
-
field: key,
|
|
333
|
-
}));
|
|
334
|
-
});
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
if (result.length) {
|
|
338
|
-
// Set Pgination data into URL
|
|
339
|
-
Location.search({ ...Location.search(), current, pageSize });
|
|
340
|
-
|
|
341
|
-
setPagination((prev) => ({
|
|
342
|
-
...prev,
|
|
343
|
-
current: pagination.current,
|
|
344
|
-
pageSize: pagination.pageSize,
|
|
345
|
-
total: resultDetails?.[0]?.TotalCount ?? pagination?.total,
|
|
346
|
-
}));
|
|
347
|
-
}
|
|
348
|
-
} catch (error) {
|
|
349
|
-
console.error('Error fetching report data:', error);
|
|
350
|
-
} finally {
|
|
351
|
-
// Always runs, success or error
|
|
352
|
-
setLoading(false);
|
|
353
309
|
}
|
|
310
|
+
|
|
311
|
+
return {
|
|
312
|
+
body: {
|
|
313
|
+
...formattedValues,
|
|
314
|
+
...paginationData,
|
|
315
|
+
},
|
|
316
|
+
};
|
|
354
317
|
};
|
|
355
318
|
|
|
356
319
|
const handleSubmit = (values) => {
|
|
@@ -561,17 +524,12 @@ export default function ReportingDashboard({
|
|
|
561
524
|
|
|
562
525
|
// Call API
|
|
563
526
|
try {
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
// Pagination Handler
|
|
572
|
-
const handlePagination = async (newPagination) => {
|
|
573
|
-
try {
|
|
574
|
-
await fetchReportData(id, formContents, dbPtr, newPagination);
|
|
527
|
+
setPagination((prev) => ({
|
|
528
|
+
...prev,
|
|
529
|
+
current: paginationData.current,
|
|
530
|
+
pageSize: paginationData.pageSize,
|
|
531
|
+
}));
|
|
532
|
+
setReportRequestPayload(buildReportRequestPayload(values, paginationData));
|
|
575
533
|
} finally {
|
|
576
534
|
setLoading(false);
|
|
577
535
|
setCardLoading(false);
|
|
@@ -659,459 +617,36 @@ export default function ReportingDashboard({
|
|
|
659
617
|
) : null}
|
|
660
618
|
</div>
|
|
661
619
|
|
|
662
|
-
|
|
663
|
-
<GuestList
|
|
664
|
-
patients={patients}
|
|
620
|
+
<ReportingTable
|
|
665
621
|
columns={columns}
|
|
666
|
-
summaryColumns={summaryColumns}
|
|
667
622
|
isFixedIndex={isFixedIndex}
|
|
668
623
|
showScanner={showScanner}
|
|
624
|
+
reportId={reportId}
|
|
625
|
+
requestId={scriptId.current ? scriptId.current : id}
|
|
626
|
+
requestPayload={reportRequestPayload}
|
|
627
|
+
dbPtr={dbPtr}
|
|
669
628
|
barcodeFilterKey={barcodeFilterKey}
|
|
670
|
-
CustomComponents={
|
|
629
|
+
CustomComponents={CustomComponents}
|
|
671
630
|
refresh={refresh}
|
|
672
631
|
config={config}
|
|
632
|
+
|
|
673
633
|
loading={cardLoading}
|
|
674
634
|
pagination={pagination}
|
|
675
|
-
|
|
635
|
+
onPaginationChange={(nextPagination) => {
|
|
636
|
+
Location.search({
|
|
637
|
+
...Location.search(),
|
|
638
|
+
current: nextPagination.current,
|
|
639
|
+
pageSize: nextPagination.pageSize,
|
|
640
|
+
});
|
|
641
|
+
setPagination((prev) => ({
|
|
642
|
+
...prev,
|
|
643
|
+
...nextPagination,
|
|
644
|
+
}));
|
|
645
|
+
}}
|
|
676
646
|
attributes={attributes}
|
|
677
|
-
selectedSearchFields={selectedSearchFields}
|
|
678
|
-
handleRemoveSearchField={handleRemoveSearchField}
|
|
679
|
-
fetchReportData={(paginationUpdate) => fetchReportData(id, formContents, dbPtr, paginationUpdate || pagination)}
|
|
680
|
-
reportScriptId={reportMailRequest.scriptId}
|
|
681
|
-
reportInputParameters={reportMailRequest.inputParameters}
|
|
682
647
|
/>
|
|
683
|
-
{/** GuestList component end*/}
|
|
684
648
|
</>
|
|
685
649
|
)}
|
|
686
650
|
</Card>
|
|
687
651
|
);
|
|
688
652
|
}
|
|
689
|
-
|
|
690
|
-
/**
|
|
691
|
-
* Filters report records using the same search text that drives the table.
|
|
692
|
-
*
|
|
693
|
-
* @param {Array<Object>} records - Report rows.
|
|
694
|
-
* @param {string} searchText - Current table search text.
|
|
695
|
-
* @returns {Array<Object>} Rows matching the search text.
|
|
696
|
-
*/
|
|
697
|
-
function filterReportRecords(records, searchText) {
|
|
698
|
-
if (!Array.isArray(records)) return [];
|
|
699
|
-
|
|
700
|
-
const query = String(searchText || '').trim().toLowerCase();
|
|
701
|
-
|
|
702
|
-
if (!query) return records;
|
|
703
|
-
|
|
704
|
-
return records.filter((record) =>
|
|
705
|
-
Object.values(record).some((value) => {
|
|
706
|
-
if (value === undefined || value === null || typeof value === 'object') return false;
|
|
707
|
-
|
|
708
|
-
return String(value).toLowerCase().indexOf(query) !== -1;
|
|
709
|
-
})
|
|
710
|
-
);
|
|
711
|
-
}
|
|
712
|
-
|
|
713
|
-
/**
|
|
714
|
-
*
|
|
715
|
-
* @param root0
|
|
716
|
-
* @param root0.patients
|
|
717
|
-
* @param root0.CustomComponents
|
|
718
|
-
* @param root0.summaryColumns
|
|
719
|
-
* @param root0.refresh
|
|
720
|
-
* @param root0.isFixedIndex
|
|
721
|
-
* @returns {*}
|
|
722
|
-
*/
|
|
723
|
-
//Renders a table displaying a list of patients with dynamic columns
|
|
724
|
-
function GuestList({
|
|
725
|
-
patients,
|
|
726
|
-
columns,
|
|
727
|
-
loading,
|
|
728
|
-
CustomComponents,
|
|
729
|
-
refresh,
|
|
730
|
-
isFixedIndex,
|
|
731
|
-
barcodeFilterKey,
|
|
732
|
-
showScanner,
|
|
733
|
-
config,
|
|
734
|
-
pagination,
|
|
735
|
-
handlePagination,
|
|
736
|
-
attributes,
|
|
737
|
-
selectedSearchFields,
|
|
738
|
-
handleRemoveSearchField,
|
|
739
|
-
fetchReportData,
|
|
740
|
-
reportScriptId,
|
|
741
|
-
reportInputParameters,
|
|
742
|
-
}) {
|
|
743
|
-
/**
|
|
744
|
-
* @param {*} propValues
|
|
745
|
-
*/
|
|
746
|
-
const propValues = (attributes && JSON.parse(attributes)) || {};
|
|
747
|
-
|
|
748
|
-
const { buttonAttributes = [] } = propValues;
|
|
749
|
-
|
|
750
|
-
var [query, setQuery] = useState('');
|
|
751
|
-
|
|
752
|
-
const [exportData, setExportData] = useState({});
|
|
753
|
-
|
|
754
|
-
// const [data, setData] = useState([]);
|
|
755
|
-
|
|
756
|
-
//visibility of the QR scanner modal.
|
|
757
|
-
const [isScannerVisible, setScannerVisible] = useState(false);
|
|
758
|
-
|
|
759
|
-
// Stores the patients filtered specifically by QR scan match.
|
|
760
|
-
const [filteredPatients, setFilteredPatients] = useState([]); // Show all initially
|
|
761
|
-
|
|
762
|
-
// patient object to redirect to upon successful QR scan.
|
|
763
|
-
const [redirectPatient, setRedirectPatient] = useState(null);
|
|
764
|
-
|
|
765
|
-
const [visible, setVisible] = useState(false);
|
|
766
|
-
|
|
767
|
-
const [ActiveComponent, setActiveComponent] = useState(null);
|
|
768
|
-
|
|
769
|
-
let history = useHistory();
|
|
770
|
-
|
|
771
|
-
const { isMobile, dispatch } = useContext(GlobalContext);
|
|
772
|
-
const [single, setSingle] = useState({});
|
|
773
|
-
const otherDetails = config.other_details1 ? JSON.parse(config.other_details1) : {};
|
|
774
|
-
|
|
775
|
-
// const otherDetails = config.other_details1 ? JSON.parse(config.other_details1) : {};
|
|
776
|
-
// const [view, setView] = useState(isMobile ? true : false); //Need to check this condition
|
|
777
|
-
const cols = buildDisplayColumns({
|
|
778
|
-
columns,
|
|
779
|
-
patients,
|
|
780
|
-
isFixedIndex,
|
|
781
|
-
CustomComponents,
|
|
782
|
-
refresh,
|
|
783
|
-
otherDetails,
|
|
784
|
-
});
|
|
785
|
-
|
|
786
|
-
/**
|
|
787
|
-
*
|
|
788
|
-
* @param {*} result
|
|
789
|
-
*/
|
|
790
|
-
|
|
791
|
-
// function changeView(result) {
|
|
792
|
-
// setView(result);
|
|
793
|
-
// }
|
|
794
|
-
|
|
795
|
-
/**
|
|
796
|
-
*
|
|
797
|
-
* @param {*} event
|
|
798
|
-
*/
|
|
799
|
-
|
|
800
|
-
function onSearch(event) {
|
|
801
|
-
setQuery(event.target.value);
|
|
802
|
-
}
|
|
803
|
-
|
|
804
|
-
/**
|
|
805
|
-
*
|
|
806
|
-
*/
|
|
807
|
-
|
|
808
|
-
useEffect(() => {
|
|
809
|
-
//Cheaking if there is patient data exists
|
|
810
|
-
if (patients) {
|
|
811
|
-
// let data = patients?.map((entry) => {
|
|
812
|
-
// entry.rowIndex = entry.opb_id;
|
|
813
|
-
|
|
814
|
-
// entry.dispatch = dispatch;
|
|
815
|
-
|
|
816
|
-
// return entry;
|
|
817
|
-
// });
|
|
818
|
-
|
|
819
|
-
// setData(data);
|
|
820
|
-
|
|
821
|
-
// Define export data
|
|
822
|
-
// Sanitize cols for export to ensure titles are strings
|
|
823
|
-
const exportCols = cols.map((col) => {
|
|
824
|
-
if (col.title && typeof col.title === 'object' && col.title.props) {
|
|
825
|
-
return { ...col, title: col.title.props.title };
|
|
826
|
-
}
|
|
827
|
-
return col;
|
|
828
|
-
});
|
|
829
|
-
const summaryCols = columns.filter((col) => col.enable_summary);
|
|
830
|
-
let dataToExport = [...filterReportRecords(patients, query)];
|
|
831
|
-
|
|
832
|
-
if (summaryCols.length > 0) {
|
|
833
|
-
// Build one synthetic row for CSV export that mirrors the table layout:
|
|
834
|
-
// numeric summary cells are populated from `calculateSummaryValues`, while
|
|
835
|
-
// non-summary columns stay blank unless a configured caption should be shown.
|
|
836
|
-
const summaryValues = calculateSummaryValues(summaryCols, dataToExport);
|
|
837
|
-
const summaryRow = { isSummaryRow: true };
|
|
838
|
-
|
|
839
|
-
cols.forEach((col, index) => {
|
|
840
|
-
// Start each export column empty so the appended row keeps the same shape
|
|
841
|
-
// as the data rows and does not leak index/helper values into the export.
|
|
842
|
-
const colKey = col.field || col.key || col.dataIndex;
|
|
843
|
-
if (colKey && !summaryRow[colKey]) {
|
|
844
|
-
summaryRow[colKey] = '';
|
|
845
|
-
}
|
|
846
|
-
|
|
847
|
-
if (summaryValues[col.field] !== undefined) {
|
|
848
|
-
// Fill columns that have an aggregate configured (sum, count, avg, etc.).
|
|
849
|
-
summaryRow[col.field] = summaryValues[col.field];
|
|
850
|
-
} else {
|
|
851
|
-
// If this column is marked as the caption target for a summary column,
|
|
852
|
-
// place the configured label (for example "Total") into that cell.
|
|
853
|
-
const captionConfig = columns.find((c) => col.field && c.caption_field === col.field);
|
|
854
|
-
if (captionConfig) {
|
|
855
|
-
summaryRow[col.field] = captionConfig.summary_caption || '';
|
|
856
|
-
}
|
|
857
|
-
}
|
|
858
|
-
});
|
|
859
|
-
dataToExport.push(summaryRow);
|
|
860
|
-
}
|
|
861
|
-
let exportDatas = getExportData(dataToExport, exportCols);
|
|
862
|
-
|
|
863
|
-
if (exportDatas.exportDataHeaders.length) {
|
|
864
|
-
setExportData({ exportDatas });
|
|
865
|
-
} else {
|
|
866
|
-
setExportData({});
|
|
867
|
-
}
|
|
868
|
-
}
|
|
869
|
-
}, [patients, columns, query]);
|
|
870
|
-
|
|
871
|
-
let filtered = filterReportRecords(patients, query);
|
|
872
|
-
|
|
873
|
-
/**
|
|
874
|
-
* Checks for a match in the filtered patient list based on a scanned code,
|
|
875
|
-
* updates the relevant state if a match is found, and redirects to the
|
|
876
|
-
* patient's detail page. Displays a warning if no match is found.
|
|
877
|
-
*
|
|
878
|
-
* @param {string} code - The scanned code to match against a specific field of each patient.
|
|
879
|
-
*/
|
|
880
|
-
|
|
881
|
-
const handleScanSuccess = (code) => {
|
|
882
|
-
// Filters patients based on the scanned code and the selected barcode key(using attributes 'barcodeFilterKey')
|
|
883
|
-
const matched = filtered.filter((patient) => patient[barcodeFilterKey] === code);
|
|
884
|
-
|
|
885
|
-
if (matched.length) {
|
|
886
|
-
const patient = matched[0];
|
|
887
|
-
setFilteredPatients(matched);
|
|
888
|
-
setRedirectPatient(matched);
|
|
889
|
-
message.success(`Match found for ${code}, redirecting...`);
|
|
890
|
-
|
|
891
|
-
const actionColumn = columns.find((col) => col.field === 'action') || columns.find((col) => col.type === 'action');
|
|
892
|
-
if (actionColumn) {
|
|
893
|
-
const redirectLink = getRedirectLink(actionColumn, patient);
|
|
894
|
-
// history.push(redirectLink);
|
|
895
|
-
window.location.href = redirectLink;
|
|
896
|
-
}
|
|
897
|
-
} else {
|
|
898
|
-
Modal.warning({
|
|
899
|
-
title: 'No matching records.',
|
|
900
|
-
content: `No match for scanned code: ${code}`,
|
|
901
|
-
});
|
|
902
|
-
}
|
|
903
|
-
};
|
|
904
|
-
|
|
905
|
-
//open the edit modal
|
|
906
|
-
const handleOpenEdit = (button) => {
|
|
907
|
-
const componentName = button.component;
|
|
908
|
-
const ComponentToRender = ReportingDashboardComp[componentName];
|
|
909
|
-
|
|
910
|
-
if (!ComponentToRender) {
|
|
911
|
-
console.error(`Component ${componentName} not found!`);
|
|
912
|
-
return;
|
|
913
|
-
}
|
|
914
|
-
|
|
915
|
-
setSingle({});
|
|
916
|
-
setActiveComponent(() => ComponentToRender);
|
|
917
|
-
setVisible(true);
|
|
918
|
-
};
|
|
919
|
-
|
|
920
|
-
// close the edit modal
|
|
921
|
-
const handleCloseEdit = () => {
|
|
922
|
-
setShowEdit(false);
|
|
923
|
-
};
|
|
924
|
-
/**
|
|
925
|
-
* Calculates aggregate values for the configured summary columns.
|
|
926
|
-
*
|
|
927
|
-
* Each summary definition contributes one value keyed by its `field`. Missing
|
|
928
|
-
* row values are treated as `0` for numeric operations so the table summary and
|
|
929
|
-
* export summary row can be built from the same result object.
|
|
930
|
-
*
|
|
931
|
-
* Supported functions:
|
|
932
|
-
* `sum` - totals all numeric values in the field.
|
|
933
|
-
* `count` - returns the number of rows in the current dataset.
|
|
934
|
-
* `avg` - returns the arithmetic mean of the field values.
|
|
935
|
-
* `min` - returns the smallest numeric value in the field.
|
|
936
|
-
* `max` - returns the largest numeric value in the field.
|
|
937
|
-
*
|
|
938
|
-
* @param {Array<Object>} summaryCols - Column configs with `field` and `function`.
|
|
939
|
-
* @param {Array<Object>} pageData - Rows currently being summarized.
|
|
940
|
-
* @returns {Object} Aggregate values keyed by field name.
|
|
941
|
-
*/
|
|
942
|
-
function calculateSummaryValues(summaryCols, pageData) {
|
|
943
|
-
const summaryValues = {};
|
|
944
|
-
|
|
945
|
-
summaryCols.forEach((col) => {
|
|
946
|
-
const field = col.field;
|
|
947
|
-
|
|
948
|
-
if (col.function === 'sum') {
|
|
949
|
-
summaryValues[field] = pageData.reduce((total, row) => total + Number(row[field] || 0), 0);
|
|
950
|
-
}
|
|
951
|
-
|
|
952
|
-
if (col.function === 'count') {
|
|
953
|
-
summaryValues[field] = pageData.length;
|
|
954
|
-
}
|
|
955
|
-
|
|
956
|
-
if (col.function === 'avg') {
|
|
957
|
-
const total = pageData.reduce((sum, row) => sum + Number(row[field] || 0), 0);
|
|
958
|
-
summaryValues[field] = pageData.length ? total / pageData.length : 0;
|
|
959
|
-
}
|
|
960
|
-
if (col.function === 'min') {
|
|
961
|
-
const values = pageData.map((row) => Number(row[field] || 0));
|
|
962
|
-
summaryValues[field] = values.length ? Math.min(...values) : 0;
|
|
963
|
-
}
|
|
964
|
-
|
|
965
|
-
if (col.function === 'max') {
|
|
966
|
-
const values = pageData.map((row) => Number(row[field] || 0));
|
|
967
|
-
summaryValues[field] = values.length ? Math.max(...values) : 0;
|
|
968
|
-
}
|
|
969
|
-
});
|
|
970
|
-
|
|
971
|
-
return summaryValues;
|
|
972
|
-
}
|
|
973
|
-
return (
|
|
974
|
-
<>
|
|
975
|
-
<div className="table-header">
|
|
976
|
-
<div className="table-left">
|
|
977
|
-
{/* {selectedSearchFields?.length > 0 ? (
|
|
978
|
-
<div className="search-tags-container">
|
|
979
|
-
{selectedSearchFields.map((field) => (
|
|
980
|
-
<Tag key={field.field} closable color="blue" onClose={() => handleRemoveSearchField(field.field)}>
|
|
981
|
-
{field.caption}
|
|
982
|
-
</Tag>
|
|
983
|
-
))}
|
|
984
|
-
</div>
|
|
985
|
-
) : null} */}
|
|
986
|
-
</div>
|
|
987
|
-
|
|
988
|
-
<div className="table-right">
|
|
989
|
-
{/* shwoing caption is not correct so this commented */}
|
|
990
|
-
{/* <span className="menu-caption">{config.caption}</span> */}
|
|
991
|
-
<Search className="table-search-input" placeholder="Enter Search Value" allowClear onChange={onSearch} />
|
|
992
|
-
<div className="table-export-button">
|
|
993
|
-
{exportData.exportDatas && (
|
|
994
|
-
<>
|
|
995
|
-
<ExportReactCSV
|
|
996
|
-
title={config.caption}
|
|
997
|
-
headers={exportData.exportDatas.exportDataHeaders}
|
|
998
|
-
csvData={exportData.exportDatas.exportDataColumns}
|
|
999
|
-
fileName={`${config.caption || 'Report'}.xlsx`}
|
|
1000
|
-
pdfFileName={`${config.caption || 'Report'}.pdf`}
|
|
1001
|
-
scriptId={reportScriptId}
|
|
1002
|
-
inputParameters={reportInputParameters}
|
|
1003
|
-
dropdown
|
|
1004
|
-
/>
|
|
1005
|
-
</>
|
|
1006
|
-
)}
|
|
1007
|
-
</div>
|
|
1008
|
-
|
|
1009
|
-
{/* QR Scan start */}
|
|
1010
|
-
{showScanner ? (
|
|
1011
|
-
<Button size="small" type="primary" icon={<QrcodeOutlined />} onClick={() => setScannerVisible(true)}>
|
|
1012
|
-
Scan QR
|
|
1013
|
-
</Button>
|
|
1014
|
-
) : null}
|
|
1015
|
-
{/** Add User button */}
|
|
1016
|
-
{Array.isArray(buttonAttributes) &&
|
|
1017
|
-
buttonAttributes.map((btn, index) => (
|
|
1018
|
-
<Button key={index} size="small" type="primary" style={{ marginLeft: 8 }} onClick={() => handleOpenEdit(btn)}>
|
|
1019
|
-
{btn.title}
|
|
1020
|
-
</Button>
|
|
1021
|
-
))}
|
|
1022
|
-
|
|
1023
|
-
<Modal open={visible} onCancel={() => setVisible(false)} footer={null} destroyOnClose width={950} style={{ top: 10 }}>
|
|
1024
|
-
{ActiveComponent && (
|
|
1025
|
-
<ActiveComponent
|
|
1026
|
-
formContent={single}
|
|
1027
|
-
callback={() => {
|
|
1028
|
-
setVisible(false);
|
|
1029
|
-
refresh();
|
|
1030
|
-
setVisible(false);
|
|
1031
|
-
fetchReportData();
|
|
1032
|
-
}}
|
|
1033
|
-
// {...dynamicProps}
|
|
1034
|
-
/>
|
|
1035
|
-
)}
|
|
1036
|
-
</Modal>
|
|
1037
|
-
|
|
1038
|
-
<Modal open={isScannerVisible} title="Scan QR Code" footer={null} onCancel={() => setScannerVisible(false)} destroyOnClose>
|
|
1039
|
-
<QrScanner onScanSuccess={handleScanSuccess} onClose={() => setScannerVisible(false)} />
|
|
1040
|
-
</Modal>
|
|
1041
|
-
{/* QR Scan End */}
|
|
1042
|
-
</div>
|
|
1043
|
-
</div>
|
|
1044
|
-
|
|
1045
|
-
<div>
|
|
1046
|
-
<Card>
|
|
1047
|
-
{loading ? (
|
|
1048
|
-
<>
|
|
1049
|
-
<Skeleton active paragraph={{ rows: 6 }} />
|
|
1050
|
-
</>
|
|
1051
|
-
) : (
|
|
1052
|
-
<TableComponent
|
|
1053
|
-
size="small"
|
|
1054
|
-
// scroll={{ x: 'max-content' }}
|
|
1055
|
-
scroll={{ x: 'max-content', y: '60vh' }}
|
|
1056
|
-
rowKey={(record) => record.OpNo}
|
|
1057
|
-
dataSource={filtered ? filtered : patients} // In case if there is no filtered values we can use patient data
|
|
1058
|
-
columns={cols}
|
|
1059
|
-
sticky
|
|
1060
|
-
pagination={true}
|
|
1061
|
-
summary={(pageData) => {
|
|
1062
|
-
const summaryCols = columns.filter((col) => col.enable_summary);
|
|
1063
|
-
if (!summaryCols.length) return null;
|
|
1064
|
-
/** calculate summary*/
|
|
1065
|
-
|
|
1066
|
-
const summaryValues = calculateSummaryValues(summaryCols, pageData);
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
return (
|
|
1070
|
-
<Table.Summary.Row className="report-summary-row">
|
|
1071
|
-
{cols.map((col, index) => {
|
|
1072
|
-
if (summaryValues[col.field] !== undefined) {
|
|
1073
|
-
return (
|
|
1074
|
-
<Table.Summary.Cell key={index}>
|
|
1075
|
-
<strong style={{ fontWeight: 900 }}>{summaryValues[col.field]}</strong>
|
|
1076
|
-
</Table.Summary.Cell>
|
|
1077
|
-
);
|
|
1078
|
-
}
|
|
1079
|
-
|
|
1080
|
-
const captionConfig = columns.find((c) => col.field && c.caption_field === col.field);
|
|
1081
|
-
if (captionConfig) {
|
|
1082
|
-
return (
|
|
1083
|
-
<Table.Summary.Cell key={index}>
|
|
1084
|
-
<strong style={{ fontWeight: 900 }}>{captionConfig.summary_caption || ''}</strong>
|
|
1085
|
-
</Table.Summary.Cell>
|
|
1086
|
-
);
|
|
1087
|
-
}
|
|
1088
|
-
|
|
1089
|
-
return <Table.Summary.Cell key={index} />;
|
|
1090
|
-
})}
|
|
1091
|
-
</Table.Summary.Row>
|
|
1092
|
-
);
|
|
1093
|
-
}}
|
|
1094
|
-
/>
|
|
1095
|
-
)}
|
|
1096
|
-
|
|
1097
|
-
{/* Pagination aligned to the right */}
|
|
1098
|
-
<div style={{ display: 'flex', justifyContent: 'flex-end', marginTop: 8 }}>
|
|
1099
|
-
<Pagination
|
|
1100
|
-
showSizeChanger
|
|
1101
|
-
current={pagination.current}
|
|
1102
|
-
pageSize={pagination.pageSize}
|
|
1103
|
-
total={pagination.total}
|
|
1104
|
-
pageSizeOptions={[20, 30, 50, 100]}
|
|
1105
|
-
onChange={(page, pageSize) => handlePagination({ current: page, pageSize })}
|
|
1106
|
-
/>
|
|
1107
|
-
</div>
|
|
1108
|
-
|
|
1109
|
-
{/*If patient data exists show the number else to 0 */}
|
|
1110
|
-
<p className="size-hint">{patients ? patients.length : 0} records. </p>
|
|
1111
|
-
</Card>
|
|
1112
|
-
{/* </> */}
|
|
1113
|
-
{/* )} */}
|
|
1114
|
-
</div>
|
|
1115
|
-
</>
|
|
1116
|
-
);
|
|
1117
|
-
}
|