ui-soxo-bootstrap-core 2.6.18 → 2.6.20
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/elements/basic/country-phone-input/country-phone-input.js +34 -60
- package/core/lib/elements/basic/country-phone-input/phone-input.scss +14 -0
- package/core/lib/modules/generic/generic-list/ExportReactCSV.js +28 -2
- package/core/lib/pages/login/login.js +26 -39
- package/core/lib/utils/api/api.utils.js +23 -27
- package/core/lib/utils/common/common.utils.js +0 -35
- package/core/lib/utils/generic/generic.utils.js +2 -1
- package/core/lib/utils/http/http.utils.js +32 -4
- package/core/modules/reporting/components/reporting-dashboard/display-columns/build-display-columns.js +1 -1
- package/core/modules/reporting/components/reporting-dashboard/reporting-dashboard.js +112 -41
- package/core/modules/reporting/components/reporting-dashboard/reporting-dashboard.scss +7 -0
- package/package.json +1 -1
|
@@ -6,79 +6,53 @@
|
|
|
6
6
|
* Ensure to follow a minimal standard
|
|
7
7
|
*/
|
|
8
8
|
import React, { useState, useEffect, useRef } from 'react';
|
|
9
|
-
|
|
10
9
|
import PhoneInput from 'react-phone-input-2';
|
|
11
|
-
|
|
12
10
|
import 'react-phone-input-2/lib/style.css';
|
|
13
|
-
|
|
14
|
-
import PropTypes from "prop-types";
|
|
15
|
-
|
|
11
|
+
import PropTypes from 'prop-types';
|
|
16
12
|
import './phone-input.scss';
|
|
17
13
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
14
|
+
export default function CountryPhoneInput({
|
|
15
|
+
value,
|
|
16
|
+
onChange,
|
|
17
|
+
disabled,
|
|
18
|
+
disableCountryCode,
|
|
19
|
+
required,
|
|
20
|
+
defaultCountryCode,
|
|
21
|
+
onBlur,
|
|
22
|
+
className,
|
|
23
|
+
onKeyDown,
|
|
24
|
+
}) {
|
|
25
|
+
const inputRef = useRef();
|
|
26
|
+
const hasInitializedRef = useRef(false); // IMPORTANT
|
|
27
|
+
|
|
28
|
+
const [phone, setPhone] = useState('');
|
|
29
|
+
/**
|
|
30
|
+
* run ONLY ONCE when initial value has dial code
|
|
31
|
+
*/
|
|
25
32
|
useEffect(() => {
|
|
26
|
-
if (
|
|
27
|
-
// To prepopulate phone number ,concat dialCode and phone number
|
|
33
|
+
if (!hasInitializedRef.current && value?.code?.dialCode && value?.value) {
|
|
28
34
|
setPhone(value.code.dialCode + value.value);
|
|
29
|
-
|
|
30
|
-
// In case of update , to identify the country code that is getting selected
|
|
31
|
-
// We are setting a timeout
|
|
32
|
-
setTimeout(() => {
|
|
33
|
-
// Using the reference , we get the internal state of the library component
|
|
34
|
-
let selectedCountry = null;
|
|
35
|
-
|
|
36
|
-
let selectedCode = null;
|
|
37
|
-
|
|
38
|
-
if (
|
|
39
|
-
inputRef.current &&
|
|
40
|
-
inputRef.current.state &&
|
|
41
|
-
inputRef.current.state.selectedCountry
|
|
42
|
-
) {
|
|
43
|
-
selectedCountry = inputRef.current.state.selectedCountry;
|
|
44
|
-
|
|
45
|
-
// Once we get the selected country ,we format it to the form that is received for normal onChange
|
|
46
|
-
selectedCode = {
|
|
47
|
-
countryCode: selectedCountry.iso2,
|
|
48
|
-
dialCode: selectedCountry.dialCode,
|
|
49
|
-
name: selectedCountry.name,
|
|
50
|
-
};
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// Below being the format expected , we trigger the onChange to manually update the form
|
|
54
|
-
let updatedValue = {
|
|
55
|
-
value: value.code.dialCode + value.value,
|
|
56
|
-
code: selectedCode,
|
|
57
|
-
};
|
|
58
|
-
|
|
59
|
-
onChange(updatedValue);
|
|
60
|
-
}, 0);
|
|
35
|
+
hasInitializedRef.current = true; // 👈 mark as done
|
|
61
36
|
}
|
|
62
|
-
}, []);
|
|
37
|
+
}, [value]);
|
|
63
38
|
|
|
64
39
|
/**
|
|
65
|
-
*
|
|
66
|
-
* @param {*} value
|
|
40
|
+
* handle user input
|
|
67
41
|
*/
|
|
68
|
-
function handleInput(
|
|
69
|
-
if (code
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
42
|
+
function handleInput(inputValue, code) {
|
|
43
|
+
if (!code?.dialCode) return;
|
|
44
|
+
|
|
45
|
+
const phoneNumber = inputValue.slice(code.dialCode.length);
|
|
46
|
+
|
|
47
|
+
// remove autofill class when user edits field (if you use it)
|
|
48
|
+
onChange({
|
|
49
|
+
value: phoneNumber,
|
|
75
50
|
code,
|
|
76
|
-
};
|
|
77
|
-
onChange(value);
|
|
51
|
+
});
|
|
78
52
|
}
|
|
79
53
|
|
|
80
54
|
return (
|
|
81
|
-
<div className=
|
|
55
|
+
<div className={`phone-input ${className || ''}`}>
|
|
82
56
|
<PhoneInput
|
|
83
57
|
ref={inputRef}
|
|
84
58
|
country={defaultCountryCode || 'in'}
|
|
@@ -104,4 +78,4 @@ CountryPhoneInput.propTypes = {
|
|
|
104
78
|
disableCountryCode: PropTypes.bool,
|
|
105
79
|
/** A boolean indicating whether the input is required or not. This prop is optional. */
|
|
106
80
|
required: PropTypes.bool,
|
|
107
|
-
};
|
|
81
|
+
};
|
|
@@ -12,6 +12,20 @@
|
|
|
12
12
|
// }
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
+
.autofilled-field {
|
|
16
|
+
.react-tel-input {
|
|
17
|
+
.form-control {
|
|
18
|
+
border-color: #fa8c16 !important; // AntD warning orange
|
|
19
|
+
background-color: #fff7e6;
|
|
20
|
+
box-shadow: none;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.flag-dropdown {
|
|
24
|
+
border-color: #fa8c16 !important;
|
|
25
|
+
background-color: #fff7e6;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
15
29
|
// /* For tablets and smaller screens (max-width: 1024px) */
|
|
16
30
|
// @media (max-width: 1024px) {
|
|
17
31
|
// .phone-input {
|
|
@@ -10,6 +10,8 @@
|
|
|
10
10
|
import React from 'react';
|
|
11
11
|
// Import XLSX library to handle Excel file creation
|
|
12
12
|
import * as XLSX from 'xlsx';
|
|
13
|
+
// import * as XLSX from 'xlsx-js-style';
|
|
14
|
+
// import * as XLSX from 'xlsx-js-style';
|
|
13
15
|
// Import saveAs from file-saver to trigger file download in browser
|
|
14
16
|
import { saveAs } from 'file-saver';
|
|
15
17
|
import { Button } from 'antd';
|
|
@@ -30,7 +32,27 @@ export const ExportReactCSV = ({ csvData, headers, fileName,title }) => {
|
|
|
30
32
|
// Extract label names for the header row
|
|
31
33
|
const headerLabels = headers.map((h) => h.label);
|
|
32
34
|
// Map data rows according to the headers' key order
|
|
33
|
-
const rows = csvData.map((row) =>
|
|
35
|
+
const rows = csvData.map((row) =>
|
|
36
|
+
headers.map((h, hIndex) => {
|
|
37
|
+
let value = row[h.key];
|
|
38
|
+
// If it's a summary row, return a cell object with bold styling
|
|
39
|
+
if (row.isSummaryRow) {
|
|
40
|
+
// Remove the index value (typically the first column) for the summary row
|
|
41
|
+
if (hIndex === 0) value = "";
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
v: value,
|
|
45
|
+
s: {
|
|
46
|
+
font: { bold: true },
|
|
47
|
+
// "Thick" visual indicator via border
|
|
48
|
+
// Visual indicator via border
|
|
49
|
+
border: { bottom: { style: "thick", color: { rgb: "000000" } } }
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
return value;
|
|
54
|
+
})
|
|
55
|
+
);
|
|
34
56
|
// Create a worksheet from an array of arrays (header + rows)
|
|
35
57
|
const data = [[title || "Report"], [], headerLabels, ...rows];
|
|
36
58
|
// worksheet = XLSX.utils.aoa_to_sheet([headerLabels, ...rows]);
|
|
@@ -44,7 +66,11 @@ export const ExportReactCSV = ({ csvData, headers, fileName,title }) => {
|
|
|
44
66
|
// Append the worksheet to the workbook with the name 'Sheet1'
|
|
45
67
|
XLSX.utils.book_append_sheet(workbook, worksheet, 'Sheet1');
|
|
46
68
|
// Write workbook to a buffer as an array
|
|
47
|
-
const excelBuffer = XLSX.write(workbook, {
|
|
69
|
+
const excelBuffer = XLSX.write(workbook, {
|
|
70
|
+
bookType: 'xlsx',
|
|
71
|
+
type: 'array',
|
|
72
|
+
cellStyles: true, // Required to export styles
|
|
73
|
+
});
|
|
48
74
|
// Create a Blob object from the buffer with correct MIME type
|
|
49
75
|
const blob = new Blob([excelBuffer], {
|
|
50
76
|
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
@@ -30,7 +30,7 @@ import { getAccessToken, getRefreshToken } from '../../utils/http/auth.helper';
|
|
|
30
30
|
|
|
31
31
|
import { Location } from '../../utils';
|
|
32
32
|
|
|
33
|
-
import { checkLicenseStatus, formatMobile,
|
|
33
|
+
import { checkLicenseStatus, formatMobile, safeJSON } from '../../utils/common/common.utils';
|
|
34
34
|
|
|
35
35
|
import { MailOutlined, MessageOutlined, WhatsAppOutlined } from '@ant-design/icons';
|
|
36
36
|
|
|
@@ -50,8 +50,6 @@ const tailLayout = {
|
|
|
50
50
|
|
|
51
51
|
const LICENSE_EXPIRY = '2026-12-12';
|
|
52
52
|
|
|
53
|
-
//password valdity expire
|
|
54
|
-
const PASSWORD_VALIDITY_DAYS = 90;
|
|
55
53
|
|
|
56
54
|
const headers = {
|
|
57
55
|
db_ptr: 'nuraho',
|
|
@@ -115,47 +113,32 @@ function LoginPhone({ history, appSettings }) {
|
|
|
115
113
|
*
|
|
116
114
|
* - Parses `last_password_change` from user.other_details.
|
|
117
115
|
* - Calculates expiry using PASSWORD_VALIDITY_DAYS.
|
|
118
|
-
* - Uses `checkExpiryStatus()` to determine status.
|
|
119
116
|
* - Shows Ant Design warning message if expired or within warning period.
|
|
120
117
|
* - Warning message includes navigation to `/change-password`.
|
|
121
118
|
*
|
|
122
119
|
* Requires:
|
|
123
120
|
* - PASSWORD_VALIDITY_DAYS constant
|
|
124
|
-
* - checkExpiryStatus utility
|
|
125
121
|
* - React Router history
|
|
126
122
|
* - antd message component
|
|
127
123
|
*/
|
|
128
|
-
const handlePasswordExpiryCheck = (
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
const lastPasswordChange = otherDetails?.last_password_change;
|
|
132
|
-
|
|
133
|
-
if (lastPasswordChange) {
|
|
134
|
-
const onlyDate = new Date(lastPasswordChange).toISOString().split('T')[0];
|
|
135
|
-
const passwordExpiryDate = new Date(onlyDate);
|
|
136
|
-
passwordExpiryDate.setDate(passwordExpiryDate.getDate() + PASSWORD_VALIDITY_DAYS);
|
|
137
|
-
|
|
138
|
-
const passwordStatus = checkExpiryStatus({
|
|
139
|
-
expiryDate: passwordExpiryDate,
|
|
140
|
-
warningDays: 2,
|
|
141
|
-
expiredMessage: 'Your password has expired. Please reset it.',
|
|
142
|
-
warningMessage: (d) => (
|
|
143
|
-
<span>
|
|
144
|
-
Your password will expire in {d} day(s).{' '}
|
|
145
|
-
<a
|
|
146
|
-
onClick={() => {
|
|
147
|
-
history.push('/change-password');
|
|
148
|
-
}}
|
|
149
|
-
>
|
|
150
|
-
Click here to update.
|
|
151
|
-
</a>
|
|
152
|
-
</span>
|
|
153
|
-
),
|
|
154
|
-
});
|
|
124
|
+
const handlePasswordExpiryCheck = (expiryDays) => {
|
|
125
|
+
if (expiryDays == null) return;
|
|
155
126
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
127
|
+
if (expiryDays <= 0) {
|
|
128
|
+
message.error('Your password has expired. Please reset it.');
|
|
129
|
+
|
|
130
|
+
setExpiredPassword(true);
|
|
131
|
+
setShowResetpassword(true);
|
|
132
|
+
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (expiryDays <= 2) {
|
|
137
|
+
message.warning(
|
|
138
|
+
<span>
|
|
139
|
+
Your password will expire in {expiryDays} day(s). <a onClick={() => history.push('/change-password')}>Click here to update.</a>
|
|
140
|
+
</span>
|
|
141
|
+
);
|
|
159
142
|
}
|
|
160
143
|
};
|
|
161
144
|
|
|
@@ -196,7 +179,7 @@ function LoginPhone({ history, appSettings }) {
|
|
|
196
179
|
if (insider_token) localStorage.insider_token = insider_token;
|
|
197
180
|
|
|
198
181
|
if (result.success) {
|
|
199
|
-
handlePasswordExpiryCheck(
|
|
182
|
+
handlePasswordExpiryCheck(result.expiry_in_days);
|
|
200
183
|
|
|
201
184
|
//two_factor_authentication variable is present then proceed Two factor authentication
|
|
202
185
|
if (result.data && result.data.two_factor_authentication) {
|
|
@@ -323,6 +306,8 @@ function LoginPhone({ history, appSettings }) {
|
|
|
323
306
|
*
|
|
324
307
|
*/
|
|
325
308
|
const sendAuthenticationOtp = () => {
|
|
309
|
+
if (loading) return;
|
|
310
|
+
|
|
326
311
|
// Reset previous error
|
|
327
312
|
setModeError(false);
|
|
328
313
|
|
|
@@ -421,7 +406,7 @@ function LoginPhone({ history, appSettings }) {
|
|
|
421
406
|
// set user info into local storage
|
|
422
407
|
localStorage.setItem('userInfo', JSON.stringify(userInfo));
|
|
423
408
|
|
|
424
|
-
handlePasswordExpiryCheck(result.
|
|
409
|
+
handlePasswordExpiryCheck(result.expiry_in_days);
|
|
425
410
|
|
|
426
411
|
setTimeout(() => history.push('/'), 500);
|
|
427
412
|
} else {
|
|
@@ -462,6 +447,8 @@ function LoginPhone({ history, appSettings }) {
|
|
|
462
447
|
* Otp resend Logic
|
|
463
448
|
*/
|
|
464
449
|
const handleResendOTP = () => {
|
|
450
|
+
if (loading) return;
|
|
451
|
+
|
|
465
452
|
setOtpValue('');
|
|
466
453
|
setModeError(false);
|
|
467
454
|
setOtpError(false);
|
|
@@ -731,7 +718,7 @@ function LoginPhone({ history, appSettings }) {
|
|
|
731
718
|
|
|
732
719
|
{!otpVerification ? (
|
|
733
720
|
<div className="otp-container">
|
|
734
|
-
<Button type="primary" onClick={sendAuthenticationOtp} style={{ marginTop: '6px' }}>
|
|
721
|
+
<Button loading={loading} type="primary" onClick={sendAuthenticationOtp} style={{ marginTop: '6px' }}>
|
|
735
722
|
Send OTP
|
|
736
723
|
</Button>
|
|
737
724
|
</div>
|
|
@@ -777,7 +764,7 @@ function LoginPhone({ history, appSettings }) {
|
|
|
777
764
|
</div>
|
|
778
765
|
</div>
|
|
779
766
|
{!otpSuccess && (
|
|
780
|
-
<Button type="primary" disabled={otpExpired} onClick={verifyOtp} style={{ marginTop: 16 }}>
|
|
767
|
+
<Button loading={loading} type="primary" disabled={otpExpired} onClick={verifyOtp} style={{ marginTop: 16 }}>
|
|
781
768
|
Verify OTP
|
|
782
769
|
</Button>
|
|
783
770
|
)}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { PostData, GetData, PutData, PatchData, DeleteData } from './../http/http.utils';
|
|
1
|
+
import { PostData, GetData, PutData, PatchData, DeleteData,UploadData } from './../http/http.utils';
|
|
2
2
|
|
|
3
3
|
let headers = {};
|
|
4
4
|
|
|
@@ -124,33 +124,29 @@ export default class ApiUtils {
|
|
|
124
124
|
});
|
|
125
125
|
};
|
|
126
126
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
return progress;
|
|
150
|
-
}
|
|
151
|
-
);
|
|
127
|
+
// upload document
|
|
128
|
+
static upload = async ({ url, data, headers: customHeaders = {}, ...props }) => {
|
|
129
|
+
const dbPtr = localStorage.getItem('db_ptr');
|
|
130
|
+
|
|
131
|
+
// Ensure settings object exists to avoid undefined errors in ApiCall
|
|
132
|
+
const settings = {
|
|
133
|
+
headers: {},
|
|
134
|
+
getToken: () => localStorage.getItem('access_token')
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
return UploadData({
|
|
138
|
+
url,
|
|
139
|
+
formBody: data,
|
|
140
|
+
// Your FormData goes here
|
|
141
|
+
isUpload: true, // Indicate that this is an upload request
|
|
142
|
+
settings,
|
|
143
|
+
headers: {
|
|
144
|
+
...(dbPtr ? { db_ptr: dbPtr } : {}),
|
|
145
|
+
...customHeaders,
|
|
146
|
+
},
|
|
147
|
+
...props,
|
|
148
|
+
});
|
|
152
149
|
};
|
|
153
|
-
|
|
154
150
|
/**
|
|
155
151
|
* Get Auth Status
|
|
156
152
|
*
|
|
@@ -123,41 +123,6 @@ export const checkLicenseStatus = (expiryDate) => {
|
|
|
123
123
|
return { valid: true, daysLeft, message: null, level: null };
|
|
124
124
|
};
|
|
125
125
|
|
|
126
|
-
/**
|
|
127
|
-
* Checks password expiry status.
|
|
128
|
-
*
|
|
129
|
-
* @param {string|Date} expiryDate
|
|
130
|
-
* @param {number} warningDays
|
|
131
|
-
* @param {string} expiredMessage
|
|
132
|
-
* @param {(daysLeft: number) => string} warningMessage
|
|
133
|
-
*
|
|
134
|
-
* @returns {{ valid: boolean, daysLeft: number, message: string|null, level: "error"|"warning"|null }}
|
|
135
|
-
*/
|
|
136
|
-
|
|
137
|
-
export const checkExpiryStatus = ({ expiryDate, warningDays, expiredMessage, warningMessage }) => {
|
|
138
|
-
const expiry = new Date(expiryDate);
|
|
139
|
-
|
|
140
|
-
if (isNaN(expiry)) {
|
|
141
|
-
return { valid: false, daysLeft: 0, message: 'Invalid date', level: 'error' };
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
expiry.setHours(0, 0, 0, 0);
|
|
145
|
-
const today = new Date();
|
|
146
|
-
today.setHours(0, 0, 0, 0);
|
|
147
|
-
|
|
148
|
-
const msDiff = expiry.getTime() - today.getTime();
|
|
149
|
-
const daysLeft = Math.ceil(msDiff / (1000 * 60 * 60 * 24));
|
|
150
|
-
|
|
151
|
-
if (daysLeft < 0) {
|
|
152
|
-
return { valid: false, daysLeft, message: expiredMessage, level: 'error' };
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
if (daysLeft <= warningDays) {
|
|
156
|
-
return { valid: true, daysLeft, message: warningMessage(daysLeft), level: 'warning' };
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
return { valid: true, daysLeft, message: null, level: null };
|
|
160
|
-
};
|
|
161
126
|
|
|
162
127
|
/**
|
|
163
128
|
* Masks a mobile number by hiding all but the last `visibleDigits`.
|
|
@@ -31,7 +31,8 @@ export function getExportData(records, exportDataColumns) {
|
|
|
31
31
|
const response = records.map((row, columnIndex) => {
|
|
32
32
|
|
|
33
33
|
let entry = {
|
|
34
|
-
'Sl No': columnIndex + 1
|
|
34
|
+
'Sl No': row.isSummaryRow ? '' : columnIndex + 1,
|
|
35
|
+
isSummaryRow: row.isSummaryRow
|
|
35
36
|
};
|
|
36
37
|
|
|
37
38
|
filteredColumns.forEach((column, indexValue) => {
|
|
@@ -70,24 +70,37 @@ export async function DeleteData({ url, ...props }) {
|
|
|
70
70
|
*
|
|
71
71
|
* @param {*} param
|
|
72
72
|
*/
|
|
73
|
-
|
|
74
73
|
export async function ApiCall({ url, formBody, method, settings, ...props }) {
|
|
75
74
|
const token = props.token || (await settings.getToken());
|
|
76
75
|
const path = window.location.pathname;
|
|
77
76
|
const baseUrl = props.baseUrl || process.env.REACT_APP_endpoint;
|
|
78
77
|
|
|
78
|
+
const isFormData = formBody instanceof FormData;
|
|
79
|
+
|
|
79
80
|
const payload = {
|
|
80
81
|
method,
|
|
81
82
|
headers: {
|
|
82
83
|
...settings.headers,
|
|
83
|
-
...headers,
|
|
84
84
|
...(props.headers || {}),
|
|
85
85
|
Authorization: `Bearer ${token}`,
|
|
86
|
-
'Content-Type': 'application/json',
|
|
87
86
|
},
|
|
88
|
-
body: formBody ? JSON.stringify(formBody) : null,
|
|
89
87
|
};
|
|
90
88
|
|
|
89
|
+
// ✅ Upload + normal handling
|
|
90
|
+
if (method !== 'GET' && formBody) {
|
|
91
|
+
payload.body = formBody;
|
|
92
|
+
|
|
93
|
+
if (!isFormData && !(props.isUpload)) {
|
|
94
|
+
payload.body = JSON.stringify(formBody);
|
|
95
|
+
payload.headers['Content-Type'] = 'application/json';
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// ❗ VERY IMPORTANT
|
|
99
|
+
if (isFormData || (props.isUpload)) {
|
|
100
|
+
delete payload.headers['Content-Type'];
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
91
104
|
// 🟡 Handles both normal and retried responses
|
|
92
105
|
const handleResponse = async (res) => {
|
|
93
106
|
if (props.responseType === 'blob') {
|
|
@@ -156,3 +169,18 @@ export async function ApiCall({ url, formBody, method, settings, ...props }) {
|
|
|
156
169
|
throw err;
|
|
157
170
|
}
|
|
158
171
|
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Method for uploading documents
|
|
175
|
+
* @public
|
|
176
|
+
*/
|
|
177
|
+
export async function UploadData({ url, formBody, ...props }) {
|
|
178
|
+
return ApiCall({
|
|
179
|
+
url,
|
|
180
|
+
formBody,
|
|
181
|
+
isUpload: true,
|
|
182
|
+
method: 'POST',
|
|
183
|
+
returnResponse: true,
|
|
184
|
+
...props,
|
|
185
|
+
});
|
|
186
|
+
}
|
|
@@ -25,7 +25,7 @@ function getExportDefinition(entry, record) {
|
|
|
25
25
|
* @param {Array} root0.columns
|
|
26
26
|
* @param {Array} root0.patients
|
|
27
27
|
* @param {boolean} root0.isFixedIndex
|
|
28
|
-
* @param {Object} root0.CustomComponents
|
|
28
|
+
* @param {Object.<string, React.ComponentType<any>>} root0.CustomComponents
|
|
29
29
|
* @param {Function} root0.refresh
|
|
30
30
|
* @param {Object} [root0.otherDetails={}] - Optional details from the report configuration.
|
|
31
31
|
* @param {boolean} [root0.otherDetails.isFilterEnabled] - Fallback to enable filtering on all columns.
|
|
@@ -298,7 +298,6 @@ export default function ReportingDashboard({
|
|
|
298
298
|
}
|
|
299
299
|
// Update patients
|
|
300
300
|
setPatients(resultDetails || []);
|
|
301
|
-
console.log(parsedColumns);
|
|
302
301
|
|
|
303
302
|
// Check if columns are not yet defined
|
|
304
303
|
if (parsedColumns.length === 0 && resultDetails.length > 0) {
|
|
@@ -601,6 +600,7 @@ function GuestList({
|
|
|
601
600
|
const [single, setSingle] = useState({});
|
|
602
601
|
const otherDetails = config.other_details1 ? JSON.parse(config.other_details1) : {};
|
|
603
602
|
|
|
603
|
+
// const otherDetails = config.other_details1 ? JSON.parse(config.other_details1) : {};
|
|
604
604
|
// const [view, setView] = useState(isMobile ? true : false); //Need to check this condition
|
|
605
605
|
const cols = buildDisplayColumns({
|
|
606
606
|
columns,
|
|
@@ -608,7 +608,7 @@ function GuestList({
|
|
|
608
608
|
isFixedIndex,
|
|
609
609
|
CustomComponents,
|
|
610
610
|
refresh,
|
|
611
|
-
otherDetails
|
|
611
|
+
otherDetails,
|
|
612
612
|
});
|
|
613
613
|
|
|
614
614
|
/**
|
|
@@ -654,8 +654,39 @@ function GuestList({
|
|
|
654
654
|
}
|
|
655
655
|
return col;
|
|
656
656
|
});
|
|
657
|
+
const summaryCols = columns.filter((col) => col.enable_summary);
|
|
658
|
+
let dataToExport = [...patients];
|
|
659
|
+
|
|
660
|
+
if (summaryCols.length > 0) {
|
|
661
|
+
// Build one synthetic row for CSV export that mirrors the table layout:
|
|
662
|
+
// numeric summary cells are populated from `calculateSummaryValues`, while
|
|
663
|
+
// non-summary columns stay blank unless a configured caption should be shown.
|
|
664
|
+
const summaryValues = calculateSummaryValues(summaryCols, patients);
|
|
665
|
+
const summaryRow = { isSummaryRow: true };
|
|
666
|
+
|
|
667
|
+
cols.forEach((col, index) => {
|
|
668
|
+
// Start each export column empty so the appended row keeps the same shape
|
|
669
|
+
// as the data rows and does not leak index/helper values into the export.
|
|
670
|
+
const colKey = col.field || col.key || col.dataIndex;
|
|
671
|
+
if (colKey && !summaryRow[colKey]) {
|
|
672
|
+
summaryRow[colKey] = '';
|
|
673
|
+
}
|
|
657
674
|
|
|
658
|
-
|
|
675
|
+
if (summaryValues[col.field] !== undefined) {
|
|
676
|
+
// Fill columns that have an aggregate configured (sum, count, avg, etc.).
|
|
677
|
+
summaryRow[col.field] = summaryValues[col.field];
|
|
678
|
+
} else {
|
|
679
|
+
// If this column is marked as the caption target for a summary column,
|
|
680
|
+
// place the configured label (for example "Total") into that cell.
|
|
681
|
+
const captionConfig = columns.find((c) => col.field && c.caption_field === col.field);
|
|
682
|
+
if (captionConfig) {
|
|
683
|
+
summaryRow[col.field] = captionConfig.summary_caption || '';
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
});
|
|
687
|
+
dataToExport.push(summaryRow);
|
|
688
|
+
}
|
|
689
|
+
let exportDatas = getExportData(dataToExport, exportCols);
|
|
659
690
|
|
|
660
691
|
if (exportDatas.exportDataColumns.length && exportDatas.exportDataHeaders.length) {
|
|
661
692
|
setExportData({ exportDatas });
|
|
@@ -740,6 +771,55 @@ function GuestList({
|
|
|
740
771
|
const handleCloseEdit = () => {
|
|
741
772
|
setShowEdit(false);
|
|
742
773
|
};
|
|
774
|
+
/**
|
|
775
|
+
* Calculates aggregate values for the configured summary columns.
|
|
776
|
+
*
|
|
777
|
+
* Each summary definition contributes one value keyed by its `field`. Missing
|
|
778
|
+
* row values are treated as `0` for numeric operations so the table summary and
|
|
779
|
+
* export summary row can be built from the same result object.
|
|
780
|
+
*
|
|
781
|
+
* Supported functions:
|
|
782
|
+
* `sum` - totals all numeric values in the field.
|
|
783
|
+
* `count` - returns the number of rows in the current dataset.
|
|
784
|
+
* `avg` - returns the arithmetic mean of the field values.
|
|
785
|
+
* `min` - returns the smallest numeric value in the field.
|
|
786
|
+
* `max` - returns the largest numeric value in the field.
|
|
787
|
+
*
|
|
788
|
+
* @param {Array<Object>} summaryCols - Column configs with `field` and `function`.
|
|
789
|
+
* @param {Array<Object>} pageData - Rows currently being summarized.
|
|
790
|
+
* @returns {Object} Aggregate values keyed by field name.
|
|
791
|
+
*/
|
|
792
|
+
function calculateSummaryValues(summaryCols, pageData) {
|
|
793
|
+
const summaryValues = {};
|
|
794
|
+
|
|
795
|
+
summaryCols.forEach((col) => {
|
|
796
|
+
const field = col.field;
|
|
797
|
+
|
|
798
|
+
if (col.function === 'sum') {
|
|
799
|
+
summaryValues[field] = pageData.reduce((total, row) => total + Number(row[field] || 0), 0);
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
if (col.function === 'count') {
|
|
803
|
+
summaryValues[field] = pageData.length;
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
if (col.function === 'avg') {
|
|
807
|
+
const total = pageData.reduce((sum, row) => sum + Number(row[field] || 0), 0);
|
|
808
|
+
summaryValues[field] = pageData.length ? total / pageData.length : 0;
|
|
809
|
+
}
|
|
810
|
+
if (col.function === 'min') {
|
|
811
|
+
const values = pageData.map((row) => Number(row[field] || 0));
|
|
812
|
+
summaryValues[field] = values.length ? Math.min(...values) : 0;
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
if (col.function === 'max') {
|
|
816
|
+
const values = pageData.map((row) => Number(row[field] || 0));
|
|
817
|
+
summaryValues[field] = values.length ? Math.max(...values) : 0;
|
|
818
|
+
}
|
|
819
|
+
});
|
|
820
|
+
|
|
821
|
+
return summaryValues;
|
|
822
|
+
}
|
|
743
823
|
return (
|
|
744
824
|
<>
|
|
745
825
|
<div className="table-header">
|
|
@@ -790,6 +870,7 @@ function GuestList({
|
|
|
790
870
|
{exportData.exportDatas && (
|
|
791
871
|
<ExportReactCSV
|
|
792
872
|
title={config.caption}
|
|
873
|
+
fileName={`${(config.caption || 'Report').trim().replace(/\s+/g, '_')}_${moment().format('YYYY-MM-DD-HH-mm-ss-SSS')}.xlsx`}
|
|
793
874
|
headers={exportData.exportDatas.exportDataHeaders}
|
|
794
875
|
csvData={exportData.exportDatas.exportDataColumns}
|
|
795
876
|
/>
|
|
@@ -814,47 +895,37 @@ function GuestList({
|
|
|
814
895
|
columns={cols}
|
|
815
896
|
sticky
|
|
816
897
|
pagination={false}
|
|
817
|
-
// title={config.caption}
|
|
818
898
|
summary={(pageData) => {
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
];
|
|
826
|
-
|
|
827
|
-
let tableColumns = cols;
|
|
828
|
-
|
|
829
|
-
// Creating a copy of columns to append the summary configuration that is needed to set
|
|
830
|
-
tableColumns.forEach((record, index) => {
|
|
831
|
-
summaryColumns.forEach((inner) => {
|
|
832
|
-
if (record.field === inner.field) {
|
|
833
|
-
tableColumns[index].summary = inner;
|
|
834
|
-
}
|
|
835
|
-
});
|
|
836
|
-
});
|
|
837
|
-
|
|
838
|
-
// Initialize
|
|
839
|
-
summaryColumns.map((item) => {
|
|
840
|
-
return (summary[item.field] = 0);
|
|
841
|
-
});
|
|
842
|
-
|
|
843
|
-
// Find the total
|
|
844
|
-
summaryColumns.map((item) => {
|
|
845
|
-
pageData.forEach((entry) => {
|
|
846
|
-
return (summary[item.field] = summary[item.field] + entry[item.field]);
|
|
847
|
-
});
|
|
848
|
-
});
|
|
899
|
+
const summaryCols = columns.filter((col) => col.enable_summary);
|
|
900
|
+
if (!summaryCols.length) return null;
|
|
901
|
+
/** calculate summary*/
|
|
902
|
+
|
|
903
|
+
const summaryValues = calculateSummaryValues(summaryCols, pageData);
|
|
904
|
+
|
|
849
905
|
|
|
850
906
|
return (
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
return
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
907
|
+
<Table.Summary.Row className="report-summary-row">
|
|
908
|
+
{cols.map((col, index) => {
|
|
909
|
+
if (summaryValues[col.field] !== undefined) {
|
|
910
|
+
return (
|
|
911
|
+
<Table.Summary.Cell key={index}>
|
|
912
|
+
<strong style={{ fontWeight: 900 }}>{summaryValues[col.field]}</strong>
|
|
913
|
+
</Table.Summary.Cell>
|
|
914
|
+
);
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
const captionConfig = columns.find((c) => col.field && c.caption_field === col.field);
|
|
918
|
+
if (captionConfig) {
|
|
919
|
+
return (
|
|
920
|
+
<Table.Summary.Cell key={index}>
|
|
921
|
+
<strong style={{ fontWeight: 900 }}>{captionConfig.summary_caption || ''}</strong>
|
|
922
|
+
</Table.Summary.Cell>
|
|
923
|
+
);
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
return <Table.Summary.Cell key={index} />;
|
|
927
|
+
})}
|
|
928
|
+
</Table.Summary.Row>
|
|
858
929
|
);
|
|
859
930
|
}}
|
|
860
931
|
/>
|