@visns-studio/visns-components 5.10.10 → 5.11.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.
@@ -0,0 +1,194 @@
1
+ import React, { useEffect, useState } from 'react';
2
+ import { toast } from 'react-toastify';
3
+ import { saveAs } from 'file-saver';
4
+ import { CloudDownload } from 'akar-icons';
5
+
6
+ import Download from '../Download';
7
+ import Field from '../Field';
8
+
9
+ import reportStyles from './styles/ReportForm.module.scss';
10
+
11
+ const GenericReportForm = ({ config, userProfile, onExport, onFormChange }) => {
12
+ const [formData, setFormData] = useState({});
13
+
14
+ const handleChange = (e) => {
15
+ const {
16
+ value,
17
+ dataset: { name },
18
+ } = e.target;
19
+
20
+ const newFormData = { ...formData, [name]: value };
21
+ setFormData(newFormData);
22
+
23
+ // Notify parent component of form change
24
+ if (onFormChange) {
25
+ onFormChange(newFormData);
26
+ }
27
+ };
28
+
29
+ const handleChangeDate = (date, id) => {
30
+ const newFormData = { ...formData, [id]: date };
31
+ setFormData(newFormData);
32
+
33
+ // Notify parent component of form change
34
+ if (onFormChange) {
35
+ onFormChange(newFormData);
36
+ }
37
+ };
38
+
39
+ const handleChangeSelect = (inputValue, _, id) => {
40
+ const newFormData = { ...formData, [id]: inputValue };
41
+ setFormData(newFormData);
42
+
43
+ // Notify parent component of form change
44
+ if (onFormChange) {
45
+ onFormChange(newFormData);
46
+ }
47
+ };
48
+
49
+ const handleExport = async (e) => {
50
+ try {
51
+ if (e) {
52
+ e.preventDefault();
53
+ }
54
+
55
+ // Check if there's a custom export handler
56
+ if (onExport) {
57
+ return onExport(formData);
58
+ }
59
+
60
+ // Default export logic
61
+ const missingFields = config.form.filters
62
+ .filter(
63
+ (item) => item.required === true && formData[item.id] === ''
64
+ )
65
+ .map((item) => item.label);
66
+
67
+ if (missingFields.length > 0) {
68
+ const fieldNames = missingFields.join(', ');
69
+ const errorMessage = `Please fill in a value for the following field(s): ${fieldNames}.`;
70
+ toast.error(errorMessage);
71
+ } else {
72
+ toast.info('Preparing your export...');
73
+
74
+ const res = await Download(
75
+ config.form.url,
76
+ config.form.method,
77
+ formData
78
+ );
79
+
80
+ if (res.data.error) {
81
+ toast.error(res.data.error);
82
+ } else {
83
+ // Assuming the response contains a Blob or similar data
84
+ if (res.data && res.data instanceof Blob) {
85
+ const fileName = `${config.form.filename}`;
86
+ saveAs(res.data, fileName);
87
+ toast.success('Export completed successfully');
88
+ } else {
89
+ // Handle the case where the response is not a Blob
90
+ toast.error('Invalid file format received.');
91
+ }
92
+ }
93
+ }
94
+ } catch (err) {
95
+ console.error(err);
96
+ toast.error('An error occurred during export');
97
+ }
98
+ };
99
+
100
+ useEffect(() => {
101
+ if (config.form?.filters) {
102
+ const tmpForm = config.form.filters.reduce((acc, item) => {
103
+ acc[item.id] = '';
104
+ return acc;
105
+ }, {});
106
+ setFormData(tmpForm);
107
+
108
+ // Notify parent component of initial form data
109
+ if (onFormChange) {
110
+ onFormChange(tmpForm);
111
+ }
112
+ }
113
+ }, [config.form?.filters, onFormChange]);
114
+
115
+ // Determine if the form has a title
116
+ const formTitle = config.form?.title || 'Export Report';
117
+ const formDescription =
118
+ config.form?.description || 'Select your export criteria below.';
119
+
120
+ // Check if form configuration is valid
121
+ if (!config.form?.filters) {
122
+ return (
123
+ <div className={reportStyles.reportFormContainer}>
124
+ <div className={reportStyles.reportForm}>
125
+ <h2 className={reportStyles.formTitle}>No Report Configuration</h2>
126
+ <p className={reportStyles.formDescription}>
127
+ Report form configuration is missing.
128
+ </p>
129
+ </div>
130
+ </div>
131
+ );
132
+ }
133
+
134
+ return (
135
+ <div className={reportStyles.reportFormContainer}>
136
+ <form className={reportStyles.reportForm}>
137
+ <h2 className={reportStyles.formTitle}>{formTitle}</h2>
138
+ <p className={reportStyles.formDescription}>
139
+ {formDescription}
140
+ </p>
141
+
142
+ {config.form.filters.map((item, key) => (
143
+ <div
144
+ key={`filter-container-${key}`}
145
+ className={`${reportStyles.formField} ${
146
+ item.fullWidth ? reportStyles.fullWidthField : ''
147
+ }`}
148
+ >
149
+ <Field
150
+ api={userProfile?.settings?.api}
151
+ autocompleteCallback={() => {}}
152
+ childDropdownCallback={() => {}}
153
+ fetchData={() => {}}
154
+ formData={formData}
155
+ inputClass={''}
156
+ inputValue={formData[item.id]}
157
+ key={`filter-fields-${key}`}
158
+ mapbox={{}}
159
+ onChange={handleChange}
160
+ onChangeCheckbox={() => {}}
161
+ onChangeDate={handleChangeDate}
162
+ onChangeNumberFormat={() => {}}
163
+ onChangeRicheditor={() => {}}
164
+ onChangeSelect={handleChangeSelect}
165
+ onChangeToggle={() => {}}
166
+ settings={item}
167
+ setFormData={setFormData}
168
+ style={userProfile?.settings?.style}
169
+ uploadProgress={0}
170
+ userProfile={userProfile}
171
+ />
172
+ </div>
173
+ ))}
174
+
175
+ <div className={reportStyles.formActions}>
176
+ <button
177
+ className={reportStyles.exportButton}
178
+ onClick={handleExport}
179
+ type="button"
180
+ >
181
+ <CloudDownload
182
+ strokeWidth={2}
183
+ size={18}
184
+ style={{ marginRight: '8px' }}
185
+ />{' '}
186
+ Export
187
+ </button>
188
+ </div>
189
+ </form>
190
+ </div>
191
+ );
192
+ };
193
+
194
+ export default GenericReportForm;
@@ -0,0 +1,125 @@
1
+ import parse from 'html-react-parser';
2
+ import moment from 'moment';
3
+ import numeral from 'numeral';
4
+
5
+ /**
6
+ * Format cell content based on column type or value type
7
+ * @param {*} value - The value to format
8
+ * @param {Object} column - Column configuration object
9
+ * @returns {*} Formatted value
10
+ */
11
+ export const formatCellContent = (value, column) => {
12
+ if (value === null || value === undefined) return '';
13
+ if (value === 0) return '0'; // Explicitly handle zero values
14
+ if (value === '') return '';
15
+
16
+ // Try to infer the type if not provided
17
+ const valueType = column.type || typeof value;
18
+
19
+ switch (valueType) {
20
+ case 'date':
21
+ return moment(value).isValid()
22
+ ? moment(value).format(column.format || 'DD/MM/YYYY')
23
+ : value;
24
+ case 'datetime':
25
+ return moment(value).isValid()
26
+ ? moment(value).format(column.format || 'DD/MM/YYYY HH:mm')
27
+ : value;
28
+ case 'currency':
29
+ return numeral(value).format(column.format || '$0,0.00');
30
+ case 'number':
31
+ // Check if it's actually a number before formatting
32
+ return !isNaN(parseFloat(value)) && isFinite(value)
33
+ ? numeral(value).format(column.format || '0,0')
34
+ : value;
35
+ case 'boolean':
36
+ if (
37
+ value === 1 ||
38
+ value === true ||
39
+ value === 'true' ||
40
+ value === 'yes' ||
41
+ value === 'Yes'
42
+ ) {
43
+ return 'Yes';
44
+ }
45
+ if (
46
+ value === 0 ||
47
+ value === false ||
48
+ value === 'false' ||
49
+ value === 'no' ||
50
+ value === 'No'
51
+ ) {
52
+ return 'No';
53
+ }
54
+ return value;
55
+ case 'year':
56
+ // Special handling for year values
57
+ return value.toString();
58
+ case 'quarter':
59
+ // Special handling for quarter values (Q1, Q2, etc.)
60
+ if (value && value.toString().startsWith('Q')) {
61
+ return value;
62
+ }
63
+ return value;
64
+ case 'html':
65
+ // Explicitly parse HTML content
66
+ return parse(String(value));
67
+ case 'richtext':
68
+ // Parse rich text content
69
+ return parse(String(value));
70
+ default:
71
+ // For text values, check if it contains HTML tags
72
+ if (
73
+ typeof value === 'string' &&
74
+ (value.includes('<br') ||
75
+ value.includes('<p>') ||
76
+ value.includes('<div') ||
77
+ value.includes('<span'))
78
+ ) {
79
+ // Ensure <br /> tags are properly handled
80
+ const processedValue = String(value)
81
+ .replace(/<br \/>/g, '<br>')
82
+ .replace(/<br\/>/g, '<br>');
83
+ return parse(processedValue);
84
+ }
85
+ return value;
86
+ }
87
+ };
88
+
89
+ /**
90
+ * Generate year options for dropdown
91
+ * @param {number} startYear - Starting year
92
+ * @returns {Array} Array of year options
93
+ */
94
+ export const generateYearOptions = (startYear) => {
95
+ const currentYear = new Date().getFullYear();
96
+ const years = [];
97
+
98
+ // Start from the specified year and go up to current year
99
+ for (let year = startYear; year <= currentYear + 1; year++) {
100
+ years.push({ value: year.toString(), label: year.toString() });
101
+ }
102
+
103
+ return years;
104
+ };
105
+
106
+ /**
107
+ * Apply custom styling from userProfile
108
+ * @param {Object} userProfile - User profile object
109
+ * @returns {Object} Custom styles object
110
+ */
111
+ export const getCustomStyles = (userProfile) => {
112
+ const customStyles = {};
113
+
114
+ if (userProfile?.settings?.style) {
115
+ const { style } = userProfile.settings;
116
+
117
+ if (style.hoverColor) {
118
+ customStyles.hoverColor = {
119
+ '--hover-color': style.hoverColor,
120
+ };
121
+ }
122
+ }
123
+
124
+ return customStyles;
125
+ };
@@ -0,0 +1,194 @@
1
+ /**
2
+ * Sort data based on column configuration
3
+ * @param {Array} data - Data array to sort
4
+ * @param {string} key - Column key to sort by
5
+ * @param {string} direction - Sort direction ('asc' or 'desc')
6
+ * @param {Array} gridHeaders - Grid headers configuration
7
+ * @returns {Array} Sorted data array
8
+ */
9
+ export const sortData = (data, key, direction, gridHeaders) => {
10
+ if (!key) return [...data];
11
+
12
+ return [...data].sort((a, b) => {
13
+ // Find the header config for this key to determine type
14
+ const headerConfig = gridHeaders.find((h) => h.key === key);
15
+ const type = headerConfig?.type || 'text';
16
+
17
+ // Get values, handling null/undefined
18
+ let aValue = a[key] === null || a[key] === undefined ? '' : a[key];
19
+ let bValue = b[key] === null || b[key] === undefined ? '' : b[key];
20
+
21
+ // Sort based on type
22
+ switch (type) {
23
+ case 'number':
24
+ case 'year':
25
+ aValue = parseFloat(aValue) || 0;
26
+ bValue = parseFloat(bValue) || 0;
27
+ break;
28
+ case 'date':
29
+ // Convert to timestamp for comparison
30
+ aValue = aValue ? new Date(aValue).getTime() : 0;
31
+ bValue = bValue ? new Date(bValue).getTime() : 0;
32
+ break;
33
+ default:
34
+ // For text, convert to lowercase strings
35
+ aValue = String(aValue).toLowerCase();
36
+ bValue = String(bValue).toLowerCase();
37
+ }
38
+
39
+ // Compare values
40
+ if (aValue < bValue) {
41
+ return direction === 'asc' ? -1 : 1;
42
+ }
43
+ if (aValue > bValue) {
44
+ return direction === 'asc' ? 1 : -1;
45
+ }
46
+ return 0;
47
+ });
48
+ };
49
+
50
+ /**
51
+ * Filter data based on filter values and configuration
52
+ * @param {Array} data - Data array to filter
53
+ * @param {Object} filterValues - Filter values object
54
+ * @param {Array} gridHeaders - Grid headers configuration
55
+ * @returns {Array} Filtered data array
56
+ */
57
+ export const filterData = (data, filterValues, gridHeaders) => {
58
+ // If no filter values are set, return the original data
59
+ if (Object.keys(filterValues).length === 0) {
60
+ return data;
61
+ }
62
+
63
+ // Filter the data based on the filter values
64
+ return data.filter((row) => {
65
+ // Check each filter
66
+ return Object.entries(filterValues).every(([key, value]) => {
67
+ // Skip empty filter values
68
+ if (!value) return true;
69
+
70
+ // Get the row value
71
+ const rowValue = row[key];
72
+ if (rowValue === null || rowValue === undefined) return false;
73
+
74
+ // Find the header config for this key
75
+ const headerConfig = gridHeaders.find((h) => h.key === key);
76
+ if (!headerConfig || !headerConfig.filter) return true;
77
+
78
+ const { type, operator } = headerConfig.filter;
79
+
80
+ // Apply filter based on type and operator
81
+ switch (type) {
82
+ case 'string':
83
+ const rowStr = String(rowValue).toLowerCase();
84
+ const filterStr = String(value).toLowerCase();
85
+
86
+ switch (operator) {
87
+ case 'equals':
88
+ return rowStr === filterStr;
89
+ case 'contains':
90
+ return rowStr.includes(filterStr);
91
+ case 'startsWith':
92
+ return rowStr.startsWith(filterStr);
93
+ case 'endsWith':
94
+ return rowStr.endsWith(filterStr);
95
+ default:
96
+ return rowStr.includes(filterStr);
97
+ }
98
+
99
+ case 'number':
100
+ const rowNum = parseFloat(rowValue);
101
+ const filterNum = parseFloat(value);
102
+
103
+ if (isNaN(rowNum) || isNaN(filterNum)) return false;
104
+
105
+ switch (operator) {
106
+ case 'equals':
107
+ return rowNum === filterNum;
108
+ case 'greaterThan':
109
+ return rowNum > filterNum;
110
+ case 'lessThan':
111
+ return rowNum < filterNum;
112
+ case 'greaterThanOrEqual':
113
+ return rowNum >= filterNum;
114
+ case 'lessThanOrEqual':
115
+ return rowNum <= filterNum;
116
+ default:
117
+ return rowNum === filterNum;
118
+ }
119
+
120
+ case 'date':
121
+ const rowDate = new Date(rowValue);
122
+ const filterDate = new Date(value);
123
+
124
+ if (isNaN(rowDate.getTime()) || isNaN(filterDate.getTime()))
125
+ return false;
126
+
127
+ switch (operator) {
128
+ case 'equals':
129
+ return (
130
+ rowDate.toDateString() ===
131
+ filterDate.toDateString()
132
+ );
133
+ case 'after':
134
+ return rowDate > filterDate;
135
+ case 'before':
136
+ return rowDate < filterDate;
137
+ default:
138
+ return (
139
+ rowDate.toDateString() ===
140
+ filterDate.toDateString()
141
+ );
142
+ }
143
+
144
+ default:
145
+ return true;
146
+ }
147
+ });
148
+ });
149
+ };
150
+
151
+ /**
152
+ * Transform headers from API response to grid headers format
153
+ * @param {Object} headerData - Header data from API response
154
+ * @returns {Array} Formatted grid headers array
155
+ */
156
+ export const transformApiHeaders = (headerData) => {
157
+ const headerEntries = Object.entries(headerData);
158
+ return headerEntries.map(([key, config]) => ({
159
+ key,
160
+ label: config.label || key,
161
+ sort: config.sort || key,
162
+ type: config.type || 'text',
163
+ filter: config.filter || null,
164
+ onClick: config.onClick || null,
165
+ }));
166
+ };
167
+
168
+ /**
169
+ * Initialize filter values from grid headers
170
+ * @param {Array} gridHeaders - Grid headers array
171
+ * @returns {Object} Initial filter values object
172
+ */
173
+ export const initializeFilterValues = (gridHeaders) => {
174
+ const initialFilterValues = {};
175
+ gridHeaders.forEach((header) => {
176
+ if (header.filter) {
177
+ initialFilterValues[header.key] = '';
178
+ }
179
+ });
180
+ return initialFilterValues;
181
+ };
182
+
183
+ /**
184
+ * Get sort indicator style based on current sort configuration
185
+ * @param {string} key - Column key
186
+ * @param {Object} sortConfig - Current sort configuration
187
+ * @returns {Object} Style object for sort indicator
188
+ */
189
+ export const getSortIndicatorStyle = (key, sortConfig) => {
190
+ if (sortConfig.key !== key) {
191
+ return { opacity: 0.3 }; // Dim the icon when not sorting by this column
192
+ }
193
+ return {}; // Full opacity when sorting by this column
194
+ };