ui-soxo-bootstrap-core 2.6.40-dev.20 → 2.6.40-dev.22
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/core/lib/components/consent/signature-pad.js +141 -4
- package/core/lib/models/forms/components/form-creator/form-creator.js +4 -2
- package/core/modules/reporting/components/reporting-dashboard/reporting-dashboard.js +81 -546
- package/core/modules/reporting/components/reporting-dashboard/reporting-table.js +48 -30
- package/package.json +1 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/components/SignatureCanvas.js
|
|
2
|
-
import { useRef } from 'react';
|
|
2
|
+
import { useRef, useState } from 'react';
|
|
3
3
|
import SignatureCanvas from 'react-signature-canvas';
|
|
4
4
|
import { Button, message } from 'antd';
|
|
5
5
|
import { useTranslation } from '../../../lib';
|
|
@@ -8,12 +8,22 @@ import './signature-pad.scss';
|
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
|
|
11
|
-
const SignatureCanvasComponent = ({
|
|
11
|
+
const SignatureCanvasComponent = ({
|
|
12
|
+
onClear,
|
|
13
|
+
onSaveAndAddSignature,
|
|
14
|
+
btnloading,
|
|
15
|
+
showUpload = true,
|
|
16
|
+
accept = 'image/png',
|
|
17
|
+
maxSizeMB = 5,
|
|
18
|
+
}) => {
|
|
12
19
|
const sigCanvas = useRef({});
|
|
20
|
+
const fileInput = useRef(null);
|
|
21
|
+
const [uploadedName, setUploadedName] = useState(null);
|
|
13
22
|
const { t, i18n } = useTranslation(); // To Translate to another language
|
|
14
23
|
|
|
15
24
|
const clear = () => {
|
|
16
25
|
sigCanvas.current.clear()
|
|
26
|
+
setUploadedName(null);
|
|
17
27
|
if (onClear) onClear();
|
|
18
28
|
};
|
|
19
29
|
|
|
@@ -34,6 +44,102 @@ const SignatureCanvasComponent = ({ onClear, onSaveAndAddSignature, btnloading }
|
|
|
34
44
|
}
|
|
35
45
|
};
|
|
36
46
|
|
|
47
|
+
/**
|
|
48
|
+
* Open the (hidden) file picker
|
|
49
|
+
*/
|
|
50
|
+
const selectImage = () => {
|
|
51
|
+
if (fileInput.current) fileInput.current.click();
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Draw the picked image on the pad, scaled to fit and centered,
|
|
56
|
+
* so that Save / Clear keep working exactly as with a drawn signature
|
|
57
|
+
*
|
|
58
|
+
* @param {string} dataUrl
|
|
59
|
+
*/
|
|
60
|
+
const drawImageOnPad = (dataUrl) => {
|
|
61
|
+
const image = new Image();
|
|
62
|
+
|
|
63
|
+
image.onerror = () => message.error(t('Unable to load the selected image'));
|
|
64
|
+
|
|
65
|
+
image.onload = () => {
|
|
66
|
+
const canvas = sigCanvas.current.getCanvas();
|
|
67
|
+
|
|
68
|
+
if (!canvas || !image.naturalWidth || !image.naturalHeight) {
|
|
69
|
+
message.error(t('Unable to load the selected image'));
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Size the canvas is drawn with, and its actual (device pixel) resolution
|
|
74
|
+
const drawWidth = canvas.offsetWidth || canvas.width;
|
|
75
|
+
const drawHeight = canvas.offsetHeight || canvas.height;
|
|
76
|
+
const ratio = canvas.width / drawWidth;
|
|
77
|
+
|
|
78
|
+
// Fit inside the pad without distorting the image
|
|
79
|
+
const scale = Math.min(
|
|
80
|
+
canvas.width / image.naturalWidth,
|
|
81
|
+
canvas.height / image.naturalHeight
|
|
82
|
+
);
|
|
83
|
+
const imageWidth = image.naturalWidth * scale;
|
|
84
|
+
const imageHeight = image.naturalHeight * scale;
|
|
85
|
+
|
|
86
|
+
const offscreen = document.createElement('canvas');
|
|
87
|
+
offscreen.width = canvas.width;
|
|
88
|
+
offscreen.height = canvas.height;
|
|
89
|
+
offscreen.getContext('2d').drawImage(
|
|
90
|
+
image,
|
|
91
|
+
(canvas.width - imageWidth) / 2,
|
|
92
|
+
(canvas.height - imageHeight) / 2,
|
|
93
|
+
imageWidth,
|
|
94
|
+
imageHeight
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
sigCanvas.current.clear();
|
|
98
|
+
sigCanvas.current.fromDataURL(offscreen.toDataURL('image/png'), {
|
|
99
|
+
width: drawWidth,
|
|
100
|
+
height: drawHeight,
|
|
101
|
+
ratio,
|
|
102
|
+
});
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
image.src = dataUrl;
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Validate and read the picked image
|
|
110
|
+
*
|
|
111
|
+
* @param {Event} event
|
|
112
|
+
*/
|
|
113
|
+
const onFileSelect = (event) => {
|
|
114
|
+
const file = event.target.files && event.target.files[0];
|
|
115
|
+
|
|
116
|
+
// Reset so that picking the same file again still fires a change
|
|
117
|
+
event.target.value = '';
|
|
118
|
+
|
|
119
|
+
if (!file) return;
|
|
120
|
+
|
|
121
|
+
// Only the types listed in `accept` are allowed, whatever the file dialog let through
|
|
122
|
+
const allowed = accept.split(',').map((type) => type.trim()).filter(Boolean);
|
|
123
|
+
|
|
124
|
+
if (!file.type || allowed.indexOf(file.type) < 0) {
|
|
125
|
+
message.error(`${t('Please select a valid image file')} (${allowed.join(', ')})`);
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (file.size > maxSizeMB * 1024 * 1024) {
|
|
130
|
+
message.error(`${t('Image should be smaller than')} ${maxSizeMB}MB`);
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const reader = new FileReader();
|
|
135
|
+
reader.onerror = () => message.error(t('Unable to read the selected image'));
|
|
136
|
+
reader.onload = () => {
|
|
137
|
+
drawImageOnPad(reader.result);
|
|
138
|
+
setUploadedName(file.name);
|
|
139
|
+
};
|
|
140
|
+
reader.readAsDataURL(file);
|
|
141
|
+
};
|
|
142
|
+
|
|
37
143
|
return (
|
|
38
144
|
<div className='signature-pad'>
|
|
39
145
|
<SignatureCanvas
|
|
@@ -47,9 +153,19 @@ const SignatureCanvasComponent = ({ onClear, onSaveAndAddSignature, btnloading }
|
|
|
47
153
|
/>
|
|
48
154
|
|
|
49
155
|
<p>
|
|
50
|
-
<small>
|
|
156
|
+
<small>
|
|
157
|
+
{showUpload
|
|
158
|
+
? <>Draw the signature or <strong>Upload Image</strong>, then click<strong> Save </strong>to add signature and save the document</>
|
|
159
|
+
: <>Click<strong> Save </strong>to add signature and save the document</>}
|
|
160
|
+
</small>
|
|
51
161
|
</p>
|
|
52
162
|
|
|
163
|
+
{showUpload && uploadedName && (
|
|
164
|
+
<p className='signature-file-name'>
|
|
165
|
+
<small>{uploadedName}</small>
|
|
166
|
+
</p>
|
|
167
|
+
)}
|
|
168
|
+
|
|
53
169
|
<div className='signature-actions'>
|
|
54
170
|
<div className='left'>
|
|
55
171
|
<Button
|
|
@@ -61,6 +177,16 @@ const SignatureCanvasComponent = ({ onClear, onSaveAndAddSignature, btnloading }
|
|
|
61
177
|
>
|
|
62
178
|
{t('Save')}
|
|
63
179
|
</Button>
|
|
180
|
+
{showUpload && (
|
|
181
|
+
<Button
|
|
182
|
+
style={{ borderRadius: '2px' }}
|
|
183
|
+
type="secondary"
|
|
184
|
+
size="medium"
|
|
185
|
+
onClick={selectImage}
|
|
186
|
+
>
|
|
187
|
+
{t('Upload Image')}
|
|
188
|
+
</Button>
|
|
189
|
+
)}
|
|
64
190
|
<Button
|
|
65
191
|
style={{ borderRadius: '2px' }}
|
|
66
192
|
type="secondary"
|
|
@@ -82,9 +208,20 @@ const SignatureCanvasComponent = ({ onClear, onSaveAndAddSignature, btnloading }
|
|
|
82
208
|
{t('Add Signature')}
|
|
83
209
|
</Button> */}
|
|
84
210
|
</div>
|
|
211
|
+
|
|
212
|
+
{showUpload && (
|
|
213
|
+
<input
|
|
214
|
+
ref={fileInput}
|
|
215
|
+
type="file"
|
|
216
|
+
accept={accept}
|
|
217
|
+
className='signature-file-input'
|
|
218
|
+
style={{ display: 'none' }}
|
|
219
|
+
onChange={onFileSelect}
|
|
220
|
+
/>
|
|
221
|
+
)}
|
|
85
222
|
</div>
|
|
86
223
|
|
|
87
224
|
);
|
|
88
225
|
};
|
|
89
226
|
|
|
90
|
-
export default SignatureCanvasComponent;
|
|
227
|
+
export default SignatureCanvasComponent;
|
|
@@ -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
|
-
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import React, { useState, useEffect } from 'react';
|
|
1
|
+
import React, { useState, useEffect, useMemo, useContext } from 'react';
|
|
2
2
|
import { Table, Skeleton, Input, Modal, message, Pagination } from 'antd';
|
|
3
3
|
import { QrcodeOutlined } from '@ant-design/icons';
|
|
4
|
-
import { ExportReactCSV, getExportData, Card, TableComponent, QrScanner } from './../../../../lib/';
|
|
4
|
+
import { ExportReactCSV, getExportData, Card, TableComponent, QrScanner, GlobalContext, safeJSON } from './../../../../lib/';
|
|
5
5
|
import moment from 'moment-timezone';
|
|
6
6
|
import { CoreScripts } from './../../../../models/';
|
|
7
7
|
import Button from '../../../../lib/elements/basic/button/button';
|
|
@@ -93,9 +93,22 @@ export default function ReportingTable({
|
|
|
93
93
|
const [internalConfig, setInternalConfig] = useState({});
|
|
94
94
|
const [internalPagination, setInternalPagination] = useState({ current: 1, pageSize: 20, total: 0 });
|
|
95
95
|
|
|
96
|
+
const { role = {} } = useContext(GlobalContext);
|
|
97
|
+
// When any of the logged in user's roles has `enableUserFilter: true` in its other details,
|
|
98
|
+
// reports receive `enableUserFilter: true` so they can restrict rows to the user's own records.
|
|
99
|
+
// `role` may be a single role object or an array of roles.
|
|
100
|
+
const roles = Array.isArray(role) ? role : role ? [role] : [];
|
|
101
|
+
const isUserFilterEnabled = roles.some((r) => {
|
|
102
|
+
const details = r?.other_details;
|
|
103
|
+
// Some records store other_details with single quotes, e.g. "{'enableUserFilter':true}"
|
|
104
|
+
const parsed = safeJSON(details) || (typeof details === 'string' ? safeJSON(details.replace(/'/g, '"')) : null) || {};
|
|
105
|
+
return parsed.enableUserFilter === true;
|
|
106
|
+
});
|
|
107
|
+
const userFilter = isUserFilterEnabled ? { enableUserFilter: true } : {};
|
|
108
|
+
|
|
96
109
|
// Independent mode is enabled when enough identifiers are present for the
|
|
97
110
|
// table to load its own schema and data instead of relying on parent props.
|
|
98
|
-
const shouldFetchData = !!(requestId || reportId || replacements
|
|
111
|
+
const shouldFetchData = !!(requestId || reportId || replacements?.submode || replacements?.mode);
|
|
99
112
|
const requestPayloadKey = JSON.stringify(requestPayload || {});
|
|
100
113
|
|
|
101
114
|
const propValues = (attributes && JSON.parse(attributes)) || {};
|
|
@@ -189,6 +202,7 @@ export default function ReportingTable({
|
|
|
189
202
|
page: currentPager.current,
|
|
190
203
|
limit: currentPager.pageSize,
|
|
191
204
|
...(requestPayload?.body ? {} : { mode, submode }),
|
|
205
|
+
...userFilter,
|
|
192
206
|
},
|
|
193
207
|
};
|
|
194
208
|
// if (!isNuradesk) {
|
|
@@ -199,6 +213,7 @@ export default function ReportingTable({
|
|
|
199
213
|
...replacements,
|
|
200
214
|
page: currentPager.current,
|
|
201
215
|
limit: currentPager.pageSize,
|
|
216
|
+
...userFilter,
|
|
202
217
|
},
|
|
203
218
|
};
|
|
204
219
|
}
|
|
@@ -270,10 +285,28 @@ export default function ReportingTable({
|
|
|
270
285
|
}
|
|
271
286
|
};
|
|
272
287
|
|
|
288
|
+
const filtered = useMemo(() => {
|
|
289
|
+
if (!patients || !query) return patients;
|
|
290
|
+
|
|
291
|
+
return patients.filter((record) => {
|
|
292
|
+
let keys = Object.keys(record);
|
|
293
|
+
let flag = false;
|
|
294
|
+
keys.forEach((key) => {
|
|
295
|
+
let ele = record[key];
|
|
296
|
+
if (ele && typeof ele === 'string' && ele.toLowerCase().indexOf(query.toLowerCase()) !== -1) {
|
|
297
|
+
flag = true;
|
|
298
|
+
}
|
|
299
|
+
});
|
|
300
|
+
return flag;
|
|
301
|
+
});
|
|
302
|
+
}, [patients, query]);
|
|
303
|
+
|
|
273
304
|
useEffect(() => {
|
|
274
|
-
if (
|
|
305
|
+
if (filtered) {
|
|
275
306
|
// Export uses the visible display column structure and appends the same
|
|
276
307
|
// summary row that appears in the table when summary columns are enabled.
|
|
308
|
+
// Uses `filtered` (not the raw `patients`) so the exported file matches
|
|
309
|
+
// whatever the search box currently shows on screen.
|
|
277
310
|
const exportCols = cols.map((col) => {
|
|
278
311
|
if (col.title && typeof col.title === 'object' && col.title.props) {
|
|
279
312
|
return { ...col, title: col.title.props.title };
|
|
@@ -281,10 +314,10 @@ export default function ReportingTable({
|
|
|
281
314
|
return col;
|
|
282
315
|
});
|
|
283
316
|
const summaryCols = columns.filter((col) => col.enable_summary);
|
|
284
|
-
let dataToExport = [...
|
|
317
|
+
let dataToExport = [...filtered];
|
|
285
318
|
|
|
286
319
|
if (summaryCols.length > 0) {
|
|
287
|
-
const summaryValues = calculateSummaryValues(summaryCols,
|
|
320
|
+
const summaryValues = calculateSummaryValues(summaryCols, filtered);
|
|
288
321
|
const summaryRow = { isSummaryRow: true };
|
|
289
322
|
|
|
290
323
|
cols.forEach((col) => {
|
|
@@ -310,22 +343,7 @@ export default function ReportingTable({
|
|
|
310
343
|
setExportData({ exportDatas });
|
|
311
344
|
}
|
|
312
345
|
}
|
|
313
|
-
}, [
|
|
314
|
-
|
|
315
|
-
let filtered = patients;
|
|
316
|
-
if (patients && query) {
|
|
317
|
-
filtered = patients.filter((record) => {
|
|
318
|
-
let keys = Object.keys(record);
|
|
319
|
-
let flag = false;
|
|
320
|
-
keys.forEach((key) => {
|
|
321
|
-
let ele = record[key];
|
|
322
|
-
if (ele && typeof ele === 'string' && ele.toLowerCase().indexOf(query.toLowerCase()) !== -1) {
|
|
323
|
-
flag = true;
|
|
324
|
-
}
|
|
325
|
-
});
|
|
326
|
-
return flag;
|
|
327
|
-
});
|
|
328
|
-
}
|
|
346
|
+
}, [filtered, columns]);
|
|
329
347
|
|
|
330
348
|
/**
|
|
331
349
|
* Handles successful QR scans by looking up a matching record and navigating
|
|
@@ -449,9 +467,11 @@ export default function ReportingTable({
|
|
|
449
467
|
{exportData.exportDatas && !isNuradesk && (
|
|
450
468
|
<ExportReactCSV
|
|
451
469
|
title={config.caption}
|
|
452
|
-
fileName={`${(config.caption || 'Report').trim().replace(/\s+/g, '_')}_${moment().format('YYYY-MM-DD-HH-mm-ss-SSS')}.xlsx`}
|
|
453
470
|
headers={exportData.exportDatas.exportDataHeaders}
|
|
454
471
|
csvData={exportData.exportDatas.exportDataColumns}
|
|
472
|
+
fileName={`${config.caption || 'Report'}.xlsx`}
|
|
473
|
+
pdfFileName={`${config.caption || 'Report'}.pdf`}
|
|
474
|
+
dropdown
|
|
455
475
|
/>
|
|
456
476
|
)}
|
|
457
477
|
</div>
|
|
@@ -464,14 +484,12 @@ export default function ReportingTable({
|
|
|
464
484
|
) : (
|
|
465
485
|
<TableComponent
|
|
466
486
|
size="small"
|
|
467
|
-
|
|
468
|
-
// Your vertical logic (adjust '10' based on how many rows fit on your screen)
|
|
469
|
-
scroll={{ x: 'max-content', y: (filtered?.length || 0) > 11 ? 400 : undefined }}
|
|
487
|
+
scroll={{ x: 'max-content', y: '60vh' }}
|
|
470
488
|
rowKey={(record) => record.OpNo}
|
|
471
489
|
dataSource={filtered}
|
|
472
490
|
columns={cols}
|
|
473
491
|
sticky
|
|
474
|
-
pagination={
|
|
492
|
+
pagination={true}
|
|
475
493
|
summary={(pageData) => {
|
|
476
494
|
const summaryCols = columns.filter((col) => col.enable_summary);
|
|
477
495
|
if (!summaryCols.length) return null;
|
|
@@ -502,14 +520,14 @@ export default function ReportingTable({
|
|
|
502
520
|
/>
|
|
503
521
|
)}
|
|
504
522
|
<div style={{ display: 'flex', justifyContent: 'flex-end', marginTop: 8 }}>
|
|
505
|
-
<Pagination
|
|
523
|
+
{/* <Pagination
|
|
506
524
|
showSizeChanger
|
|
507
525
|
current={pagination?.current}
|
|
508
526
|
pageSize={pagination?.pageSize}
|
|
509
527
|
total={pagination?.total}
|
|
510
528
|
pageSizeOptions={[20, 30, 50, 100]}
|
|
511
|
-
onChange={(page, pageSize) => handlePaginationInternal({ current: page, pageSize })}
|
|
512
|
-
/>
|
|
529
|
+
onChange={(page, pageSize) => handlePaginationInternal({ current: page, pageSize })} */}
|
|
530
|
+
{/* /> */}
|
|
513
531
|
</div>
|
|
514
532
|
<p className="size-hint">{patients ? patients.length : 0} records.</p>
|
|
515
533
|
</Card>
|