@visns-studio/visns-components 5.15.21 → 5.15.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md
CHANGED
|
@@ -69,7 +69,7 @@ VISNS Components is a React-based UI component library that provides a set of re
|
|
|
69
69
|
|
|
70
70
|
### Utility Components
|
|
71
71
|
|
|
72
|
-
- **Fetch** -
|
|
72
|
+
- **Fetch** - Advanced data fetching with intelligent caching, error handling, and TanStack Query integration
|
|
73
73
|
- **Download** - File download functionality
|
|
74
74
|
- **Loader** - Loading indicators
|
|
75
75
|
- **Notification** - Toast notifications
|
|
@@ -2489,18 +2489,20 @@ const BulkCardProcessor = () => {
|
|
|
2489
2489
|
|
|
2490
2490
|
### Utility Components
|
|
2491
2491
|
|
|
2492
|
-
#### CustomFetch
|
|
2492
|
+
#### CustomFetch (Fetch.jsx)
|
|
2493
2493
|
|
|
2494
|
-
|
|
2494
|
+
Advanced data fetching utility with intelligent caching, error handling, and payload validation powered by TanStack Query (v5.15.21+).
|
|
2495
2495
|
|
|
2496
2496
|
```jsx
|
|
2497
|
-
|
|
2497
|
+
import CustomFetch from '@visns-studio/visns-components';
|
|
2498
|
+
|
|
2499
|
+
// Basic usage - async/await
|
|
2498
2500
|
const result = await CustomFetch('/api/data', 'GET', { param1: 'value1' });
|
|
2499
2501
|
|
|
2500
2502
|
// Using callbacks
|
|
2501
2503
|
CustomFetch(
|
|
2502
2504
|
'/api/data',
|
|
2503
|
-
'
|
|
2505
|
+
'POST',
|
|
2504
2506
|
{ param1: 'value1' },
|
|
2505
2507
|
(result) => {
|
|
2506
2508
|
// Success callback
|
|
@@ -2511,16 +2513,71 @@ CustomFetch(
|
|
|
2511
2513
|
console.error(error);
|
|
2512
2514
|
}
|
|
2513
2515
|
);
|
|
2516
|
+
|
|
2517
|
+
// Manual cache management
|
|
2518
|
+
CustomFetch.invalidateCache('/api/data'); // Invalidate specific pattern
|
|
2519
|
+
CustomFetch.clearCache(); // Clear all cache
|
|
2514
2520
|
```
|
|
2515
2521
|
|
|
2516
|
-
|
|
2522
|
+
**Core Features:**
|
|
2517
2523
|
|
|
2518
|
-
- **
|
|
2519
|
-
- **
|
|
2524
|
+
- **Intelligent Caching**: Selective caching with TanStack Query for optimal performance
|
|
2525
|
+
- **Payload Size Validation**: Automatically validates request size (default: 4.5MB, configurable via `VITE_MAX_PAYLOAD_SIZE_MB`)
|
|
2526
|
+
- **Toast Notifications**: Error messages displayed using react-toastify
|
|
2520
2527
|
- **Promise Tracking**: Integrates with react-promise-tracker for loading indicators
|
|
2521
2528
|
- **HTML Error Parsing**: Properly renders HTML content in error messages
|
|
2522
|
-
- **CSRF Protection**:
|
|
2523
|
-
- **Authentication Handling**:
|
|
2529
|
+
- **CSRF Protection**: Automatic token handling and redirect on mismatch
|
|
2530
|
+
- **Authentication Handling**: Silent handling of unauthenticated errors
|
|
2531
|
+
|
|
2532
|
+
**Caching Strategy (v5.15.21+):**
|
|
2533
|
+
|
|
2534
|
+
The Fetch component implements an intelligent caching strategy designed to balance performance with data freshness:
|
|
2535
|
+
|
|
2536
|
+
- **GET Requests**: NOT cached by default to ensure data freshness
|
|
2537
|
+
- **Selective POST Caching**: Only specific safe endpoints are cached:
|
|
2538
|
+
- `/dropdown` - Reference data (10 min stale time) - Industries, facilities, user lists
|
|
2539
|
+
- `/table` - Table queries (1 min stale time) - Grid data with stable results
|
|
2540
|
+
- `/availabilities` - Report availability (2 min stale time) - Report metadata
|
|
2541
|
+
- `/reports/` - Report data (2 min stale time) - Generated report results
|
|
2542
|
+
- `/dashboard/` - Dashboard metrics (5 min stale time) - Aggregated metrics
|
|
2543
|
+
- Wharf usage endpoints (30 sec stale time) - Real-time usage data
|
|
2544
|
+
|
|
2545
|
+
- **Auto-invalidation**: Mutations (POST/PUT/PATCH/DELETE) automatically invalidate related caches:
|
|
2546
|
+
- Lead mutations → invalidate lead tables and dashboards
|
|
2547
|
+
- Client mutations → invalidate client dropdowns and tables
|
|
2548
|
+
- Agreement mutations → invalidate agreement tables and reports
|
|
2549
|
+
- Facility mutations → invalidate availability and report caches
|
|
2550
|
+
- Any data mutation → invalidate related report caches
|
|
2551
|
+
|
|
2552
|
+
- **Cache Configuration**:
|
|
2553
|
+
- **Cache Time**: 10 minutes - data stays in memory
|
|
2554
|
+
- **Stale Time**: Varies by endpoint type (30 seconds to 10 minutes)
|
|
2555
|
+
- **Retry Policy**: Conservative 1 retry on failure
|
|
2556
|
+
- **Window Focus**: No automatic refetch on focus
|
|
2557
|
+
- **Network Reconnect**: Automatic refetch on reconnection
|
|
2558
|
+
|
|
2559
|
+
**Advanced Cache Control:**
|
|
2560
|
+
|
|
2561
|
+
```jsx
|
|
2562
|
+
// Invalidate specific cache patterns
|
|
2563
|
+
CustomFetch.invalidateCache('/clients/dropdown'); // String pattern
|
|
2564
|
+
CustomFetch.invalidateCache(['clients', 'table']); // Array key
|
|
2565
|
+
|
|
2566
|
+
// Clear entire cache
|
|
2567
|
+
CustomFetch.clearCache();
|
|
2568
|
+
|
|
2569
|
+
// Access QueryClient for advanced operations
|
|
2570
|
+
const queryClient = CustomFetch.getQueryClient();
|
|
2571
|
+
queryClient.invalidateQueries({ queryKey: ['specific', 'key'] });
|
|
2572
|
+
```
|
|
2573
|
+
|
|
2574
|
+
**Implementation Details:**
|
|
2575
|
+
|
|
2576
|
+
- **Singleton QueryClient**: Single instance shared across entire application
|
|
2577
|
+
- **Cache Key Generation**: Consistent keys based on URL, method, and parameters
|
|
2578
|
+
- **Smart Invalidation**: Related caches automatically cleared on mutations
|
|
2579
|
+
- **Error Preservation**: Original error handling callbacks maintained
|
|
2580
|
+
- **Backward Compatibility**: All existing CustomFetch usage continues to work unchanged
|
|
2524
2581
|
|
|
2525
2582
|
#### CameraPlacement
|
|
2526
2583
|
Interactive camera placement and visualization tool for security system design. Features a comprehensive Verkada camera database, FOV triangle visualization, and professional export capabilities with enhanced UI consistency.
|
package/package.json
CHANGED
|
@@ -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.22",
|
|
98
98
|
"description": "Various packages to assist in the development of our Custom Applications.",
|
|
99
99
|
"main": "src/index.js",
|
|
100
100
|
"files": [
|
package/src/components/Fetch.jsx
CHANGED
|
@@ -75,7 +75,7 @@ const shouldCache = (method, url) => {
|
|
|
75
75
|
|
|
76
76
|
// Dynamic stale time based on endpoint type
|
|
77
77
|
const getStaleTime = (url) => {
|
|
78
|
-
if (url.includes('/dropdown')) return
|
|
78
|
+
if (url.includes('/dropdown')) return 1 * 60 * 1000; // 1 minute for dropdown data
|
|
79
79
|
if (url.includes('/dashboard/')) return 5 * 60 * 1000; // 5 minutes for dashboard data
|
|
80
80
|
if (url.includes('/availabilities') || url.includes('/reports/')) return 2 * 60 * 1000; // 2 minutes for reports
|
|
81
81
|
if (url.includes('/table')) return 1 * 60 * 1000; // 1 minute for table data
|
|
@@ -0,0 +1,627 @@
|
|
|
1
|
+
import React, { useState, useEffect } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
X,
|
|
4
|
+
Upload,
|
|
5
|
+
ArrowRight,
|
|
6
|
+
ArrowLeft,
|
|
7
|
+
CheckCircle,
|
|
8
|
+
AlertTriangle,
|
|
9
|
+
FileText,
|
|
10
|
+
Eye,
|
|
11
|
+
Download,
|
|
12
|
+
RefreshCw,
|
|
13
|
+
Zap
|
|
14
|
+
} from 'lucide-react';
|
|
15
|
+
import { toast } from 'react-toastify';
|
|
16
|
+
import { useDropzone } from 'react-dropzone';
|
|
17
|
+
import StandardModal from './generic/StandardModal';
|
|
18
|
+
import Select from './Select';
|
|
19
|
+
import CustomFetch from './Fetch';
|
|
20
|
+
import styles from './styles/ImportWizardModal.module.scss';
|
|
21
|
+
|
|
22
|
+
const ImportWizardModal = ({
|
|
23
|
+
isOpen,
|
|
24
|
+
onClose,
|
|
25
|
+
modelConfig = [],
|
|
26
|
+
importUrl,
|
|
27
|
+
onComplete,
|
|
28
|
+
title = "Import Data",
|
|
29
|
+
targetModel,
|
|
30
|
+
parentId = null,
|
|
31
|
+
relationKey = null
|
|
32
|
+
}) => {
|
|
33
|
+
const [currentStep, setCurrentStep] = useState(1);
|
|
34
|
+
const [loading, setLoading] = useState(false);
|
|
35
|
+
const [uploadedFile, setUploadedFile] = useState(null);
|
|
36
|
+
const [fileData, setFileData] = useState(null);
|
|
37
|
+
const [columnMapping, setColumnMapping] = useState({});
|
|
38
|
+
const [previewData, setPreviewData] = useState([]);
|
|
39
|
+
const [validationSummary, setValidationSummary] = useState(null);
|
|
40
|
+
const [importResults, setImportResults] = useState(null);
|
|
41
|
+
|
|
42
|
+
const steps = [
|
|
43
|
+
{
|
|
44
|
+
id: 1,
|
|
45
|
+
title: 'Upload File',
|
|
46
|
+
description: 'Upload your CSV or Excel file',
|
|
47
|
+
icon: <Upload size={20} />
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
id: 2,
|
|
51
|
+
title: 'Map Columns',
|
|
52
|
+
description: 'Map file columns to data fields',
|
|
53
|
+
icon: <ArrowRight size={20} />
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
id: 3,
|
|
57
|
+
title: 'Preview Data',
|
|
58
|
+
description: 'Review mapped data before import',
|
|
59
|
+
icon: <Eye size={20} />
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
id: 4,
|
|
63
|
+
title: 'Import',
|
|
64
|
+
description: 'Import data to system',
|
|
65
|
+
icon: <Zap size={20} />
|
|
66
|
+
}
|
|
67
|
+
];
|
|
68
|
+
|
|
69
|
+
// Reset state when modal opens/closes
|
|
70
|
+
useEffect(() => {
|
|
71
|
+
if (isOpen) {
|
|
72
|
+
resetWizard();
|
|
73
|
+
}
|
|
74
|
+
}, [isOpen]);
|
|
75
|
+
|
|
76
|
+
const resetWizard = () => {
|
|
77
|
+
setCurrentStep(1);
|
|
78
|
+
setUploadedFile(null);
|
|
79
|
+
setFileData(null);
|
|
80
|
+
setColumnMapping({});
|
|
81
|
+
setPreviewData([]);
|
|
82
|
+
setValidationSummary(null);
|
|
83
|
+
setImportResults(null);
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
// File upload dropzone configuration
|
|
87
|
+
const { getRootProps, getInputProps, isDragActive } = useDropzone({
|
|
88
|
+
accept: {
|
|
89
|
+
'text/csv': ['.csv'],
|
|
90
|
+
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': ['.xlsx'],
|
|
91
|
+
'application/vnd.ms-excel': ['.xls']
|
|
92
|
+
},
|
|
93
|
+
maxFiles: 1,
|
|
94
|
+
maxSize: 10 * 1024 * 1024, // 10MB
|
|
95
|
+
onDrop: handleFileUpload,
|
|
96
|
+
disabled: loading
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
async function handleFileUpload(acceptedFiles) {
|
|
100
|
+
if (acceptedFiles.length === 0) return;
|
|
101
|
+
|
|
102
|
+
const file = acceptedFiles[0];
|
|
103
|
+
setUploadedFile(file);
|
|
104
|
+
setLoading(true);
|
|
105
|
+
|
|
106
|
+
try {
|
|
107
|
+
const formData = new FormData();
|
|
108
|
+
formData.append('file', file);
|
|
109
|
+
|
|
110
|
+
const response = await CustomFetch('/ajax/import/parse-file', {
|
|
111
|
+
method: 'POST',
|
|
112
|
+
body: formData,
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
if (response.success) {
|
|
116
|
+
setFileData(response.data);
|
|
117
|
+
|
|
118
|
+
// Auto-suggest column mappings
|
|
119
|
+
await getSuggestedMappings(response.data.headers);
|
|
120
|
+
|
|
121
|
+
setCurrentStep(2);
|
|
122
|
+
toast.success(`File parsed successfully! Found ${response.data.total_rows} rows.`);
|
|
123
|
+
} else {
|
|
124
|
+
toast.error(response.message || 'Failed to parse file');
|
|
125
|
+
}
|
|
126
|
+
} catch (error) {
|
|
127
|
+
console.error('File upload error:', error);
|
|
128
|
+
toast.error('Error uploading file');
|
|
129
|
+
} finally {
|
|
130
|
+
setLoading(false);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async function getSuggestedMappings(headers) {
|
|
135
|
+
try {
|
|
136
|
+
const response = await CustomFetch('/ajax/import/column-suggestions', {
|
|
137
|
+
method: 'POST',
|
|
138
|
+
headers: { 'Content-Type': 'application/json' },
|
|
139
|
+
body: JSON.stringify({
|
|
140
|
+
headers: headers,
|
|
141
|
+
model_fields: modelConfig
|
|
142
|
+
})
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
if (response.success) {
|
|
146
|
+
setColumnMapping(response.suggestions);
|
|
147
|
+
}
|
|
148
|
+
} catch (error) {
|
|
149
|
+
console.error('Error getting suggestions:', error);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const handleMappingChange = (fileColumn, modelField) => {
|
|
154
|
+
setColumnMapping(prev => ({
|
|
155
|
+
...prev,
|
|
156
|
+
[fileColumn]: modelField
|
|
157
|
+
}));
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
const getRequiredFields = () => {
|
|
161
|
+
return modelConfig.filter(field => field.required).map(field => field.id);
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
const getMappedFields = () => {
|
|
165
|
+
return Object.values(columnMapping).filter(field => field);
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
const getUnmappedRequiredFields = () => {
|
|
169
|
+
const requiredFields = getRequiredFields();
|
|
170
|
+
const mappedFields = getMappedFields();
|
|
171
|
+
return requiredFields.filter(field => !mappedFields.includes(field));
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
const canProceedToPreview = () => {
|
|
175
|
+
return getUnmappedRequiredFields().length === 0;
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
async function handlePreviewMapping() {
|
|
179
|
+
if (!canProceedToPreview()) {
|
|
180
|
+
toast.error('Please map all required fields before proceeding');
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
setLoading(true);
|
|
185
|
+
|
|
186
|
+
try {
|
|
187
|
+
const formData = new FormData();
|
|
188
|
+
formData.append('file', uploadedFile);
|
|
189
|
+
formData.append('mapping', JSON.stringify(columnMapping));
|
|
190
|
+
formData.append('model_config', JSON.stringify(modelConfig));
|
|
191
|
+
|
|
192
|
+
const response = await CustomFetch('/ajax/import/preview-mapping', {
|
|
193
|
+
method: 'POST',
|
|
194
|
+
body: formData,
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
if (response.success) {
|
|
198
|
+
setPreviewData(response.preview);
|
|
199
|
+
setValidationSummary(response.validation_summary);
|
|
200
|
+
setCurrentStep(3);
|
|
201
|
+
} else {
|
|
202
|
+
toast.error(response.message || 'Failed to preview data');
|
|
203
|
+
}
|
|
204
|
+
} catch (error) {
|
|
205
|
+
console.error('Preview error:', error);
|
|
206
|
+
toast.error('Error previewing data');
|
|
207
|
+
} finally {
|
|
208
|
+
setLoading(false);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
async function handleImport() {
|
|
213
|
+
setLoading(true);
|
|
214
|
+
|
|
215
|
+
try {
|
|
216
|
+
const formData = new FormData();
|
|
217
|
+
formData.append('file', uploadedFile);
|
|
218
|
+
formData.append('mapping', JSON.stringify(columnMapping));
|
|
219
|
+
formData.append('model_config', JSON.stringify(modelConfig));
|
|
220
|
+
formData.append('target_model', targetModel);
|
|
221
|
+
if (parentId) formData.append('parent_id', parentId.toString());
|
|
222
|
+
if (relationKey) formData.append('relation_key', relationKey);
|
|
223
|
+
|
|
224
|
+
const response = await CustomFetch('/ajax/import/process', {
|
|
225
|
+
method: 'POST',
|
|
226
|
+
body: formData,
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
if (response.success) {
|
|
230
|
+
setImportResults(response);
|
|
231
|
+
setCurrentStep(4);
|
|
232
|
+
toast.success(`Import completed! ${response.imported} records imported successfully.`);
|
|
233
|
+
} else {
|
|
234
|
+
toast.error(response.message || 'Import failed');
|
|
235
|
+
}
|
|
236
|
+
} catch (error) {
|
|
237
|
+
console.error('Import error:', error);
|
|
238
|
+
toast.error('Error during import');
|
|
239
|
+
} finally {
|
|
240
|
+
setLoading(false);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const handleClose = () => {
|
|
245
|
+
if (importResults && importResults.imported > 0 && onComplete) {
|
|
246
|
+
onComplete();
|
|
247
|
+
}
|
|
248
|
+
onClose();
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
const canGoNext = () => {
|
|
252
|
+
switch (currentStep) {
|
|
253
|
+
case 1: return fileData !== null;
|
|
254
|
+
case 2: return canProceedToPreview();
|
|
255
|
+
case 3: return validationSummary && validationSummary.valid_rows > 0;
|
|
256
|
+
default: return false;
|
|
257
|
+
}
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
const handleNext = () => {
|
|
261
|
+
switch (currentStep) {
|
|
262
|
+
case 2:
|
|
263
|
+
handlePreviewMapping();
|
|
264
|
+
break;
|
|
265
|
+
case 3:
|
|
266
|
+
handleImport();
|
|
267
|
+
break;
|
|
268
|
+
default:
|
|
269
|
+
setCurrentStep(prev => prev + 1);
|
|
270
|
+
}
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
const handleBack = () => {
|
|
274
|
+
setCurrentStep(prev => Math.max(1, prev - 1));
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
const renderStepIndicator = () => (
|
|
278
|
+
<div className={styles.stepIndicator}>
|
|
279
|
+
{steps.map((step, index) => (
|
|
280
|
+
<div key={step.id} className={styles.stepContainer}>
|
|
281
|
+
<div className={`${styles.step} ${currentStep >= step.id ? styles.active : ''}`}>
|
|
282
|
+
{step.icon}
|
|
283
|
+
</div>
|
|
284
|
+
<div className={styles.stepInfo}>
|
|
285
|
+
<div className={styles.stepTitle}>{step.title}</div>
|
|
286
|
+
<div className={styles.stepDescription}>{step.description}</div>
|
|
287
|
+
</div>
|
|
288
|
+
{index < steps.length - 1 && (
|
|
289
|
+
<div className={`${styles.stepConnector} ${currentStep > step.id ? styles.completed : ''}`} />
|
|
290
|
+
)}
|
|
291
|
+
</div>
|
|
292
|
+
))}
|
|
293
|
+
</div>
|
|
294
|
+
);
|
|
295
|
+
|
|
296
|
+
const renderUploadStep = () => (
|
|
297
|
+
<div className={styles.uploadStep}>
|
|
298
|
+
<div {...getRootProps()} className={`${styles.dropzone} ${isDragActive ? styles.active : ''}`}>
|
|
299
|
+
<input {...getInputProps()} />
|
|
300
|
+
<Upload size={48} className={styles.uploadIcon} />
|
|
301
|
+
<h3>Drop your file here, or click to browse</h3>
|
|
302
|
+
<p>Supports CSV, XLS, and XLSX files (max 10MB)</p>
|
|
303
|
+
{uploadedFile && (
|
|
304
|
+
<div className={styles.fileInfo}>
|
|
305
|
+
<FileText size={20} />
|
|
306
|
+
<span>{uploadedFile.name}</span>
|
|
307
|
+
</div>
|
|
308
|
+
)}
|
|
309
|
+
</div>
|
|
310
|
+
|
|
311
|
+
<div className={styles.templateSection}>
|
|
312
|
+
<h4>Need a template?</h4>
|
|
313
|
+
<p>Download a template with the correct column headers:</p>
|
|
314
|
+
<button
|
|
315
|
+
type="button"
|
|
316
|
+
className={styles.templateButton}
|
|
317
|
+
onClick={() => downloadTemplate()}
|
|
318
|
+
>
|
|
319
|
+
<Download size={16} />
|
|
320
|
+
Download Template
|
|
321
|
+
</button>
|
|
322
|
+
</div>
|
|
323
|
+
</div>
|
|
324
|
+
);
|
|
325
|
+
|
|
326
|
+
const downloadTemplate = () => {
|
|
327
|
+
const headers = modelConfig.map(field => field.label);
|
|
328
|
+
const csvContent = headers.join(',') + '\n';
|
|
329
|
+
const blob = new Blob([csvContent], { type: 'text/csv' });
|
|
330
|
+
const url = window.URL.createObjectURL(blob);
|
|
331
|
+
const a = document.createElement('a');
|
|
332
|
+
a.href = url;
|
|
333
|
+
a.download = `${title.toLowerCase().replace(/\s+/g, '_')}_template.csv`;
|
|
334
|
+
a.click();
|
|
335
|
+
window.URL.revokeObjectURL(url);
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
const renderMappingStep = () => (
|
|
339
|
+
<div className={styles.mappingStep}>
|
|
340
|
+
<div className={styles.mappingHeader}>
|
|
341
|
+
<h3>Map File Columns to Data Fields</h3>
|
|
342
|
+
<p>Match columns from your file to the appropriate data fields. Required fields are marked with *</p>
|
|
343
|
+
</div>
|
|
344
|
+
|
|
345
|
+
<div className={styles.mappingContainer}>
|
|
346
|
+
<div className={styles.mappingList}>
|
|
347
|
+
{fileData.headers.map(header => (
|
|
348
|
+
<div key={header} className={styles.mappingRow}>
|
|
349
|
+
<div className={styles.fileColumn}>
|
|
350
|
+
<strong>{header}</strong>
|
|
351
|
+
{fileData.sample_data.length > 0 && (
|
|
352
|
+
<div className={styles.sampleValue}>
|
|
353
|
+
Example: "{fileData.sample_data[0][header] || 'N/A'}"
|
|
354
|
+
</div>
|
|
355
|
+
)}
|
|
356
|
+
</div>
|
|
357
|
+
|
|
358
|
+
<ArrowRight size={20} className={styles.arrow} />
|
|
359
|
+
|
|
360
|
+
<div className={styles.fieldColumn}>
|
|
361
|
+
<Select
|
|
362
|
+
value={columnMapping[header] || ''}
|
|
363
|
+
onChange={(value) => handleMappingChange(header, value)}
|
|
364
|
+
options={[
|
|
365
|
+
{ value: '', label: 'Skip Column' },
|
|
366
|
+
...modelConfig.map(field => ({
|
|
367
|
+
value: field.id,
|
|
368
|
+
label: `${field.label}${field.required ? ' *' : ''}`,
|
|
369
|
+
disabled: getMappedFields().includes(field.id) && columnMapping[header] !== field.id
|
|
370
|
+
}))
|
|
371
|
+
]}
|
|
372
|
+
placeholder="Select field..."
|
|
373
|
+
/>
|
|
374
|
+
</div>
|
|
375
|
+
</div>
|
|
376
|
+
))}
|
|
377
|
+
</div>
|
|
378
|
+
|
|
379
|
+
<div className={styles.mappingSummary}>
|
|
380
|
+
<h4>Mapping Summary</h4>
|
|
381
|
+
<div className={styles.summaryItem}>
|
|
382
|
+
<span>Total Columns:</span>
|
|
383
|
+
<span>{fileData.headers.length}</span>
|
|
384
|
+
</div>
|
|
385
|
+
<div className={styles.summaryItem}>
|
|
386
|
+
<span>Mapped Columns:</span>
|
|
387
|
+
<span>{getMappedFields().length}</span>
|
|
388
|
+
</div>
|
|
389
|
+
<div className={styles.summaryItem}>
|
|
390
|
+
<span>Required Fields:</span>
|
|
391
|
+
<span>{getRequiredFields().length}</span>
|
|
392
|
+
</div>
|
|
393
|
+
|
|
394
|
+
{getUnmappedRequiredFields().length > 0 && (
|
|
395
|
+
<div className={styles.missingFields}>
|
|
396
|
+
<AlertTriangle size={16} />
|
|
397
|
+
<div>
|
|
398
|
+
<strong>Missing Required Fields:</strong>
|
|
399
|
+
<ul>
|
|
400
|
+
{getUnmappedRequiredFields().map(field => {
|
|
401
|
+
const fieldConfig = modelConfig.find(f => f.id === field);
|
|
402
|
+
return <li key={field}>{fieldConfig?.label || field}</li>;
|
|
403
|
+
})}
|
|
404
|
+
</ul>
|
|
405
|
+
</div>
|
|
406
|
+
</div>
|
|
407
|
+
)}
|
|
408
|
+
</div>
|
|
409
|
+
</div>
|
|
410
|
+
</div>
|
|
411
|
+
);
|
|
412
|
+
|
|
413
|
+
const renderPreviewStep = () => (
|
|
414
|
+
<div className={styles.previewStep}>
|
|
415
|
+
<div className={styles.previewHeader}>
|
|
416
|
+
<h3>Data Preview</h3>
|
|
417
|
+
<p>Review the mapped data before importing. Only the first 10 rows are shown.</p>
|
|
418
|
+
</div>
|
|
419
|
+
|
|
420
|
+
{validationSummary && (
|
|
421
|
+
<div className={styles.validationSummary}>
|
|
422
|
+
<div className={styles.summaryCards}>
|
|
423
|
+
<div className={styles.summaryCard}>
|
|
424
|
+
<CheckCircle size={24} className={styles.successIcon} />
|
|
425
|
+
<div>
|
|
426
|
+
<div className={styles.number}>{validationSummary.valid_rows}</div>
|
|
427
|
+
<div className={styles.label}>Valid Rows</div>
|
|
428
|
+
</div>
|
|
429
|
+
</div>
|
|
430
|
+
|
|
431
|
+
<div className={styles.summaryCard}>
|
|
432
|
+
<AlertTriangle size={24} className={styles.warningIcon} />
|
|
433
|
+
<div>
|
|
434
|
+
<div className={styles.number}>{validationSummary.invalid_rows}</div>
|
|
435
|
+
<div className={styles.label}>Invalid Rows</div>
|
|
436
|
+
</div>
|
|
437
|
+
</div>
|
|
438
|
+
</div>
|
|
439
|
+
|
|
440
|
+
{validationSummary.errors.length > 0 && (
|
|
441
|
+
<div className={styles.errorList}>
|
|
442
|
+
<h4>Validation Errors:</h4>
|
|
443
|
+
<div className={styles.errors}>
|
|
444
|
+
{validationSummary.errors.slice(0, 5).map((error, index) => (
|
|
445
|
+
<div key={index} className={styles.errorItem}>
|
|
446
|
+
<strong>Row {error.row}:</strong>
|
|
447
|
+
<ul>
|
|
448
|
+
{error.errors.map((err, i) => <li key={i}>{err}</li>)}
|
|
449
|
+
</ul>
|
|
450
|
+
</div>
|
|
451
|
+
))}
|
|
452
|
+
{validationSummary.errors.length > 5 && (
|
|
453
|
+
<div className={styles.moreErrors}>
|
|
454
|
+
... and {validationSummary.errors.length - 5} more errors
|
|
455
|
+
</div>
|
|
456
|
+
)}
|
|
457
|
+
</div>
|
|
458
|
+
</div>
|
|
459
|
+
)}
|
|
460
|
+
</div>
|
|
461
|
+
)}
|
|
462
|
+
|
|
463
|
+
<div className={styles.previewTable}>
|
|
464
|
+
<div className={styles.tableContainer}>
|
|
465
|
+
<table>
|
|
466
|
+
<thead>
|
|
467
|
+
<tr>
|
|
468
|
+
<th>Status</th>
|
|
469
|
+
{getMappedFields().map(field => {
|
|
470
|
+
const fieldConfig = modelConfig.find(f => f.id === field);
|
|
471
|
+
return <th key={field}>{fieldConfig?.label || field}</th>;
|
|
472
|
+
})}
|
|
473
|
+
</tr>
|
|
474
|
+
</thead>
|
|
475
|
+
<tbody>
|
|
476
|
+
{previewData.map((row, index) => (
|
|
477
|
+
<tr key={index} className={row.valid ? styles.validRow : styles.invalidRow}>
|
|
478
|
+
<td>
|
|
479
|
+
{row.valid ? (
|
|
480
|
+
<CheckCircle size={16} className={styles.successIcon} />
|
|
481
|
+
) : (
|
|
482
|
+
<AlertTriangle size={16} className={styles.errorIcon} />
|
|
483
|
+
)}
|
|
484
|
+
</td>
|
|
485
|
+
{getMappedFields().map(field => (
|
|
486
|
+
<td key={field}>{row.mapped[field] || '-'}</td>
|
|
487
|
+
))}
|
|
488
|
+
</tr>
|
|
489
|
+
))}
|
|
490
|
+
</tbody>
|
|
491
|
+
</table>
|
|
492
|
+
</div>
|
|
493
|
+
</div>
|
|
494
|
+
</div>
|
|
495
|
+
);
|
|
496
|
+
|
|
497
|
+
const renderImportStep = () => (
|
|
498
|
+
<div className={styles.importStep}>
|
|
499
|
+
{loading ? (
|
|
500
|
+
<div className={styles.importing}>
|
|
501
|
+
<RefreshCw size={48} className={styles.spinner} />
|
|
502
|
+
<h3>Importing data...</h3>
|
|
503
|
+
<p>Please wait while we process your data.</p>
|
|
504
|
+
</div>
|
|
505
|
+
) : (
|
|
506
|
+
<div className={styles.importResults}>
|
|
507
|
+
<CheckCircle size={64} className={styles.successIcon} />
|
|
508
|
+
<h3>Import Completed!</h3>
|
|
509
|
+
|
|
510
|
+
{importResults && (
|
|
511
|
+
<div className={styles.resultsSummary}>
|
|
512
|
+
<div className={styles.resultsGrid}>
|
|
513
|
+
<div className={styles.resultItem}>
|
|
514
|
+
<div className={styles.resultNumber}>{importResults.imported}</div>
|
|
515
|
+
<div className={styles.resultLabel}>Records Imported</div>
|
|
516
|
+
</div>
|
|
517
|
+
|
|
518
|
+
<div className={styles.resultItem}>
|
|
519
|
+
<div className={styles.resultNumber}>{importResults.errors?.length || 0}</div>
|
|
520
|
+
<div className={styles.resultLabel}>Errors</div>
|
|
521
|
+
</div>
|
|
522
|
+
|
|
523
|
+
<div className={styles.resultItem}>
|
|
524
|
+
<div className={styles.resultNumber}>{importResults.skipped}</div>
|
|
525
|
+
<div className={styles.resultLabel}>Skipped</div>
|
|
526
|
+
</div>
|
|
527
|
+
</div>
|
|
528
|
+
|
|
529
|
+
{importResults.errors && importResults.errors.length > 0 && (
|
|
530
|
+
<div className={styles.importErrors}>
|
|
531
|
+
<h4>Import Errors:</h4>
|
|
532
|
+
<div className={styles.errorList}>
|
|
533
|
+
{importResults.errors.slice(0, 5).map((error, index) => (
|
|
534
|
+
<div key={index} className={styles.errorItem}>
|
|
535
|
+
<strong>Row {error.row}:</strong>
|
|
536
|
+
<ul>
|
|
537
|
+
{error.errors.map((err, i) => <li key={i}>{err}</li>)}
|
|
538
|
+
</ul>
|
|
539
|
+
</div>
|
|
540
|
+
))}
|
|
541
|
+
</div>
|
|
542
|
+
</div>
|
|
543
|
+
)}
|
|
544
|
+
</div>
|
|
545
|
+
)}
|
|
546
|
+
</div>
|
|
547
|
+
)}
|
|
548
|
+
</div>
|
|
549
|
+
);
|
|
550
|
+
|
|
551
|
+
const renderCurrentStep = () => {
|
|
552
|
+
switch (currentStep) {
|
|
553
|
+
case 1: return renderUploadStep();
|
|
554
|
+
case 2: return renderMappingStep();
|
|
555
|
+
case 3: return renderPreviewStep();
|
|
556
|
+
case 4: return renderImportStep();
|
|
557
|
+
default: return null;
|
|
558
|
+
}
|
|
559
|
+
};
|
|
560
|
+
|
|
561
|
+
return (
|
|
562
|
+
<StandardModal
|
|
563
|
+
isOpen={isOpen}
|
|
564
|
+
onClose={handleClose}
|
|
565
|
+
title={title}
|
|
566
|
+
size="xl"
|
|
567
|
+
className={styles.importWizardModal}
|
|
568
|
+
>
|
|
569
|
+
<div className={styles.modalContent}>
|
|
570
|
+
{renderStepIndicator()}
|
|
571
|
+
|
|
572
|
+
<div className={styles.stepContent}>
|
|
573
|
+
{renderCurrentStep()}
|
|
574
|
+
</div>
|
|
575
|
+
|
|
576
|
+
<div className={styles.modalFooter}>
|
|
577
|
+
<div className={styles.footerLeft}>
|
|
578
|
+
{currentStep > 1 && currentStep < 4 && (
|
|
579
|
+
<button
|
|
580
|
+
type="button"
|
|
581
|
+
onClick={handleBack}
|
|
582
|
+
className={styles.backButton}
|
|
583
|
+
disabled={loading}
|
|
584
|
+
>
|
|
585
|
+
<ArrowLeft size={16} />
|
|
586
|
+
Back
|
|
587
|
+
</button>
|
|
588
|
+
)}
|
|
589
|
+
</div>
|
|
590
|
+
|
|
591
|
+
<div className={styles.footerRight}>
|
|
592
|
+
{currentStep < 4 ? (
|
|
593
|
+
<button
|
|
594
|
+
type="button"
|
|
595
|
+
onClick={handleNext}
|
|
596
|
+
className={styles.nextButton}
|
|
597
|
+
disabled={!canGoNext() || loading}
|
|
598
|
+
>
|
|
599
|
+
{loading ? (
|
|
600
|
+
<>
|
|
601
|
+
<RefreshCw size={16} className={styles.spinner} />
|
|
602
|
+
Processing...
|
|
603
|
+
</>
|
|
604
|
+
) : (
|
|
605
|
+
<>
|
|
606
|
+
{currentStep === 3 ? 'Import' : 'Next'}
|
|
607
|
+
<ArrowRight size={16} />
|
|
608
|
+
</>
|
|
609
|
+
)}
|
|
610
|
+
</button>
|
|
611
|
+
) : (
|
|
612
|
+
<button
|
|
613
|
+
type="button"
|
|
614
|
+
onClick={handleClose}
|
|
615
|
+
className={styles.finishButton}
|
|
616
|
+
>
|
|
617
|
+
Finish
|
|
618
|
+
</button>
|
|
619
|
+
)}
|
|
620
|
+
</div>
|
|
621
|
+
</div>
|
|
622
|
+
</div>
|
|
623
|
+
</StandardModal>
|
|
624
|
+
);
|
|
625
|
+
};
|
|
626
|
+
|
|
627
|
+
export default ImportWizardModal;
|
|
@@ -0,0 +1,622 @@
|
|
|
1
|
+
.importWizardModal {
|
|
2
|
+
.modalContent {
|
|
3
|
+
display: flex;
|
|
4
|
+
flex-direction: column;
|
|
5
|
+
height: 80vh;
|
|
6
|
+
max-height: 800px;
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
.stepIndicator {
|
|
11
|
+
display: flex;
|
|
12
|
+
align-items: center;
|
|
13
|
+
justify-content: center;
|
|
14
|
+
padding: 2rem 0;
|
|
15
|
+
border-bottom: 1px solid #e5e7eb;
|
|
16
|
+
margin-bottom: 2rem;
|
|
17
|
+
|
|
18
|
+
.stepContainer {
|
|
19
|
+
display: flex;
|
|
20
|
+
align-items: center;
|
|
21
|
+
position: relative;
|
|
22
|
+
|
|
23
|
+
&:not(:last-child) {
|
|
24
|
+
margin-right: 2rem;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.step {
|
|
29
|
+
width: 48px;
|
|
30
|
+
height: 48px;
|
|
31
|
+
border-radius: 50%;
|
|
32
|
+
border: 2px solid #d1d5db;
|
|
33
|
+
display: flex;
|
|
34
|
+
align-items: center;
|
|
35
|
+
justify-content: center;
|
|
36
|
+
background: white;
|
|
37
|
+
color: #6b7280;
|
|
38
|
+
transition: all 0.3s ease;
|
|
39
|
+
|
|
40
|
+
&.active {
|
|
41
|
+
border-color: #3b82f6;
|
|
42
|
+
background: #3b82f6;
|
|
43
|
+
color: white;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.stepInfo {
|
|
48
|
+
margin-left: 1rem;
|
|
49
|
+
|
|
50
|
+
.stepTitle {
|
|
51
|
+
font-weight: 600;
|
|
52
|
+
font-size: 0.95rem;
|
|
53
|
+
color: #111827;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.stepDescription {
|
|
57
|
+
font-size: 0.85rem;
|
|
58
|
+
color: #6b7280;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.stepConnector {
|
|
63
|
+
width: 40px;
|
|
64
|
+
height: 2px;
|
|
65
|
+
background: #d1d5db;
|
|
66
|
+
margin: 0 1rem;
|
|
67
|
+
transition: background-color 0.3s ease;
|
|
68
|
+
|
|
69
|
+
&.completed {
|
|
70
|
+
background: #3b82f6;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.stepContent {
|
|
76
|
+
flex: 1;
|
|
77
|
+
overflow-y: auto;
|
|
78
|
+
padding: 0 1rem;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Upload Step
|
|
82
|
+
.uploadStep {
|
|
83
|
+
.dropzone {
|
|
84
|
+
border: 2px dashed #d1d5db;
|
|
85
|
+
border-radius: 12px;
|
|
86
|
+
padding: 3rem 2rem;
|
|
87
|
+
text-align: center;
|
|
88
|
+
cursor: pointer;
|
|
89
|
+
transition: all 0.3s ease;
|
|
90
|
+
background: #fafafa;
|
|
91
|
+
|
|
92
|
+
&:hover, &.active {
|
|
93
|
+
border-color: #3b82f6;
|
|
94
|
+
background: #f0f8ff;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.uploadIcon {
|
|
98
|
+
color: #6b7280;
|
|
99
|
+
margin-bottom: 1rem;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
h3 {
|
|
103
|
+
margin: 0 0 0.5rem 0;
|
|
104
|
+
color: #111827;
|
|
105
|
+
font-size: 1.25rem;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
p {
|
|
109
|
+
margin: 0;
|
|
110
|
+
color: #6b7280;
|
|
111
|
+
font-size: 0.95rem;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
.fileInfo {
|
|
115
|
+
display: flex;
|
|
116
|
+
align-items: center;
|
|
117
|
+
justify-content: center;
|
|
118
|
+
gap: 0.5rem;
|
|
119
|
+
margin-top: 1rem;
|
|
120
|
+
padding: 0.75rem 1rem;
|
|
121
|
+
background: #e0f2fe;
|
|
122
|
+
border-radius: 8px;
|
|
123
|
+
color: #0369a1;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
.templateSection {
|
|
128
|
+
margin-top: 2rem;
|
|
129
|
+
padding: 1.5rem;
|
|
130
|
+
background: #f9fafb;
|
|
131
|
+
border-radius: 8px;
|
|
132
|
+
text-align: center;
|
|
133
|
+
|
|
134
|
+
h4 {
|
|
135
|
+
margin: 0 0 0.5rem 0;
|
|
136
|
+
color: #111827;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
p {
|
|
140
|
+
margin: 0 0 1rem 0;
|
|
141
|
+
color: #6b7280;
|
|
142
|
+
font-size: 0.9rem;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
.templateButton {
|
|
146
|
+
display: inline-flex;
|
|
147
|
+
align-items: center;
|
|
148
|
+
gap: 0.5rem;
|
|
149
|
+
padding: 0.75rem 1.25rem;
|
|
150
|
+
background: #3b82f6;
|
|
151
|
+
color: white;
|
|
152
|
+
border: none;
|
|
153
|
+
border-radius: 6px;
|
|
154
|
+
cursor: pointer;
|
|
155
|
+
font-size: 0.9rem;
|
|
156
|
+
transition: background-color 0.2s ease;
|
|
157
|
+
|
|
158
|
+
&:hover {
|
|
159
|
+
background: #2563eb;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Mapping Step
|
|
166
|
+
.mappingStep {
|
|
167
|
+
.mappingHeader {
|
|
168
|
+
margin-bottom: 2rem;
|
|
169
|
+
|
|
170
|
+
h3 {
|
|
171
|
+
margin: 0 0 0.5rem 0;
|
|
172
|
+
color: #111827;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
p {
|
|
176
|
+
margin: 0;
|
|
177
|
+
color: #6b7280;
|
|
178
|
+
font-size: 0.95rem;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
.mappingContainer {
|
|
183
|
+
display: grid;
|
|
184
|
+
grid-template-columns: 1fr 300px;
|
|
185
|
+
gap: 2rem;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
.mappingList {
|
|
189
|
+
.mappingRow {
|
|
190
|
+
display: grid;
|
|
191
|
+
grid-template-columns: 1fr auto 1fr;
|
|
192
|
+
gap: 1rem;
|
|
193
|
+
align-items: center;
|
|
194
|
+
padding: 1rem;
|
|
195
|
+
border: 1px solid #e5e7eb;
|
|
196
|
+
border-radius: 8px;
|
|
197
|
+
margin-bottom: 1rem;
|
|
198
|
+
background: white;
|
|
199
|
+
|
|
200
|
+
.fileColumn {
|
|
201
|
+
strong {
|
|
202
|
+
display: block;
|
|
203
|
+
color: #111827;
|
|
204
|
+
margin-bottom: 0.25rem;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
.sampleValue {
|
|
208
|
+
font-size: 0.8rem;
|
|
209
|
+
color: #6b7280;
|
|
210
|
+
font-style: italic;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
.arrow {
|
|
215
|
+
color: #6b7280;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
.fieldColumn {
|
|
219
|
+
select, .select-container {
|
|
220
|
+
width: 100%;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
.mappingSummary {
|
|
227
|
+
background: #f9fafb;
|
|
228
|
+
border-radius: 8px;
|
|
229
|
+
padding: 1.5rem;
|
|
230
|
+
|
|
231
|
+
h4 {
|
|
232
|
+
margin: 0 0 1rem 0;
|
|
233
|
+
color: #111827;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
.summaryItem {
|
|
237
|
+
display: flex;
|
|
238
|
+
justify-content: space-between;
|
|
239
|
+
padding: 0.5rem 0;
|
|
240
|
+
border-bottom: 1px solid #e5e7eb;
|
|
241
|
+
|
|
242
|
+
&:last-child {
|
|
243
|
+
border-bottom: none;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
.missingFields {
|
|
248
|
+
margin-top: 1rem;
|
|
249
|
+
padding: 1rem;
|
|
250
|
+
background: #fef2f2;
|
|
251
|
+
border-radius: 6px;
|
|
252
|
+
color: #dc2626;
|
|
253
|
+
|
|
254
|
+
display: flex;
|
|
255
|
+
align-items: flex-start;
|
|
256
|
+
gap: 0.5rem;
|
|
257
|
+
|
|
258
|
+
ul {
|
|
259
|
+
margin: 0.5rem 0 0 0;
|
|
260
|
+
padding-left: 1rem;
|
|
261
|
+
|
|
262
|
+
li {
|
|
263
|
+
margin-bottom: 0.25rem;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Preview Step
|
|
271
|
+
.previewStep {
|
|
272
|
+
.previewHeader {
|
|
273
|
+
margin-bottom: 2rem;
|
|
274
|
+
|
|
275
|
+
h3 {
|
|
276
|
+
margin: 0 0 0.5rem 0;
|
|
277
|
+
color: #111827;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
p {
|
|
281
|
+
margin: 0;
|
|
282
|
+
color: #6b7280;
|
|
283
|
+
font-size: 0.95rem;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
.validationSummary {
|
|
288
|
+
margin-bottom: 2rem;
|
|
289
|
+
|
|
290
|
+
.summaryCards {
|
|
291
|
+
display: grid;
|
|
292
|
+
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
293
|
+
gap: 1rem;
|
|
294
|
+
margin-bottom: 1rem;
|
|
295
|
+
|
|
296
|
+
.summaryCard {
|
|
297
|
+
display: flex;
|
|
298
|
+
align-items: center;
|
|
299
|
+
gap: 1rem;
|
|
300
|
+
padding: 1.5rem;
|
|
301
|
+
background: white;
|
|
302
|
+
border-radius: 8px;
|
|
303
|
+
border: 1px solid #e5e7eb;
|
|
304
|
+
|
|
305
|
+
.number {
|
|
306
|
+
font-size: 1.75rem;
|
|
307
|
+
font-weight: bold;
|
|
308
|
+
color: #111827;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
.label {
|
|
312
|
+
font-size: 0.9rem;
|
|
313
|
+
color: #6b7280;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
.successIcon {
|
|
317
|
+
color: #059669;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
.warningIcon {
|
|
321
|
+
color: #d97706;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
.errorList {
|
|
327
|
+
background: #fef2f2;
|
|
328
|
+
border-radius: 8px;
|
|
329
|
+
padding: 1rem;
|
|
330
|
+
|
|
331
|
+
h4 {
|
|
332
|
+
margin: 0 0 1rem 0;
|
|
333
|
+
color: #dc2626;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
.errors {
|
|
337
|
+
max-height: 200px;
|
|
338
|
+
overflow-y: auto;
|
|
339
|
+
|
|
340
|
+
.errorItem {
|
|
341
|
+
margin-bottom: 1rem;
|
|
342
|
+
|
|
343
|
+
strong {
|
|
344
|
+
color: #dc2626;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
ul {
|
|
348
|
+
margin: 0.5rem 0 0 1rem;
|
|
349
|
+
|
|
350
|
+
li {
|
|
351
|
+
color: #991b1b;
|
|
352
|
+
font-size: 0.9rem;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
.moreErrors {
|
|
358
|
+
color: #6b7280;
|
|
359
|
+
font-style: italic;
|
|
360
|
+
text-align: center;
|
|
361
|
+
padding-top: 1rem;
|
|
362
|
+
border-top: 1px solid #f3f4f6;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
.previewTable {
|
|
369
|
+
border: 1px solid #e5e7eb;
|
|
370
|
+
border-radius: 8px;
|
|
371
|
+
overflow: hidden;
|
|
372
|
+
|
|
373
|
+
.tableContainer {
|
|
374
|
+
overflow-x: auto;
|
|
375
|
+
max-height: 400px;
|
|
376
|
+
|
|
377
|
+
table {
|
|
378
|
+
width: 100%;
|
|
379
|
+
border-collapse: collapse;
|
|
380
|
+
|
|
381
|
+
th, td {
|
|
382
|
+
padding: 0.75rem;
|
|
383
|
+
text-align: left;
|
|
384
|
+
border-bottom: 1px solid #e5e7eb;
|
|
385
|
+
font-size: 0.9rem;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
th {
|
|
389
|
+
background: #f9fafb;
|
|
390
|
+
font-weight: 600;
|
|
391
|
+
color: #111827;
|
|
392
|
+
position: sticky;
|
|
393
|
+
top: 0;
|
|
394
|
+
z-index: 10;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
.validRow {
|
|
398
|
+
&:hover {
|
|
399
|
+
background: #f0fdf4;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
.invalidRow {
|
|
404
|
+
background: #fef2f2;
|
|
405
|
+
|
|
406
|
+
&:hover {
|
|
407
|
+
background: #fecaca;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
.successIcon {
|
|
412
|
+
color: #059669;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
.errorIcon {
|
|
416
|
+
color: #dc2626;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// Import Step
|
|
424
|
+
.importStep {
|
|
425
|
+
display: flex;
|
|
426
|
+
align-items: center;
|
|
427
|
+
justify-content: center;
|
|
428
|
+
min-height: 400px;
|
|
429
|
+
|
|
430
|
+
.importing {
|
|
431
|
+
text-align: center;
|
|
432
|
+
|
|
433
|
+
.spinner {
|
|
434
|
+
animation: spin 1s linear infinite;
|
|
435
|
+
color: #3b82f6;
|
|
436
|
+
margin-bottom: 1rem;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
h3 {
|
|
440
|
+
margin: 0 0 0.5rem 0;
|
|
441
|
+
color: #111827;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
p {
|
|
445
|
+
margin: 0;
|
|
446
|
+
color: #6b7280;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
.importResults {
|
|
451
|
+
text-align: center;
|
|
452
|
+
width: 100%;
|
|
453
|
+
|
|
454
|
+
.successIcon {
|
|
455
|
+
color: #059669;
|
|
456
|
+
margin-bottom: 1rem;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
h3 {
|
|
460
|
+
margin: 0 0 2rem 0;
|
|
461
|
+
color: #111827;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
.resultsSummary {
|
|
465
|
+
.resultsGrid {
|
|
466
|
+
display: grid;
|
|
467
|
+
grid-template-columns: repeat(3, 1fr);
|
|
468
|
+
gap: 1rem;
|
|
469
|
+
margin-bottom: 2rem;
|
|
470
|
+
|
|
471
|
+
.resultItem {
|
|
472
|
+
padding: 1.5rem;
|
|
473
|
+
background: white;
|
|
474
|
+
border: 1px solid #e5e7eb;
|
|
475
|
+
border-radius: 8px;
|
|
476
|
+
|
|
477
|
+
.resultNumber {
|
|
478
|
+
font-size: 2rem;
|
|
479
|
+
font-weight: bold;
|
|
480
|
+
color: #111827;
|
|
481
|
+
margin-bottom: 0.25rem;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
.resultLabel {
|
|
485
|
+
color: #6b7280;
|
|
486
|
+
font-size: 0.9rem;
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
.importErrors {
|
|
492
|
+
background: #fef2f2;
|
|
493
|
+
border-radius: 8px;
|
|
494
|
+
padding: 1rem;
|
|
495
|
+
text-align: left;
|
|
496
|
+
|
|
497
|
+
h4 {
|
|
498
|
+
margin: 0 0 1rem 0;
|
|
499
|
+
color: #dc2626;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
.errorList {
|
|
503
|
+
max-height: 200px;
|
|
504
|
+
overflow-y: auto;
|
|
505
|
+
|
|
506
|
+
.errorItem {
|
|
507
|
+
margin-bottom: 1rem;
|
|
508
|
+
|
|
509
|
+
strong {
|
|
510
|
+
color: #dc2626;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
ul {
|
|
514
|
+
margin: 0.5rem 0 0 1rem;
|
|
515
|
+
|
|
516
|
+
li {
|
|
517
|
+
color: #991b1b;
|
|
518
|
+
font-size: 0.9rem;
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// Footer
|
|
529
|
+
.modalFooter {
|
|
530
|
+
display: flex;
|
|
531
|
+
justify-content: space-between;
|
|
532
|
+
align-items: center;
|
|
533
|
+
padding: 1.5rem 0 0 0;
|
|
534
|
+
border-top: 1px solid #e5e7eb;
|
|
535
|
+
margin-top: 2rem;
|
|
536
|
+
|
|
537
|
+
.footerLeft, .footerRight {
|
|
538
|
+
display: flex;
|
|
539
|
+
gap: 1rem;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
.backButton, .nextButton, .finishButton {
|
|
543
|
+
display: inline-flex;
|
|
544
|
+
align-items: center;
|
|
545
|
+
gap: 0.5rem;
|
|
546
|
+
padding: 0.75rem 1.5rem;
|
|
547
|
+
border-radius: 6px;
|
|
548
|
+
cursor: pointer;
|
|
549
|
+
font-weight: 500;
|
|
550
|
+
transition: all 0.2s ease;
|
|
551
|
+
border: none;
|
|
552
|
+
|
|
553
|
+
&:disabled {
|
|
554
|
+
opacity: 0.6;
|
|
555
|
+
cursor: not-allowed;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
.spinner {
|
|
559
|
+
animation: spin 1s linear infinite;
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
.backButton {
|
|
564
|
+
background: #f3f4f6;
|
|
565
|
+
color: #374151;
|
|
566
|
+
|
|
567
|
+
&:hover:not(:disabled) {
|
|
568
|
+
background: #e5e7eb;
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
.nextButton, .finishButton {
|
|
573
|
+
background: #3b82f6;
|
|
574
|
+
color: white;
|
|
575
|
+
|
|
576
|
+
&:hover:not(:disabled) {
|
|
577
|
+
background: #2563eb;
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
@keyframes spin {
|
|
583
|
+
from {
|
|
584
|
+
transform: rotate(0deg);
|
|
585
|
+
}
|
|
586
|
+
to {
|
|
587
|
+
transform: rotate(360deg);
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
// Responsive
|
|
592
|
+
@media (max-width: 768px) {
|
|
593
|
+
.stepIndicator {
|
|
594
|
+
flex-direction: column;
|
|
595
|
+
gap: 1rem;
|
|
596
|
+
|
|
597
|
+
.stepContainer {
|
|
598
|
+
margin-right: 0;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
.stepConnector {
|
|
602
|
+
display: none;
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
.mappingContainer {
|
|
607
|
+
grid-template-columns: 1fr;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
.mappingRow {
|
|
611
|
+
grid-template-columns: 1fr;
|
|
612
|
+
gap: 0.5rem;
|
|
613
|
+
|
|
614
|
+
.arrow {
|
|
615
|
+
transform: rotate(90deg);
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
.resultsGrid {
|
|
620
|
+
grid-template-columns: 1fr;
|
|
621
|
+
}
|
|
622
|
+
}
|
package/src/index.js
CHANGED
|
@@ -38,6 +38,7 @@ import AudioPlayer from './components/controls/AudioPlayer';
|
|
|
38
38
|
import GalleryModal from './components/modals/GalleryModal';
|
|
39
39
|
import ImageModal from './components/ImageModal';
|
|
40
40
|
import DatePickerPortal from './components/utils/DatePickerPortal';
|
|
41
|
+
import ImportWizardModal from './components/ImportWizardModal';
|
|
41
42
|
export * from './components/columns/ColumnRenderers.jsx';
|
|
42
43
|
|
|
43
44
|
/** Auth Specific Components */
|
|
@@ -137,6 +138,7 @@ export {
|
|
|
137
138
|
GenericQuote,
|
|
138
139
|
GenericReport,
|
|
139
140
|
GenericSort,
|
|
141
|
+
ImportWizardModal,
|
|
140
142
|
Loader,
|
|
141
143
|
Login,
|
|
142
144
|
MergeEntity,
|