ui-soxo-bootstrap-core 2.6.40-dev.21 → 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.
|
@@ -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;
|
|
@@ -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>
|