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 = ({ onClear, onSaveAndAddSignature, btnloading }) => {
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>Click<strong> Save </strong>to add signature and save the document</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.submode || replacements.mode);
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 (patients) {
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 = [...patients];
317
+ let dataToExport = [...filtered];
285
318
 
286
319
  if (summaryCols.length > 0) {
287
- const summaryValues = calculateSummaryValues(summaryCols, patients);
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
- }, [patients, columns]);
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
- // Change x to '100%' or true to make it responsive to the container width
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={false}
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>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ui-soxo-bootstrap-core",
3
- "version": "2.6.40-dev.21",
3
+ "version": "2.6.40-dev.22",
4
4
  "description": "All the Core Components for you to start",
5
5
  "keywords": [
6
6
  "all in one"