@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 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.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
- 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,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
  }
@@ -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,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
- formData.append('target_model', targetModel);
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
- const response = await CustomFetch('/ajax/import/process', {
225
- method: 'POST',
226
- body: formData,
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 (response.success) {
230
- setImportResults(response);
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! ${response.imported} records imported successfully.`);
236
+ toast.success(`Import completed! ${result.imported} records imported successfully.`);
233
237
  } else {
234
- toast.error(response.message || 'Import failed');
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
- <Select
365
+ <select
366
+ className={styles.fieldSelect}
362
367
  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
- />
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 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
  }