@visns-studio/visns-components 5.15.24 → 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 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.99.0",
12
- "@nivo/core": "^0.99.0",
13
- "@nivo/line": "^0.99.0",
14
- "@nivo/pie": "^0.99.0",
15
- "@react-pdf/renderer": "^4.3.0",
16
- "@tanstack/react-query": "^5.89.0",
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.8.0",
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.14",
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.14",
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.92.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.24",
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
- const text = await performAdvancedOCR(capturedImage);
560
- const parsedData = intelligentFieldMapping(text);
561
- setExtractedData({ ...parsedData, rawText: text });
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
  }
@@ -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
- formData = {
177
- ...formData,
178
- _method: 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
- fetchOptions.body = formData ? JSON.stringify(formData) : undefined;
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
- method: 'POST',
112
- body: formData,
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(response.data.headers);
120
-
117
+ await getSuggestedMappings(result.data.headers);
118
+
121
119
  setCurrentStep(2);
122
- toast.success(`File parsed successfully! Found ${response.data.total_rows} rows.`);
120
+ toast.success(`File parsed successfully! Found ${result.data.total_rows} rows.`);
123
121
  } else {
124
- toast.error(response.message || 'Failed to parse file');
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 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);
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
- method: 'POST',
194
- body: formData,
195
- });
188
+ const response = await CustomFetch('/ajax/import/preview-mapping', 'POST', formData);
189
+ const result = response.data;
196
190
 
197
- if (response.success) {
198
- setPreviewData(response.preview);
199
- setValidationSummary(response.validation_summary);
191
+ if (result.success) {
192
+ setPreviewData(result.preview);
193
+ setValidationSummary(result.validation_summary);
200
194
  setCurrentStep(3);
201
195
  } else {
202
- toast.error(response.message || 'Failed to preview data');
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
- formData.append('target_model', targetModel);
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
- const response = await CustomFetch('/ajax/import/process', {
225
- method: 'POST',
226
- body: formData,
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 (response.success) {
230
- setImportResults(response);
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! ${response.imported} records imported successfully.`);
244
+ toast.success(`Import completed! ${result.imported} records imported successfully.`);
233
245
  } else {
234
- toast.error(response.message || 'Import failed');
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
- <Select
373
+ <select
374
+ className={styles.fieldSelect}
362
375
  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
- />
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 &&
@@ -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>
@@ -11,36 +11,46 @@
11
11
  display: flex;
12
12
  align-items: center;
13
13
  justify-content: center;
14
- padding: 2rem 0;
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: 2rem;
25
+ margin-right: 3rem;
25
26
  }
26
27
  }
27
-
28
+
28
29
  .step {
29
- width: 48px;
30
- height: 48px;
30
+ width: 56px;
31
+ height: 56px;
32
+ min-width: 56px;
33
+ min-height: 56px;
31
34
  border-radius: 50%;
32
- border: 2px solid #d1d5db;
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: #3b82f6;
42
- background: #3b82f6;
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: 40px;
64
- height: 2px;
73
+ width: 60px;
74
+ height: 3px;
65
75
  background: #d1d5db;
66
- margin: 0 1rem;
76
+ margin: 0 1.5rem;
67
77
  transition: background-color 0.3s ease;
68
-
78
+ border-radius: 2px;
79
+
69
80
  &.completed {
70
- background: #3b82f6;
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 1rem;
89
+ padding: 0 2.5rem;
79
90
  }
80
91
 
81
92
  // Upload Step
82
93
  .uploadStep {
83
94
  .dropzone {
84
- border: 2px dashed #d1d5db;
85
- border-radius: 12px;
86
- padding: 3rem 2rem;
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: #3b82f6;
94
- background: #f0f8ff;
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: #6b7280;
99
- margin-bottom: 1rem;
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.5rem;
119
- margin-top: 1rem;
120
- padding: 0.75rem 1rem;
121
- background: #e0f2fe;
122
- border-radius: 8px;
123
- color: #0369a1;
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: 2rem;
129
- padding: 1.5rem;
142
+ margin-top: 2.5rem;
143
+ padding: 2rem;
130
144
  background: #f9fafb;
131
- border-radius: 8px;
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.5rem;
149
- padding: 0.75rem 1.25rem;
150
- background: #3b82f6;
163
+ gap: 0.75rem;
164
+ padding: 1rem 2rem;
165
+ background: #001f3f;
151
166
  color: white;
152
167
  border: none;
153
- border-radius: 6px;
168
+ border-radius: 8px;
154
169
  cursor: pointer;
155
- font-size: 0.9rem;
156
- transition: background-color 0.2s ease;
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: #2563eb;
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
- select, .select-container {
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: 1.5rem 0 0 0;
534
- border-top: 1px solid #e5e7eb;
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.5rem;
546
- padding: 0.75rem 1.5rem;
547
- border-radius: 6px;
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.6;
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: #3b82f6;
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: #2563eb;
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
  }