@visns-studio/visns-components 5.15.23 → 5.15.25
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/package.json +11 -11
- package/src/components/BusinessCardOcr.jsx +49 -4
- package/src/components/DataGrid.jsx +53 -1
- package/src/components/Fetch.jsx +39 -13
- package/src/components/ImportWizardModal.jsx +68 -53
- package/src/components/controls/DataGridSearch.jsx +27 -1
- package/src/components/generic/GenericDashboard.jsx +7 -1
- package/src/components/generic/GenericDetail.jsx +13 -0
- package/src/components/generic/GenericFormBuilder.jsx +12 -0
- package/src/components/styles/ImportWizardModal.module.scss +107 -59
package/package.json
CHANGED
|
@@ -8,15 +8,15 @@
|
|
|
8
8
|
"@inovua/reactdatagrid-community": "^5.10.2",
|
|
9
9
|
"@inovua/reactdatagrid-enterprise": "^5.10.2",
|
|
10
10
|
"@mapbox/mapbox-gl-geocoder": "^5.1.2",
|
|
11
|
-
"@nivo/bar": "^0.
|
|
12
|
-
"@nivo/core": "^0.
|
|
13
|
-
"@nivo/line": "^0.
|
|
14
|
-
"@nivo/pie": "^0.
|
|
15
|
-
"@react-pdf/renderer": "^4.3.
|
|
16
|
-
"@tanstack/react-query": "^5.
|
|
11
|
+
"@nivo/bar": "^0.87.0",
|
|
12
|
+
"@nivo/core": "^0.87.0",
|
|
13
|
+
"@nivo/line": "^0.87.0",
|
|
14
|
+
"@nivo/pie": "^0.87.0",
|
|
15
|
+
"@react-pdf/renderer": "^4.3.1",
|
|
16
|
+
"@tanstack/react-query": "^5.90.2",
|
|
17
17
|
"@tinymce/miniature": "^6.0.0",
|
|
18
18
|
"@tinymce/tinymce-react": "^6.3.0",
|
|
19
|
-
"@uiw/react-color": "^2.
|
|
19
|
+
"@uiw/react-color": "^2.9.0",
|
|
20
20
|
"@visns-studio/visns-datagrid-community": "1.1.0",
|
|
21
21
|
"@visns-studio/visns-datagrid-enterprise": "1.1.0",
|
|
22
22
|
"@vitejs/plugin-react": "^4.7.0",
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"dayjs": "^1.11.18",
|
|
28
28
|
"fabric": "^6.7.1",
|
|
29
29
|
"file-saver": "^2.0.5",
|
|
30
|
-
"framer-motion": "^12.23.
|
|
30
|
+
"framer-motion": "^12.23.22",
|
|
31
31
|
"html-react-parser": "^5.2.6",
|
|
32
32
|
"html2canvas": "^1.4.1",
|
|
33
33
|
"lodash": "^4.17.21",
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"lucide-react": "^0.544.0",
|
|
36
36
|
"mapbox-gl": "^3.15.0",
|
|
37
37
|
"moment": "^2.30.1",
|
|
38
|
-
"motion": "^12.23.
|
|
38
|
+
"motion": "^12.23.22",
|
|
39
39
|
"numeral": "^2.0.6",
|
|
40
40
|
"pluralize": "^8.0.0",
|
|
41
41
|
"qrcode.react": "^4.2.0",
|
|
@@ -86,7 +86,7 @@
|
|
|
86
86
|
"mini-css-extract-plugin": "^2.9.4",
|
|
87
87
|
"react": "^18.3.1",
|
|
88
88
|
"react-dom": "^18.3.1",
|
|
89
|
-
"sass": "^1.
|
|
89
|
+
"sass": "^1.93.2",
|
|
90
90
|
"sass-loader": "^16.0.5"
|
|
91
91
|
},
|
|
92
92
|
"peerDependencies": {
|
|
@@ -94,7 +94,7 @@
|
|
|
94
94
|
"react-dom": "^17.0.0 || ^18.0.0"
|
|
95
95
|
},
|
|
96
96
|
"name": "@visns-studio/visns-components",
|
|
97
|
-
"version": "5.15.
|
|
97
|
+
"version": "5.15.25",
|
|
98
98
|
"description": "Various packages to assist in the development of our Custom Applications.",
|
|
99
99
|
"main": "src/index.js",
|
|
100
100
|
"files": [
|
|
@@ -548,6 +548,33 @@ const BusinessCardOcr = ({
|
|
|
548
548
|
}
|
|
549
549
|
};
|
|
550
550
|
|
|
551
|
+
// AWS Textract cloud OCR
|
|
552
|
+
const performCloudOCR = async (imageBlob) => {
|
|
553
|
+
const formData = new FormData();
|
|
554
|
+
formData.append('image', imageBlob, 'business-card.jpg');
|
|
555
|
+
|
|
556
|
+
setOcrProgress(20);
|
|
557
|
+
|
|
558
|
+
try {
|
|
559
|
+
const response = await CustomFetch(
|
|
560
|
+
'/ajax/ocr/processBusinessCardWithTextract',
|
|
561
|
+
'POST',
|
|
562
|
+
formData
|
|
563
|
+
);
|
|
564
|
+
|
|
565
|
+
setOcrProgress(100);
|
|
566
|
+
|
|
567
|
+
if (response.data && response.data.success) {
|
|
568
|
+
return response.data.data.extracted_data;
|
|
569
|
+
} else {
|
|
570
|
+
throw new Error(response.data?.message || 'Cloud OCR failed');
|
|
571
|
+
}
|
|
572
|
+
} catch (error) {
|
|
573
|
+
console.error('Cloud OCR error:', error);
|
|
574
|
+
throw error;
|
|
575
|
+
}
|
|
576
|
+
};
|
|
577
|
+
|
|
551
578
|
const processBusinessCard = async () => {
|
|
552
579
|
if (!capturedImage) return;
|
|
553
580
|
|
|
@@ -556,11 +583,29 @@ const BusinessCardOcr = ({
|
|
|
556
583
|
setOcrProgress(0);
|
|
557
584
|
|
|
558
585
|
try {
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
586
|
+
let parsedData;
|
|
587
|
+
|
|
588
|
+
// Try AWS Textract first (cloud-based, high accuracy)
|
|
589
|
+
try {
|
|
590
|
+
console.log('[OCR] Attempting AWS Textract...');
|
|
591
|
+
toast.info('Processing with AWS Textract...', { autoClose: 2000 });
|
|
592
|
+
parsedData = await performCloudOCR(capturedImage);
|
|
593
|
+
console.log('[OCR] AWS Textract successful:', parsedData);
|
|
594
|
+
toast.success('Business card processed with AWS Textract!');
|
|
595
|
+
} catch (cloudError) {
|
|
596
|
+
// Fallback to local Tesseract.js
|
|
597
|
+
console.warn('[OCR] AWS Textract failed, falling back to Tesseract.js:', cloudError);
|
|
598
|
+
toast.warning('Cloud OCR unavailable, using local processing...', { autoClose: 2000 });
|
|
599
|
+
|
|
600
|
+
const text = await performAdvancedOCR(capturedImage);
|
|
601
|
+
parsedData = intelligentFieldMapping(text);
|
|
602
|
+
parsedData.rawText = text;
|
|
603
|
+
|
|
604
|
+
toast.success('Business card processed locally!');
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
setExtractedData(parsedData);
|
|
562
608
|
setCurrentStep('results');
|
|
563
|
-
toast.success('Business card processed successfully!');
|
|
564
609
|
} catch (error) {
|
|
565
610
|
console.error('OCR Error:', error);
|
|
566
611
|
toast.error('Failed to process business card. Please try again.');
|
|
@@ -28,7 +28,7 @@ import React, {
|
|
|
28
28
|
useRef,
|
|
29
29
|
useState,
|
|
30
30
|
} from 'react';
|
|
31
|
-
import { useNavigate } from 'react-router-dom';
|
|
31
|
+
import { useNavigate, useParams } from 'react-router-dom';
|
|
32
32
|
import debounce from 'lodash.debounce';
|
|
33
33
|
import moment from 'moment';
|
|
34
34
|
import 'reactjs-popup/dist/index.css';
|
|
@@ -86,6 +86,7 @@ import DataGridSearch from './controls/DataGridSearch';
|
|
|
86
86
|
import AutoRefreshControls from './controls/AutoRefreshControls';
|
|
87
87
|
import AudioPlayer from './controls/AudioPlayer';
|
|
88
88
|
import GalleryModal from './modals/GalleryModal';
|
|
89
|
+
import ImportWizardModal from './ImportWizardModal';
|
|
89
90
|
import {
|
|
90
91
|
renderBooleanColumn,
|
|
91
92
|
renderCurrencyColumn,
|
|
@@ -253,10 +254,12 @@ const DataGrid = forwardRef(
|
|
|
253
254
|
style,
|
|
254
255
|
tableSetting,
|
|
255
256
|
userProfile = null,
|
|
257
|
+
onDataReload,
|
|
256
258
|
},
|
|
257
259
|
ref
|
|
258
260
|
) => {
|
|
259
261
|
const navigate = useNavigate();
|
|
262
|
+
const routeParams = useParams();
|
|
260
263
|
|
|
261
264
|
/**
|
|
262
265
|
* DEBUG: Settings Configuration Tracking
|
|
@@ -314,6 +317,9 @@ const DataGrid = forwardRef(
|
|
|
314
317
|
const [bulkEditMode, setBulkEditMode] = useState(false);
|
|
315
318
|
const [bulkEditGroupData, setBulkEditGroupData] = useState(null);
|
|
316
319
|
|
|
320
|
+
/** Import Modal States */
|
|
321
|
+
const [showImportModal, setShowImportModal] = useState(false);
|
|
322
|
+
|
|
317
323
|
/** Modal Functions */
|
|
318
324
|
const modalOpen = (formType, formId) => {
|
|
319
325
|
setModalShow(true);
|
|
@@ -327,6 +333,29 @@ const DataGrid = forwardRef(
|
|
|
327
333
|
setBulkEditGroupData(null);
|
|
328
334
|
};
|
|
329
335
|
|
|
336
|
+
/** Import Modal Functions */
|
|
337
|
+
const handleBulkUploadClick = () => {
|
|
338
|
+
console.log('[DataGrid] Opening import modal with:', {
|
|
339
|
+
paramValue,
|
|
340
|
+
pageData,
|
|
341
|
+
routeParams,
|
|
342
|
+
'pageData?.id': pageData?.id,
|
|
343
|
+
'routeParams.dataId': routeParams.dataId,
|
|
344
|
+
'form.bulkUpload': form.bulkUpload
|
|
345
|
+
});
|
|
346
|
+
setShowImportModal(true);
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
const handleImportComplete = () => {
|
|
350
|
+
setShowImportModal(false);
|
|
351
|
+
// Trigger data refresh
|
|
352
|
+
loadData();
|
|
353
|
+
// Also trigger parent page data reload if callback provided
|
|
354
|
+
if (onDataReload) {
|
|
355
|
+
onDataReload();
|
|
356
|
+
}
|
|
357
|
+
};
|
|
358
|
+
|
|
330
359
|
useEffect(() => {
|
|
331
360
|
setFormData(form);
|
|
332
361
|
}, [form]);
|
|
@@ -3423,6 +3452,12 @@ const DataGrid = forwardRef(
|
|
|
3423
3452
|
})() && (
|
|
3424
3453
|
<div className={styles.dataGridTopContainer}>
|
|
3425
3454
|
<div className={styles.dataGridTopLeftContainer}>
|
|
3455
|
+
{(() => {
|
|
3456
|
+
console.log('[DataGrid] Rendering DataGridSearch with form:', form);
|
|
3457
|
+
console.log('[DataGrid] form.bulkUpload:', form.bulkUpload);
|
|
3458
|
+
console.log('[DataGrid] paramValue:', paramValue);
|
|
3459
|
+
return null;
|
|
3460
|
+
})()}
|
|
3426
3461
|
<DataGridSearch
|
|
3427
3462
|
form={form}
|
|
3428
3463
|
search={search}
|
|
@@ -3433,6 +3468,7 @@ const DataGrid = forwardRef(
|
|
|
3433
3468
|
paramValue={paramValue}
|
|
3434
3469
|
pageData={pageData}
|
|
3435
3470
|
pageSetting={pageSetting}
|
|
3471
|
+
onBulkUploadClick={handleBulkUploadClick}
|
|
3436
3472
|
/>
|
|
3437
3473
|
</div>
|
|
3438
3474
|
<div className={styles.dataGridTopRightContainer}>
|
|
@@ -4100,6 +4136,22 @@ const DataGrid = forwardRef(
|
|
|
4100
4136
|
</div>
|
|
4101
4137
|
</div>
|
|
4102
4138
|
)}
|
|
4139
|
+
|
|
4140
|
+
{/* Import Wizard Modal */}
|
|
4141
|
+
{form.bulkUpload && form.bulkUpload.enabled && (
|
|
4142
|
+
<ImportWizardModal
|
|
4143
|
+
isOpen={showImportModal}
|
|
4144
|
+
onClose={() => setShowImportModal(false)}
|
|
4145
|
+
modelConfig={form.bulkUpload.modelConfig}
|
|
4146
|
+
title={form.bulkUpload.title || 'Import Data'}
|
|
4147
|
+
importUrl={form.bulkUpload.importUrl}
|
|
4148
|
+
onComplete={handleImportComplete}
|
|
4149
|
+
additionalParams={{
|
|
4150
|
+
visit_request_id: paramValue || routeParams.dataId || pageData?.id,
|
|
4151
|
+
...form.bulkUpload.additionalParams
|
|
4152
|
+
}}
|
|
4153
|
+
/>
|
|
4154
|
+
)}
|
|
4103
4155
|
</>
|
|
4104
4156
|
);
|
|
4105
4157
|
}
|
package/src/components/Fetch.jsx
CHANGED
|
@@ -52,12 +52,15 @@ const generateCacheKey = (url, method, formData) => {
|
|
|
52
52
|
};
|
|
53
53
|
|
|
54
54
|
// Helper to determine if request should be cached
|
|
55
|
-
const shouldCache = (method, url) => {
|
|
55
|
+
const shouldCache = (method, url, cacheOption = true) => {
|
|
56
56
|
const normalizedMethod = method.toUpperCase();
|
|
57
|
-
|
|
57
|
+
|
|
58
|
+
// If caching is explicitly disabled via option, return false
|
|
59
|
+
if (cacheOption === false) return false;
|
|
60
|
+
|
|
58
61
|
// Don't cache GET requests
|
|
59
62
|
if (normalizedMethod === 'GET') return false;
|
|
60
|
-
|
|
63
|
+
|
|
61
64
|
// Cache specific safe POST endpoints that return reference data
|
|
62
65
|
const cacheableEndpoints = [
|
|
63
66
|
'/dropdown', // Dropdown data (industries, facilities, etc.)
|
|
@@ -69,7 +72,7 @@ const shouldCache = (method, url) => {
|
|
|
69
72
|
'/getWharfUsage',
|
|
70
73
|
'/getFloatingWharfUsage',
|
|
71
74
|
];
|
|
72
|
-
|
|
75
|
+
|
|
73
76
|
return cacheableEndpoints.some(endpoint => url.includes(endpoint));
|
|
74
77
|
};
|
|
75
78
|
|
|
@@ -130,11 +133,20 @@ const originalCustomFetch = (
|
|
|
130
133
|
errorCallback = null
|
|
131
134
|
) => {
|
|
132
135
|
const baseUrl = '';
|
|
136
|
+
|
|
137
|
+
// Check if formData is FormData (for file uploads)
|
|
138
|
+
const isFormData = formData instanceof FormData;
|
|
139
|
+
|
|
133
140
|
const headers = {
|
|
134
|
-
'Content-Type': 'application/json',
|
|
135
141
|
'X-Requested-With': 'XMLHttpRequest',
|
|
136
142
|
};
|
|
137
143
|
|
|
144
|
+
// Only set Content-Type for non-FormData requests
|
|
145
|
+
// FormData will set its own Content-Type with boundary
|
|
146
|
+
if (!isFormData) {
|
|
147
|
+
headers['Content-Type'] = 'application/json';
|
|
148
|
+
}
|
|
149
|
+
|
|
138
150
|
// Add CSRF token if available
|
|
139
151
|
const csrfToken = document
|
|
140
152
|
.querySelector('meta[name="csrf-token"]')
|
|
@@ -155,7 +167,7 @@ const originalCustomFetch = (
|
|
|
155
167
|
const cookiesSize = cookies ? cookies.length : 0;
|
|
156
168
|
|
|
157
169
|
// Check payload size
|
|
158
|
-
const payloadSize = new Blob([JSON.stringify(formData)]).size;
|
|
170
|
+
const payloadSize = isFormData ? 0 : new Blob([JSON.stringify(formData)]).size; // Skip size check for FormData
|
|
159
171
|
const totalRequestSize = payloadSize + headersSize + cookiesSize;
|
|
160
172
|
|
|
161
173
|
if (totalRequestSize > MAX_PAYLOAD_SIZE) {
|
|
@@ -169,11 +181,18 @@ const originalCustomFetch = (
|
|
|
169
181
|
return Promise.reject(new Error(errorMessage));
|
|
170
182
|
}
|
|
171
183
|
|
|
184
|
+
// Handle PUT/PATCH method workaround for Laravel
|
|
172
185
|
if (method.toUpperCase() === 'PUT' || method.toUpperCase() === 'PATCH') {
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
_method
|
|
176
|
-
}
|
|
186
|
+
if (isFormData) {
|
|
187
|
+
// For FormData, append _method field
|
|
188
|
+
formData.append('_method', method);
|
|
189
|
+
} else {
|
|
190
|
+
// For regular objects, spread and add _method
|
|
191
|
+
formData = {
|
|
192
|
+
...formData,
|
|
193
|
+
_method: method,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
177
196
|
}
|
|
178
197
|
|
|
179
198
|
// Configure fetch options
|
|
@@ -199,7 +218,11 @@ const originalCustomFetch = (
|
|
|
199
218
|
}
|
|
200
219
|
} else {
|
|
201
220
|
// For other methods, add body
|
|
202
|
-
|
|
221
|
+
if (formData) {
|
|
222
|
+
// If it's FormData, use it directly (for file uploads)
|
|
223
|
+
// Otherwise, JSON stringify the data
|
|
224
|
+
fetchOptions.body = isFormData ? formData : JSON.stringify(formData);
|
|
225
|
+
}
|
|
203
226
|
}
|
|
204
227
|
|
|
205
228
|
return new Promise((resolve, reject) => {
|
|
@@ -292,11 +315,14 @@ const CustomFetch = (
|
|
|
292
315
|
method,
|
|
293
316
|
formData,
|
|
294
317
|
successCallback = null,
|
|
295
|
-
errorCallback = null
|
|
318
|
+
errorCallback = null,
|
|
319
|
+
options = {}
|
|
296
320
|
) => {
|
|
297
321
|
const queryClient = getQueryClient();
|
|
298
322
|
const cacheKey = generateCacheKey(url, method, formData);
|
|
299
|
-
|
|
323
|
+
// Default cache to true, allow override via options.cache
|
|
324
|
+
const cacheEnabled = options.cache !== undefined ? options.cache : true;
|
|
325
|
+
const useCache = shouldCache(method, url, cacheEnabled);
|
|
300
326
|
|
|
301
327
|
if (useCache) {
|
|
302
328
|
// Use TanStack Query for cacheable requests
|
|
@@ -15,7 +15,6 @@ import {
|
|
|
15
15
|
import { toast } from 'react-toastify';
|
|
16
16
|
import { useDropzone } from 'react-dropzone';
|
|
17
17
|
import StandardModal from './generic/StandardModal';
|
|
18
|
-
import Select from './Select';
|
|
19
18
|
import CustomFetch from './Fetch';
|
|
20
19
|
import styles from './styles/ImportWizardModal.module.scss';
|
|
21
20
|
|
|
@@ -28,7 +27,8 @@ const ImportWizardModal = ({
|
|
|
28
27
|
title = "Import Data",
|
|
29
28
|
targetModel,
|
|
30
29
|
parentId = null,
|
|
31
|
-
relationKey = null
|
|
30
|
+
relationKey = null,
|
|
31
|
+
additionalParams = {}
|
|
32
32
|
}) => {
|
|
33
33
|
const [currentStep, setCurrentStep] = useState(1);
|
|
34
34
|
const [loading, setLoading] = useState(false);
|
|
@@ -107,21 +107,19 @@ const ImportWizardModal = ({
|
|
|
107
107
|
const formData = new FormData();
|
|
108
108
|
formData.append('file', file);
|
|
109
109
|
|
|
110
|
-
const response = await CustomFetch('/ajax/import/parse-file',
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
110
|
+
const response = await CustomFetch('/ajax/import/parse-file', 'POST', formData);
|
|
111
|
+
const result = response.data; // Extract data from response object
|
|
112
|
+
|
|
113
|
+
if (result.success) {
|
|
114
|
+
setFileData(result.data);
|
|
114
115
|
|
|
115
|
-
if (response.success) {
|
|
116
|
-
setFileData(response.data);
|
|
117
|
-
|
|
118
116
|
// Auto-suggest column mappings
|
|
119
|
-
await getSuggestedMappings(
|
|
120
|
-
|
|
117
|
+
await getSuggestedMappings(result.data.headers);
|
|
118
|
+
|
|
121
119
|
setCurrentStep(2);
|
|
122
|
-
toast.success(`File parsed successfully! Found ${
|
|
120
|
+
toast.success(`File parsed successfully! Found ${result.data.total_rows} rows.`);
|
|
123
121
|
} else {
|
|
124
|
-
toast.error(
|
|
122
|
+
toast.error(result.message || 'Failed to parse file');
|
|
125
123
|
}
|
|
126
124
|
} catch (error) {
|
|
127
125
|
console.error('File upload error:', error);
|
|
@@ -133,17 +131,15 @@ const ImportWizardModal = ({
|
|
|
133
131
|
|
|
134
132
|
async function getSuggestedMappings(headers) {
|
|
135
133
|
try {
|
|
136
|
-
const
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
if (response.success) {
|
|
146
|
-
setColumnMapping(response.suggestions);
|
|
134
|
+
const payload = {
|
|
135
|
+
headers: headers,
|
|
136
|
+
model_fields: modelConfig
|
|
137
|
+
};
|
|
138
|
+
const response = await CustomFetch('/ajax/import/column-suggestions', 'POST', payload);
|
|
139
|
+
const result = response.data;
|
|
140
|
+
|
|
141
|
+
if (result.success) {
|
|
142
|
+
setColumnMapping(result.suggestions);
|
|
147
143
|
}
|
|
148
144
|
} catch (error) {
|
|
149
145
|
console.error('Error getting suggestions:', error);
|
|
@@ -189,17 +185,15 @@ const ImportWizardModal = ({
|
|
|
189
185
|
formData.append('mapping', JSON.stringify(columnMapping));
|
|
190
186
|
formData.append('model_config', JSON.stringify(modelConfig));
|
|
191
187
|
|
|
192
|
-
const response = await CustomFetch('/ajax/import/preview-mapping',
|
|
193
|
-
|
|
194
|
-
body: formData,
|
|
195
|
-
});
|
|
188
|
+
const response = await CustomFetch('/ajax/import/preview-mapping', 'POST', formData);
|
|
189
|
+
const result = response.data;
|
|
196
190
|
|
|
197
|
-
if (
|
|
198
|
-
setPreviewData(
|
|
199
|
-
setValidationSummary(
|
|
191
|
+
if (result.success) {
|
|
192
|
+
setPreviewData(result.preview);
|
|
193
|
+
setValidationSummary(result.validation_summary);
|
|
200
194
|
setCurrentStep(3);
|
|
201
195
|
} else {
|
|
202
|
-
toast.error(
|
|
196
|
+
toast.error(result.message || 'Failed to preview data');
|
|
203
197
|
}
|
|
204
198
|
} catch (error) {
|
|
205
199
|
console.error('Preview error:', error);
|
|
@@ -211,27 +205,45 @@ const ImportWizardModal = ({
|
|
|
211
205
|
|
|
212
206
|
async function handleImport() {
|
|
213
207
|
setLoading(true);
|
|
214
|
-
|
|
208
|
+
|
|
215
209
|
try {
|
|
210
|
+
console.log('[ImportWizardModal] handleImport called with:', {
|
|
211
|
+
targetModel,
|
|
212
|
+
parentId,
|
|
213
|
+
relationKey,
|
|
214
|
+
additionalParams
|
|
215
|
+
});
|
|
216
|
+
|
|
216
217
|
const formData = new FormData();
|
|
217
218
|
formData.append('file', uploadedFile);
|
|
218
219
|
formData.append('mapping', JSON.stringify(columnMapping));
|
|
219
220
|
formData.append('model_config', JSON.stringify(modelConfig));
|
|
220
|
-
|
|
221
|
+
|
|
222
|
+
if (targetModel) formData.append('target_model', targetModel);
|
|
221
223
|
if (parentId) formData.append('parent_id', parentId.toString());
|
|
222
224
|
if (relationKey) formData.append('relation_key', relationKey);
|
|
223
225
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
226
|
+
// Append any additional parameters
|
|
227
|
+
Object.keys(additionalParams).forEach(key => {
|
|
228
|
+
const value = additionalParams[key];
|
|
229
|
+
console.log(`[ImportWizardModal] additionalParam ${key}:`, value);
|
|
230
|
+
// Only append if value is not null or undefined
|
|
231
|
+
if (value !== null && value !== undefined) {
|
|
232
|
+
formData.append(key, value);
|
|
233
|
+
}
|
|
227
234
|
});
|
|
228
235
|
|
|
229
|
-
if
|
|
230
|
-
|
|
236
|
+
// Use custom importUrl if provided, otherwise use default
|
|
237
|
+
const endpoint = importUrl || '/ajax/import/process';
|
|
238
|
+
const response = await CustomFetch(endpoint, 'POST', formData);
|
|
239
|
+
const result = response.data;
|
|
240
|
+
|
|
241
|
+
if (result.success) {
|
|
242
|
+
setImportResults(result);
|
|
231
243
|
setCurrentStep(4);
|
|
232
|
-
toast.success(`Import completed! ${
|
|
244
|
+
toast.success(`Import completed! ${result.imported} records imported successfully.`);
|
|
233
245
|
} else {
|
|
234
|
-
toast.error(
|
|
246
|
+
toast.error(result.message || 'Import failed');
|
|
235
247
|
}
|
|
236
248
|
} catch (error) {
|
|
237
249
|
console.error('Import error:', error);
|
|
@@ -358,19 +370,22 @@ const ImportWizardModal = ({
|
|
|
358
370
|
<ArrowRight size={20} className={styles.arrow} />
|
|
359
371
|
|
|
360
372
|
<div className={styles.fieldColumn}>
|
|
361
|
-
<
|
|
373
|
+
<select
|
|
374
|
+
className={styles.fieldSelect}
|
|
362
375
|
value={columnMapping[header] || ''}
|
|
363
|
-
onChange={(
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
376
|
+
onChange={(e) => handleMappingChange(header, e.target.value)}
|
|
377
|
+
>
|
|
378
|
+
<option value="">Skip Column</option>
|
|
379
|
+
{modelConfig.map(field => (
|
|
380
|
+
<option
|
|
381
|
+
key={field.id}
|
|
382
|
+
value={field.id}
|
|
383
|
+
disabled={getMappedFields().includes(field.id) && columnMapping[header] !== field.id}
|
|
384
|
+
>
|
|
385
|
+
{field.label}{field.required ? ' *' : ''}
|
|
386
|
+
</option>
|
|
387
|
+
))}
|
|
388
|
+
</select>
|
|
374
389
|
</div>
|
|
375
390
|
</div>
|
|
376
391
|
))}
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
Plus,
|
|
7
7
|
Search,
|
|
8
8
|
ArrowUpDown,
|
|
9
|
+
Upload,
|
|
9
10
|
} from 'lucide-react';
|
|
10
11
|
import Download from '../Download';
|
|
11
12
|
import styles from '../styles/DataGrid.module.scss';
|
|
@@ -19,9 +20,11 @@ const DataGridSearch = ({
|
|
|
19
20
|
dataRef,
|
|
20
21
|
filterValue,
|
|
21
22
|
paramValue,
|
|
22
|
-
// Sort dependencies
|
|
23
|
+
// Sort dependencies
|
|
23
24
|
pageData,
|
|
24
25
|
pageSetting,
|
|
26
|
+
// Bulk upload
|
|
27
|
+
onBulkUploadClick,
|
|
25
28
|
}) => {
|
|
26
29
|
const navigate = useNavigate();
|
|
27
30
|
|
|
@@ -96,6 +99,11 @@ const DataGridSearch = ({
|
|
|
96
99
|
? styles.actionButtons
|
|
97
100
|
: styles.actionButtonsStandalone;
|
|
98
101
|
|
|
102
|
+
// Debug log for bulk upload
|
|
103
|
+
console.log('[DataGridSearch] form.bulkUpload:', form.bulkUpload);
|
|
104
|
+
console.log('[DataGridSearch] form.bulkUpload?.enabled:', form.bulkUpload?.enabled);
|
|
105
|
+
console.log('[DataGridSearch] onBulkUploadClick:', onBulkUploadClick);
|
|
106
|
+
|
|
99
107
|
return (
|
|
100
108
|
<div className={buttonClassName}>
|
|
101
109
|
{form.export && form.export.url && (
|
|
@@ -118,6 +126,24 @@ const DataGridSearch = ({
|
|
|
118
126
|
Sort Order
|
|
119
127
|
</button>
|
|
120
128
|
)}
|
|
129
|
+
{form.bulkUpload && form.bulkUpload.enabled && (
|
|
130
|
+
<button
|
|
131
|
+
className={styles.btn}
|
|
132
|
+
onClick={(e) => {
|
|
133
|
+
e.preventDefault();
|
|
134
|
+
if (onBulkUploadClick) {
|
|
135
|
+
onBulkUploadClick();
|
|
136
|
+
}
|
|
137
|
+
}}
|
|
138
|
+
>
|
|
139
|
+
<Upload
|
|
140
|
+
strokeWidth={2}
|
|
141
|
+
size={16}
|
|
142
|
+
style={{ marginRight: '10px' }}
|
|
143
|
+
/>
|
|
144
|
+
Bulk Upload
|
|
145
|
+
</button>
|
|
146
|
+
)}
|
|
121
147
|
|
|
122
148
|
{(form.create &&
|
|
123
149
|
form.create.title &&
|
|
@@ -645,10 +645,16 @@ function GenericDashboard({ setting, userProfile, dynamicDashboard = false }) {
|
|
|
645
645
|
...widgetFilters,
|
|
646
646
|
};
|
|
647
647
|
|
|
648
|
+
// Check for cache option in widget.api (default: true)
|
|
649
|
+
const cacheOption = widget.api.cache !== undefined ? widget.api.cache : true;
|
|
650
|
+
|
|
648
651
|
return CustomFetch(
|
|
649
652
|
widget.api.url,
|
|
650
653
|
widget.api.method,
|
|
651
|
-
requestParams
|
|
654
|
+
requestParams,
|
|
655
|
+
null,
|
|
656
|
+
null,
|
|
657
|
+
{ cache: cacheOption }
|
|
652
658
|
);
|
|
653
659
|
}
|
|
654
660
|
return null;
|
|
@@ -3024,11 +3024,23 @@ function GenericDetail({
|
|
|
3024
3024
|
? processUrlParams(config.settings)
|
|
3025
3025
|
: config.settings;
|
|
3026
3026
|
|
|
3027
|
+
// Add bulkUpload from activeTabConfig to form if it exists
|
|
3028
|
+
const processedForm = activeTabConfig.bulkUpload
|
|
3029
|
+
? {
|
|
3030
|
+
...config.form,
|
|
3031
|
+
bulkUpload: activeTabConfig.bulkUpload
|
|
3032
|
+
}
|
|
3033
|
+
: config.form;
|
|
3034
|
+
|
|
3027
3035
|
const processedConfig = {
|
|
3028
3036
|
...config,
|
|
3029
3037
|
settings: processedSettings,
|
|
3038
|
+
form: processedForm,
|
|
3030
3039
|
};
|
|
3031
3040
|
|
|
3041
|
+
console.log('[GenericDetail] activeTabConfig.bulkUpload:', activeTabConfig.bulkUpload);
|
|
3042
|
+
console.log('[GenericDetail] processedConfig.form.bulkUpload:', processedConfig.form.bulkUpload);
|
|
3043
|
+
|
|
3032
3044
|
return (
|
|
3033
3045
|
<>
|
|
3034
3046
|
{activeTabConfig.dropzone &&
|
|
@@ -3222,6 +3234,7 @@ function GenericDetail({
|
|
|
3222
3234
|
?.style || {}
|
|
3223
3235
|
}
|
|
3224
3236
|
userProfile={userProfile}
|
|
3237
|
+
onDataReload={() => setDataReload(prev => prev + 1)}
|
|
3225
3238
|
/>
|
|
3226
3239
|
</div>
|
|
3227
3240
|
</div>
|
|
@@ -1595,6 +1595,10 @@ function GenericFormBuilder({ setting, urlParam, userProfile }) {
|
|
|
1595
1595
|
optionKey
|
|
1596
1596
|
].label =
|
|
1597
1597
|
e.target.value;
|
|
1598
|
+
newOptions[
|
|
1599
|
+
optionKey
|
|
1600
|
+
].id =
|
|
1601
|
+
`${slugify(e.target.value)}-`;
|
|
1598
1602
|
setDataField(
|
|
1599
1603
|
(
|
|
1600
1604
|
items
|
|
@@ -1732,6 +1736,10 @@ function GenericFormBuilder({ setting, urlParam, userProfile }) {
|
|
|
1732
1736
|
optionKey
|
|
1733
1737
|
].label =
|
|
1734
1738
|
e.target.value;
|
|
1739
|
+
newOptions[
|
|
1740
|
+
optionKey
|
|
1741
|
+
].id =
|
|
1742
|
+
`${slugify(e.target.value)}-`;
|
|
1735
1743
|
setDataField(
|
|
1736
1744
|
(
|
|
1737
1745
|
items
|
|
@@ -2093,6 +2101,10 @@ function GenericFormBuilder({ setting, urlParam, userProfile }) {
|
|
|
2093
2101
|
rowKey
|
|
2094
2102
|
].label =
|
|
2095
2103
|
e.target.value;
|
|
2104
|
+
newRows[
|
|
2105
|
+
rowKey
|
|
2106
|
+
].id =
|
|
2107
|
+
`${slugify(e.target.value)}-`;
|
|
2096
2108
|
setDataField(
|
|
2097
2109
|
(
|
|
2098
2110
|
items
|
|
@@ -11,36 +11,46 @@
|
|
|
11
11
|
display: flex;
|
|
12
12
|
align-items: center;
|
|
13
13
|
justify-content: center;
|
|
14
|
-
padding: 2rem
|
|
14
|
+
padding: 2.5rem 2rem;
|
|
15
15
|
border-bottom: 1px solid #e5e7eb;
|
|
16
16
|
margin-bottom: 2rem;
|
|
17
|
-
|
|
17
|
+
background: #f9fafb;
|
|
18
|
+
|
|
18
19
|
.stepContainer {
|
|
19
20
|
display: flex;
|
|
20
21
|
align-items: center;
|
|
21
22
|
position: relative;
|
|
22
|
-
|
|
23
|
+
|
|
23
24
|
&:not(:last-child) {
|
|
24
|
-
margin-right:
|
|
25
|
+
margin-right: 3rem;
|
|
25
26
|
}
|
|
26
27
|
}
|
|
27
|
-
|
|
28
|
+
|
|
28
29
|
.step {
|
|
29
|
-
width:
|
|
30
|
-
height:
|
|
30
|
+
width: 56px;
|
|
31
|
+
height: 56px;
|
|
32
|
+
min-width: 56px;
|
|
33
|
+
min-height: 56px;
|
|
31
34
|
border-radius: 50%;
|
|
32
|
-
border:
|
|
35
|
+
border: 3px solid #d1d5db;
|
|
33
36
|
display: flex;
|
|
34
37
|
align-items: center;
|
|
35
38
|
justify-content: center;
|
|
36
39
|
background: white;
|
|
37
40
|
color: #6b7280;
|
|
38
41
|
transition: all 0.3s ease;
|
|
39
|
-
|
|
42
|
+
flex-shrink: 0;
|
|
43
|
+
|
|
44
|
+
svg {
|
|
45
|
+
width: 24px;
|
|
46
|
+
height: 24px;
|
|
47
|
+
}
|
|
48
|
+
|
|
40
49
|
&.active {
|
|
41
|
-
border-color: #
|
|
42
|
-
background: #
|
|
50
|
+
border-color: #001f3f;
|
|
51
|
+
background: #001f3f;
|
|
43
52
|
color: white;
|
|
53
|
+
box-shadow: 0 4px 12px rgba(0, 31, 63, 0.2);
|
|
44
54
|
}
|
|
45
55
|
}
|
|
46
56
|
|
|
@@ -60,14 +70,15 @@
|
|
|
60
70
|
}
|
|
61
71
|
|
|
62
72
|
.stepConnector {
|
|
63
|
-
width:
|
|
64
|
-
height:
|
|
73
|
+
width: 60px;
|
|
74
|
+
height: 3px;
|
|
65
75
|
background: #d1d5db;
|
|
66
|
-
margin: 0
|
|
76
|
+
margin: 0 1.5rem;
|
|
67
77
|
transition: background-color 0.3s ease;
|
|
68
|
-
|
|
78
|
+
border-radius: 2px;
|
|
79
|
+
|
|
69
80
|
&.completed {
|
|
70
|
-
background: #
|
|
81
|
+
background: #001f3f;
|
|
71
82
|
}
|
|
72
83
|
}
|
|
73
84
|
}
|
|
@@ -75,28 +86,30 @@
|
|
|
75
86
|
.stepContent {
|
|
76
87
|
flex: 1;
|
|
77
88
|
overflow-y: auto;
|
|
78
|
-
padding: 0
|
|
89
|
+
padding: 0 2.5rem;
|
|
79
90
|
}
|
|
80
91
|
|
|
81
92
|
// Upload Step
|
|
82
93
|
.uploadStep {
|
|
83
94
|
.dropzone {
|
|
84
|
-
border:
|
|
85
|
-
border-radius:
|
|
86
|
-
padding: 3rem
|
|
95
|
+
border: 3px dashed #d1d5db;
|
|
96
|
+
border-radius: 16px;
|
|
97
|
+
padding: 4rem 3rem;
|
|
87
98
|
text-align: center;
|
|
88
99
|
cursor: pointer;
|
|
89
100
|
transition: all 0.3s ease;
|
|
90
101
|
background: #fafafa;
|
|
91
|
-
|
|
102
|
+
margin: 1rem 0;
|
|
103
|
+
|
|
92
104
|
&:hover, &.active {
|
|
93
|
-
border-color: #
|
|
94
|
-
background: #
|
|
105
|
+
border-color: #001f3f;
|
|
106
|
+
background: #f0f4f8;
|
|
107
|
+
box-shadow: 0 4px 12px rgba(0, 31, 63, 0.1);
|
|
95
108
|
}
|
|
96
|
-
|
|
109
|
+
|
|
97
110
|
.uploadIcon {
|
|
98
|
-
color: #
|
|
99
|
-
margin-bottom:
|
|
111
|
+
color: #001f3f;
|
|
112
|
+
margin-bottom: 1.5rem;
|
|
100
113
|
}
|
|
101
114
|
|
|
102
115
|
h3 {
|
|
@@ -115,21 +128,23 @@
|
|
|
115
128
|
display: flex;
|
|
116
129
|
align-items: center;
|
|
117
130
|
justify-content: center;
|
|
118
|
-
gap: 0.
|
|
119
|
-
margin-top:
|
|
120
|
-
padding:
|
|
121
|
-
background: #
|
|
122
|
-
border-radius:
|
|
123
|
-
color: #
|
|
131
|
+
gap: 0.75rem;
|
|
132
|
+
margin-top: 1.5rem;
|
|
133
|
+
padding: 1rem 1.5rem;
|
|
134
|
+
background: #e8f0f7;
|
|
135
|
+
border-radius: 10px;
|
|
136
|
+
color: #001f3f;
|
|
137
|
+
font-weight: 500;
|
|
124
138
|
}
|
|
125
139
|
}
|
|
126
|
-
|
|
140
|
+
|
|
127
141
|
.templateSection {
|
|
128
|
-
margin-top:
|
|
129
|
-
padding:
|
|
142
|
+
margin-top: 2.5rem;
|
|
143
|
+
padding: 2rem;
|
|
130
144
|
background: #f9fafb;
|
|
131
|
-
border-radius:
|
|
145
|
+
border-radius: 12px;
|
|
132
146
|
text-align: center;
|
|
147
|
+
border: 1px solid #e5e7eb;
|
|
133
148
|
|
|
134
149
|
h4 {
|
|
135
150
|
margin: 0 0 0.5rem 0;
|
|
@@ -145,18 +160,22 @@
|
|
|
145
160
|
.templateButton {
|
|
146
161
|
display: inline-flex;
|
|
147
162
|
align-items: center;
|
|
148
|
-
gap: 0.
|
|
149
|
-
padding:
|
|
150
|
-
background: #
|
|
163
|
+
gap: 0.75rem;
|
|
164
|
+
padding: 1rem 2rem;
|
|
165
|
+
background: #001f3f;
|
|
151
166
|
color: white;
|
|
152
167
|
border: none;
|
|
153
|
-
border-radius:
|
|
168
|
+
border-radius: 8px;
|
|
154
169
|
cursor: pointer;
|
|
155
|
-
font-size: 0.
|
|
156
|
-
|
|
157
|
-
|
|
170
|
+
font-size: 0.95rem;
|
|
171
|
+
font-weight: 500;
|
|
172
|
+
transition: all 0.2s ease;
|
|
173
|
+
box-shadow: 0 2px 8px rgba(0, 31, 63, 0.2);
|
|
174
|
+
|
|
158
175
|
&:hover {
|
|
159
|
-
background: #
|
|
176
|
+
background: #003366;
|
|
177
|
+
transform: translateY(-1px);
|
|
178
|
+
box-shadow: 0 4px 12px rgba(0, 31, 63, 0.3);
|
|
160
179
|
}
|
|
161
180
|
}
|
|
162
181
|
}
|
|
@@ -216,8 +235,30 @@
|
|
|
216
235
|
}
|
|
217
236
|
|
|
218
237
|
.fieldColumn {
|
|
219
|
-
|
|
238
|
+
.fieldSelect {
|
|
220
239
|
width: 100%;
|
|
240
|
+
padding: 0.75rem 1rem;
|
|
241
|
+
border: 2px solid #d1d5db;
|
|
242
|
+
border-radius: 6px;
|
|
243
|
+
font-size: 0.95rem;
|
|
244
|
+
color: #111827;
|
|
245
|
+
background-color: white;
|
|
246
|
+
cursor: pointer;
|
|
247
|
+
transition: all 0.2s;
|
|
248
|
+
|
|
249
|
+
&:hover {
|
|
250
|
+
border-color: #001f3f;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
&:focus {
|
|
254
|
+
outline: none;
|
|
255
|
+
border-color: #001f3f;
|
|
256
|
+
box-shadow: 0 0 0 3px rgba(0, 31, 63, 0.1);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
option:disabled {
|
|
260
|
+
color: #9ca3af;
|
|
261
|
+
}
|
|
221
262
|
}
|
|
222
263
|
}
|
|
223
264
|
}
|
|
@@ -530,9 +571,10 @@
|
|
|
530
571
|
display: flex;
|
|
531
572
|
justify-content: space-between;
|
|
532
573
|
align-items: center;
|
|
533
|
-
padding:
|
|
534
|
-
border-top:
|
|
574
|
+
padding: 2rem 2.5rem;
|
|
575
|
+
border-top: 2px solid #e5e7eb;
|
|
535
576
|
margin-top: 2rem;
|
|
577
|
+
background: #fafbfc;
|
|
536
578
|
|
|
537
579
|
.footerLeft, .footerRight {
|
|
538
580
|
display: flex;
|
|
@@ -542,39 +584,45 @@
|
|
|
542
584
|
.backButton, .nextButton, .finishButton {
|
|
543
585
|
display: inline-flex;
|
|
544
586
|
align-items: center;
|
|
545
|
-
gap: 0.
|
|
546
|
-
padding:
|
|
547
|
-
border-radius:
|
|
587
|
+
gap: 0.75rem;
|
|
588
|
+
padding: 1rem 2rem;
|
|
589
|
+
border-radius: 8px;
|
|
548
590
|
cursor: pointer;
|
|
549
591
|
font-weight: 500;
|
|
592
|
+
font-size: 0.95rem;
|
|
550
593
|
transition: all 0.2s ease;
|
|
551
594
|
border: none;
|
|
552
|
-
|
|
595
|
+
|
|
553
596
|
&:disabled {
|
|
554
|
-
opacity: 0.
|
|
597
|
+
opacity: 0.5;
|
|
555
598
|
cursor: not-allowed;
|
|
556
599
|
}
|
|
557
|
-
|
|
600
|
+
|
|
558
601
|
.spinner {
|
|
559
602
|
animation: spin 1s linear infinite;
|
|
560
603
|
}
|
|
561
604
|
}
|
|
562
|
-
|
|
605
|
+
|
|
563
606
|
.backButton {
|
|
564
607
|
background: #f3f4f6;
|
|
565
608
|
color: #374151;
|
|
566
|
-
|
|
609
|
+
border: 2px solid #e5e7eb;
|
|
610
|
+
|
|
567
611
|
&:hover:not(:disabled) {
|
|
568
612
|
background: #e5e7eb;
|
|
613
|
+
border-color: #d1d5db;
|
|
569
614
|
}
|
|
570
615
|
}
|
|
571
|
-
|
|
616
|
+
|
|
572
617
|
.nextButton, .finishButton {
|
|
573
|
-
background: #
|
|
618
|
+
background: #001f3f;
|
|
574
619
|
color: white;
|
|
575
|
-
|
|
620
|
+
box-shadow: 0 2px 8px rgba(0, 31, 63, 0.2);
|
|
621
|
+
|
|
576
622
|
&:hover:not(:disabled) {
|
|
577
|
-
background: #
|
|
623
|
+
background: #003366;
|
|
624
|
+
transform: translateY(-1px);
|
|
625
|
+
box-shadow: 0 4px 12px rgba(0, 31, 63, 0.3);
|
|
578
626
|
}
|
|
579
627
|
}
|
|
580
628
|
}
|