@visns-studio/visns-components 5.15.24 → 5.15.26
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 +58 -23
- package/src/components/Fetch.jsx +27 -7
- package/src/components/ImportWizardModal.jsx +60 -53
- package/src/components/controls/DataGridSearch.jsx +22 -1
- package/src/components/generic/GenericDetail.jsx +10 -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.26",
|
|
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,33 +254,13 @@ 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
|
-
/**
|
|
262
|
-
* DEBUG: Settings Configuration Tracking
|
|
263
|
-
* Monitor changes to settings prop to identify config update issues
|
|
264
|
-
*/
|
|
265
|
-
useEffect(() => {
|
|
266
|
-
console.log('🔍 DataGrid settings prop changed:', {
|
|
267
|
-
settings: settings,
|
|
268
|
-
settingsLength: settings?.length || 0,
|
|
269
|
-
hasUpdateSetting:
|
|
270
|
-
settings?.some((s) => s.id === 'update') || false,
|
|
271
|
-
settingsIds: settings?.map((s) => s.id) || [],
|
|
272
|
-
timestamp: new Date().toISOString(),
|
|
273
|
-
});
|
|
274
|
-
}, [settings]);
|
|
275
|
-
|
|
276
|
-
useEffect(() => {
|
|
277
|
-
console.log('🔍 DataGrid form prop changed:', {
|
|
278
|
-
form: form,
|
|
279
|
-
primaryKey: form?.primaryKey,
|
|
280
|
-
timestamp: new Date().toISOString(),
|
|
281
|
-
});
|
|
282
|
-
}, [form]);
|
|
283
264
|
|
|
284
265
|
/** Audio */
|
|
285
266
|
const [audioSource, setAudioSource] = useState('');
|
|
@@ -314,6 +295,9 @@ const DataGrid = forwardRef(
|
|
|
314
295
|
const [bulkEditMode, setBulkEditMode] = useState(false);
|
|
315
296
|
const [bulkEditGroupData, setBulkEditGroupData] = useState(null);
|
|
316
297
|
|
|
298
|
+
/** Import Modal States */
|
|
299
|
+
const [showImportModal, setShowImportModal] = useState(false);
|
|
300
|
+
|
|
317
301
|
/** Modal Functions */
|
|
318
302
|
const modalOpen = (formType, formId) => {
|
|
319
303
|
setModalShow(true);
|
|
@@ -327,6 +311,40 @@ const DataGrid = forwardRef(
|
|
|
327
311
|
setBulkEditGroupData(null);
|
|
328
312
|
};
|
|
329
313
|
|
|
314
|
+
/** Import Modal Functions */
|
|
315
|
+
const handleBulkUploadClick = () => {
|
|
316
|
+
console.log('[DataGrid] Opening import modal with:', {
|
|
317
|
+
paramValue,
|
|
318
|
+
pageData,
|
|
319
|
+
routeParams,
|
|
320
|
+
'pageData?.id': pageData?.id,
|
|
321
|
+
'routeParams.dataId': routeParams.dataId,
|
|
322
|
+
'form.bulkUpload': form.bulkUpload
|
|
323
|
+
});
|
|
324
|
+
setShowImportModal(true);
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
const handleImportComplete = () => {
|
|
328
|
+
setShowImportModal(false);
|
|
329
|
+
|
|
330
|
+
// Trigger data refresh using the proper grid reload method
|
|
331
|
+
if (gridRef.current && gridRef.current.paginationProps) {
|
|
332
|
+
gridRef.current.paginationProps.reload();
|
|
333
|
+
|
|
334
|
+
// Force grid to recalculate row heights after reload
|
|
335
|
+
setTimeout(() => {
|
|
336
|
+
if (gridRef.current?.api) {
|
|
337
|
+
gridRef.current.api.forceUpdate();
|
|
338
|
+
}
|
|
339
|
+
}, 50);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// Also trigger parent page data reload if callback provided
|
|
343
|
+
if (onDataReload) {
|
|
344
|
+
onDataReload();
|
|
345
|
+
}
|
|
346
|
+
};
|
|
347
|
+
|
|
330
348
|
useEffect(() => {
|
|
331
349
|
setFormData(form);
|
|
332
350
|
}, [form]);
|
|
@@ -3433,6 +3451,7 @@ const DataGrid = forwardRef(
|
|
|
3433
3451
|
paramValue={paramValue}
|
|
3434
3452
|
pageData={pageData}
|
|
3435
3453
|
pageSetting={pageSetting}
|
|
3454
|
+
onBulkUploadClick={handleBulkUploadClick}
|
|
3436
3455
|
/>
|
|
3437
3456
|
</div>
|
|
3438
3457
|
<div className={styles.dataGridTopRightContainer}>
|
|
@@ -4100,6 +4119,22 @@ const DataGrid = forwardRef(
|
|
|
4100
4119
|
</div>
|
|
4101
4120
|
</div>
|
|
4102
4121
|
)}
|
|
4122
|
+
|
|
4123
|
+
{/* Import Wizard Modal */}
|
|
4124
|
+
{form.bulkUpload && form.bulkUpload.enabled && (
|
|
4125
|
+
<ImportWizardModal
|
|
4126
|
+
isOpen={showImportModal}
|
|
4127
|
+
onClose={() => setShowImportModal(false)}
|
|
4128
|
+
modelConfig={form.bulkUpload.modelConfig}
|
|
4129
|
+
title={form.bulkUpload.title || 'Import Data'}
|
|
4130
|
+
importUrl={form.bulkUpload.importUrl}
|
|
4131
|
+
onComplete={handleImportComplete}
|
|
4132
|
+
additionalParams={{
|
|
4133
|
+
visit_request_id: paramValue || routeParams.dataId || pageData?.id,
|
|
4134
|
+
...form.bulkUpload.additionalParams
|
|
4135
|
+
}}
|
|
4136
|
+
/>
|
|
4137
|
+
)}
|
|
4103
4138
|
</>
|
|
4104
4139
|
);
|
|
4105
4140
|
}
|
package/src/components/Fetch.jsx
CHANGED
|
@@ -133,11 +133,20 @@ const originalCustomFetch = (
|
|
|
133
133
|
errorCallback = null
|
|
134
134
|
) => {
|
|
135
135
|
const baseUrl = '';
|
|
136
|
+
|
|
137
|
+
// Check if formData is FormData (for file uploads)
|
|
138
|
+
const isFormData = formData instanceof FormData;
|
|
139
|
+
|
|
136
140
|
const headers = {
|
|
137
|
-
'Content-Type': 'application/json',
|
|
138
141
|
'X-Requested-With': 'XMLHttpRequest',
|
|
139
142
|
};
|
|
140
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
|
+
|
|
141
150
|
// Add CSRF token if available
|
|
142
151
|
const csrfToken = document
|
|
143
152
|
.querySelector('meta[name="csrf-token"]')
|
|
@@ -158,7 +167,7 @@ const originalCustomFetch = (
|
|
|
158
167
|
const cookiesSize = cookies ? cookies.length : 0;
|
|
159
168
|
|
|
160
169
|
// Check payload size
|
|
161
|
-
const payloadSize = new Blob([JSON.stringify(formData)]).size;
|
|
170
|
+
const payloadSize = isFormData ? 0 : new Blob([JSON.stringify(formData)]).size; // Skip size check for FormData
|
|
162
171
|
const totalRequestSize = payloadSize + headersSize + cookiesSize;
|
|
163
172
|
|
|
164
173
|
if (totalRequestSize > MAX_PAYLOAD_SIZE) {
|
|
@@ -172,11 +181,18 @@ const originalCustomFetch = (
|
|
|
172
181
|
return Promise.reject(new Error(errorMessage));
|
|
173
182
|
}
|
|
174
183
|
|
|
184
|
+
// Handle PUT/PATCH method workaround for Laravel
|
|
175
185
|
if (method.toUpperCase() === 'PUT' || method.toUpperCase() === 'PATCH') {
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
_method
|
|
179
|
-
}
|
|
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
|
+
}
|
|
180
196
|
}
|
|
181
197
|
|
|
182
198
|
// Configure fetch options
|
|
@@ -202,7 +218,11 @@ const originalCustomFetch = (
|
|
|
202
218
|
}
|
|
203
219
|
} else {
|
|
204
220
|
// For other methods, add body
|
|
205
|
-
|
|
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
|
+
}
|
|
206
226
|
}
|
|
207
227
|
|
|
208
228
|
return new Promise((resolve, reject) => {
|
|
@@ -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,37 @@ const ImportWizardModal = ({
|
|
|
211
205
|
|
|
212
206
|
async function handleImport() {
|
|
213
207
|
setLoading(true);
|
|
214
|
-
|
|
208
|
+
|
|
215
209
|
try {
|
|
216
210
|
const formData = new FormData();
|
|
217
211
|
formData.append('file', uploadedFile);
|
|
218
212
|
formData.append('mapping', JSON.stringify(columnMapping));
|
|
219
213
|
formData.append('model_config', JSON.stringify(modelConfig));
|
|
220
|
-
|
|
214
|
+
|
|
215
|
+
if (targetModel) formData.append('target_model', targetModel);
|
|
221
216
|
if (parentId) formData.append('parent_id', parentId.toString());
|
|
222
217
|
if (relationKey) formData.append('relation_key', relationKey);
|
|
223
218
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
219
|
+
// Append any additional parameters
|
|
220
|
+
Object.keys(additionalParams).forEach(key => {
|
|
221
|
+
const value = additionalParams[key];
|
|
222
|
+
// Only append if value is not null or undefined
|
|
223
|
+
if (value !== null && value !== undefined) {
|
|
224
|
+
formData.append(key, value);
|
|
225
|
+
}
|
|
227
226
|
});
|
|
228
227
|
|
|
229
|
-
if
|
|
230
|
-
|
|
228
|
+
// Use custom importUrl if provided, otherwise use default
|
|
229
|
+
const endpoint = importUrl || '/ajax/import/process';
|
|
230
|
+
const response = await CustomFetch(endpoint, 'POST', formData);
|
|
231
|
+
const result = response.data;
|
|
232
|
+
|
|
233
|
+
if (result.success) {
|
|
234
|
+
setImportResults(result);
|
|
231
235
|
setCurrentStep(4);
|
|
232
|
-
toast.success(`Import completed! ${
|
|
236
|
+
toast.success(`Import completed! ${result.imported} records imported successfully.`);
|
|
233
237
|
} else {
|
|
234
|
-
toast.error(
|
|
238
|
+
toast.error(result.message || 'Import failed');
|
|
235
239
|
}
|
|
236
240
|
} catch (error) {
|
|
237
241
|
console.error('Import error:', error);
|
|
@@ -358,19 +362,22 @@ const ImportWizardModal = ({
|
|
|
358
362
|
<ArrowRight size={20} className={styles.arrow} />
|
|
359
363
|
|
|
360
364
|
<div className={styles.fieldColumn}>
|
|
361
|
-
<
|
|
365
|
+
<select
|
|
366
|
+
className={styles.fieldSelect}
|
|
362
367
|
value={columnMapping[header] || ''}
|
|
363
|
-
onChange={(
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
368
|
+
onChange={(e) => handleMappingChange(header, e.target.value)}
|
|
369
|
+
>
|
|
370
|
+
<option value="">Skip Column</option>
|
|
371
|
+
{modelConfig.map(field => (
|
|
372
|
+
<option
|
|
373
|
+
key={field.id}
|
|
374
|
+
value={field.id}
|
|
375
|
+
disabled={getMappedFields().includes(field.id) && columnMapping[header] !== field.id}
|
|
376
|
+
>
|
|
377
|
+
{field.label}{field.required ? ' *' : ''}
|
|
378
|
+
</option>
|
|
379
|
+
))}
|
|
380
|
+
</select>
|
|
374
381
|
</div>
|
|
375
382
|
</div>
|
|
376
383
|
))}
|
|
@@ -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
|
|
|
@@ -118,6 +121,24 @@ const DataGridSearch = ({
|
|
|
118
121
|
Sort Order
|
|
119
122
|
</button>
|
|
120
123
|
)}
|
|
124
|
+
{form.bulkUpload && form.bulkUpload.enabled && (
|
|
125
|
+
<button
|
|
126
|
+
className={styles.btn}
|
|
127
|
+
onClick={(e) => {
|
|
128
|
+
e.preventDefault();
|
|
129
|
+
if (onBulkUploadClick) {
|
|
130
|
+
onBulkUploadClick();
|
|
131
|
+
}
|
|
132
|
+
}}
|
|
133
|
+
>
|
|
134
|
+
<Upload
|
|
135
|
+
strokeWidth={2}
|
|
136
|
+
size={16}
|
|
137
|
+
style={{ marginRight: '10px' }}
|
|
138
|
+
/>
|
|
139
|
+
Bulk Upload
|
|
140
|
+
</button>
|
|
141
|
+
)}
|
|
121
142
|
|
|
122
143
|
{(form.create &&
|
|
123
144
|
form.create.title &&
|
|
@@ -3024,9 +3024,18 @@ 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
|
|
|
3032
3041
|
return (
|
|
@@ -3222,6 +3231,7 @@ function GenericDetail({
|
|
|
3222
3231
|
?.style || {}
|
|
3223
3232
|
}
|
|
3224
3233
|
userProfile={userProfile}
|
|
3234
|
+
onDataReload={() => setDataReload(prev => prev + 1)}
|
|
3225
3235
|
/>
|
|
3226
3236
|
</div>
|
|
3227
3237
|
</div>
|
|
@@ -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
|
}
|