ui-soxo-bootstrap-core 2.6.54 → 2.6.55-dev.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.
- package/core/lib/components/consent/signature-pad.js +140 -3
- package/core/lib/components/consent/signature-pad.scss +9 -0
- package/core/lib/models/forms/components/form-creator/form-creator.js +4 -2
- package/core/modules/reporting/components/reporting-dashboard/reporting-dashboard.js +27 -1
- 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,6 +208,17 @@ 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
|
);
|
|
@@ -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) => (
|
|
@@ -15,6 +15,9 @@ import MenuDashBoardComponent from '../../../../lib/elements/basic/menu-dashboar
|
|
|
15
15
|
|
|
16
16
|
import ReportingTable from './reporting-table';
|
|
17
17
|
|
|
18
|
+
// Input parameter that decides which date column the report is filtered by
|
|
19
|
+
const DATE_FILTER_FIELD = 'date_filter_type';
|
|
20
|
+
|
|
18
21
|
/**
|
|
19
22
|
* ReportingDashboard component renders the dashboard and handles patient details,
|
|
20
23
|
* configuration, and form layout for generating reports.
|
|
@@ -174,6 +177,9 @@ export default function ReportingDashboard({
|
|
|
174
177
|
if (urlParams[record.field]) {
|
|
175
178
|
if (record.type === 'date') {
|
|
176
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];
|
|
177
183
|
}
|
|
178
184
|
|
|
179
185
|
// return formContent;
|
|
@@ -201,6 +207,10 @@ export default function ReportingDashboard({
|
|
|
201
207
|
break;
|
|
202
208
|
|
|
203
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
|
+
}
|
|
204
214
|
break;
|
|
205
215
|
}
|
|
206
216
|
}
|
|
@@ -209,6 +219,11 @@ export default function ReportingDashboard({
|
|
|
209
219
|
if (record.type === 'date' && !formContent[record.field]) {
|
|
210
220
|
formContent[record.field] = moment().tz(process.env.REACT_APP_TIMEZONE);
|
|
211
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
|
+
}
|
|
212
227
|
if (record.type === 'search') {
|
|
213
228
|
if (!formContent[record.field]) formContent[record.field] = [];
|
|
214
229
|
return {
|
|
@@ -248,8 +263,19 @@ export default function ReportingDashboard({
|
|
|
248
263
|
// If enabled, clear the details array
|
|
249
264
|
setDetails([]);
|
|
250
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
|
+
|
|
251
273
|
// Keep all parameters with a type (including search) to render in FormCreator
|
|
252
|
-
setDetails([
|
|
274
|
+
setDetails([
|
|
275
|
+
...parameters
|
|
276
|
+
.filter((ele) => ele.type)
|
|
277
|
+
.map((ele) => (ele.field === DATE_FILTER_FIELD ? { ...ele, visible: isFilterEnabled } : ele)),
|
|
278
|
+
]);
|
|
253
279
|
}
|
|
254
280
|
}
|
|
255
281
|
|