@visns-studio/visns-components 5.15.9 → 5.15.10

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
@@ -9,10 +9,11 @@ A comprehensive React component library used by the VISNS Studio team for CRM an
9
9
 
10
10
  VISNS Components is a React-based UI component library that provides a set of reusable, consistent, and customizable components for building web applications. It includes components for authentication, data grids, forms, navigation, and more, designed to work seamlessly together.
11
11
 
12
- ## Recent Updates (v5.15.6)
12
+ ## Recent Updates (v5.15.10)
13
13
 
14
14
  ### Latest Enhancements
15
15
 
16
+ - **CameraPlacement Legacy Import**: Added comprehensive legacy JSON file import functionality with support for multiple data formats, automatic image loading, and seamless database integration
16
17
  - **CameraPlacement Module**: Enhanced interactive camera placement tool with improved UI consistency, better icon visibility, drag-and-drop image upload, professional export capabilities, and complete Verkada database integration
17
18
  - **Complete StandardModal Migration**: All components now use StandardModal instead of reactjs-popup for consistent modal behavior across the entire library
18
19
  - **HTML Content Support**: Terms and conditions now properly parse HTML content from TinyMCE editors with clean rendering
@@ -2461,6 +2462,15 @@ import { CameraPlacement } from 'visns-components';
2461
2462
  <CameraPlacement />
2462
2463
  ```
2463
2464
 
2465
+ **Legacy Import Workflow:**
2466
+ 1. **Click Import Button**: Orange folder icon (📁) in the header controls
2467
+ 2. **Select JSON File**: Choose legacy camera placement JSON file (.json extension)
2468
+ 3. **Automatic Processing**: Smart parser handles multiple formats automatically
2469
+ 4. **Import Validation**: Real-time validation with detailed error messages if needed
2470
+ 5. **Data Loading**: Cameras, background images, and project metadata loaded instantly
2471
+ 6. **Review & Adjust**: Fine-tune camera positions if coordinate scaling differs
2472
+ 7. **Save to Database**: Use existing save functionality to persist imported data
2473
+
2464
2474
  **Key Features:**
2465
2475
  - **Enhanced User Interface**: Modern icon-based controls with tooltips, consistent color theming using project CSS variables
2466
2476
  - **Dual Mode Support**: Switch between Mapbox map integration and image-based placement with seamless transitions
@@ -2470,10 +2480,14 @@ import { CameraPlacement } from 'visns-components';
2470
2480
  - **FOV Visualization**: Intelligent field-of-view triangles with inverse distance scaling for realistic coverage display
2471
2481
  - **Interactive Placement**: Drag-and-drop camera positioning with rotation handles for directional adjustment
2472
2482
  - **Save/Load Projects**: Export/import camera placements as JSON files with project metadata and timestamps
2483
+ - **Legacy JSON Import**: One-click import of legacy camera placement files with automatic format detection and base64 image support
2473
2484
  - **Professional Exports**: Generate high-quality PNG reports with side-by-side camera specifications and installation details
2474
2485
  - **SweetAlert Integration**: Professional modal dialogs for confirmations, error handling, and success notifications
2475
2486
 
2476
- **Recent Improvements (v5.15.6):**
2487
+ **Recent Improvements (v5.15.10):**
2488
+ - **Legacy JSON Import**: Comprehensive import functionality supporting multiple legacy data formats with automatic image loading and base64 processing
2489
+ - **Enhanced Controls Layout**: Expanded controls container to accommodate new import button with proper spacing and responsive design
2490
+ - **Smart Data Parser**: Intelligent JSON parsing that handles 4+ different legacy format structures with fallback handling
2477
2491
  - **Icon Visibility**: Fixed save button icon visibility with improved color contrast and explicit styling
2478
2492
  - **UI Consistency**: All buttons now use project-standard color variables for consistent theming
2479
2493
  - **Tooltips**: CSS-based tooltips for all control buttons with descriptive text
@@ -2488,6 +2502,30 @@ import { CameraPlacement } from 'visns-components';
2488
2502
  - **Advanced Models**: Support for optical zoom, telephoto lenses, 4K resolution, and specialized security features
2489
2503
  - **Real-time Updates**: Live camera specification display in sidebar with direction controls and coordinate tracking
2490
2504
 
2505
+ **Legacy Import System (v5.15.10+):**
2506
+ - **Multi-Format Support**: Handles 4+ different legacy JSON structures automatically with intelligent format detection
2507
+ - **Base64 Image Processing**: Seamless import of large images (4MB+) with base64 data URL support
2508
+ - **Smart Data Validation**: Comprehensive validation with user-friendly error messages and detailed debugging
2509
+ - **Format Flexibility**: Supports various naming conventions (`image.src`, `uploadedImage`, `backgroundImage`, etc.)
2510
+ - **One-Click Migration**: Import legacy projects with cameras, images, and metadata in a single operation
2511
+ - **Database Integration**: Imported data immediately compatible with save/load database functionality
2512
+ - **Visual Import Feedback**: Loading indicators, success dialogs with import summaries, and error handling
2513
+
2514
+ **Supported Legacy Formats:**
2515
+ ```javascript
2516
+ // Format 1: Standard export with image object
2517
+ { "image": { "src": "data:image/...", "name": "..." }, "cameras": [...] }
2518
+
2519
+ // Format 2: Nested placement structure
2520
+ { "placement": { "cameras": [...], "image": "...", "projectName": "..." } }
2521
+
2522
+ // Format 3: Simple camera array
2523
+ [{ "id": 1, "name": "Camera 1", "x": 100, "y": 200, ... }]
2524
+
2525
+ // Format 4: Auto-detected custom structures
2526
+ { "customData": { "cameraArray": [...] }, "backgroundImage": "..." }
2527
+ ```
2528
+
2491
2529
  **Technical Capabilities:**
2492
2530
  - **Map Integration**: Mapbox GL JS with geocoding search, satellite imagery, and custom marker styling
2493
2531
  - **Image Processing**: Support for floor plans and site diagrams with HTML5 file API and FileReader integration
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.9",
97
+ "version": "5.15.10",
98
98
  "description": "Various packages to assist in the development of our Custom Applications.",
99
99
  "main": "src/index.js",
100
100
  "files": [
@@ -48,6 +48,7 @@ const CameraPlacement = ({ userProfile }) => {
48
48
  const geocoderRef = useRef(null);
49
49
  const imageContainer = useRef(null);
50
50
  const fileInputRef = useRef(null);
51
+ const jsonFileInputRef = useRef(null);
51
52
  const [isDragOver, setIsDragOver] = useState(false);
52
53
  const [isRotating, setIsRotating] = useState(false);
53
54
 
@@ -186,6 +187,282 @@ const CameraPlacement = ({ userProfile }) => {
186
187
  [map]
187
188
  );
188
189
 
190
+ // Handle legacy JSON file upload
191
+ const handleJsonFileUpload = useCallback(async (event) => {
192
+ const file = event.target.files[0];
193
+ if (!file) return;
194
+
195
+ if (!file.name.toLowerCase().endsWith('.json')) {
196
+ Swal.fire({
197
+ icon: 'error',
198
+ title: 'Invalid File Type',
199
+ text: 'Please select a JSON file (.json extension)',
200
+ confirmButtonColor: '#ef4444',
201
+ });
202
+ return;
203
+ }
204
+
205
+ try {
206
+ const text = await file.text();
207
+ const jsonData = JSON.parse(text);
208
+
209
+ // Show loading indicator
210
+ Swal.fire({
211
+ title: 'Processing Legacy Data...',
212
+ text: 'Converting and validating JSON data',
213
+ allowOutsideClick: false,
214
+ showConfirmButton: false,
215
+ didOpen: () => {
216
+ Swal.showLoading();
217
+ }
218
+ });
219
+
220
+ // Process and validate the JSON data
221
+ const result = await processLegacyJsonData(jsonData);
222
+
223
+ Swal.close();
224
+
225
+ if (result.success) {
226
+ // Load the processed data
227
+ setProjectName(result.data.projectName);
228
+ setMode(result.data.mode);
229
+ setCameras(result.data.cameras);
230
+
231
+ if (result.data.uploadedImage) {
232
+ setUploadedImage(result.data.uploadedImage);
233
+ }
234
+
235
+ // Clear the selected project since this is new legacy data
236
+ setSelectedProject(null);
237
+ setSidebarTab('cameras');
238
+
239
+ Swal.fire({
240
+ icon: 'success',
241
+ title: 'Legacy Data Loaded!',
242
+ html: `
243
+ <div style="text-align: left;">
244
+ <p><strong>Successfully imported:</strong></p>
245
+ <ul style="margin: 10px 0; padding-left: 20px;">
246
+ <li>Project: "${result.data.projectName}"</li>
247
+ <li>Cameras: ${result.data.cameras.length}</li>
248
+ <li>Mode: ${result.data.mode}</li>
249
+ ${result.data.uploadedImage ? '<li>Image: Yes</li>' : '<li>Image: No</li>'}
250
+ </ul>
251
+ <p style="color: #6b7280; font-size: 14px; margin-top: 15px;">
252
+ <strong>Next steps:</strong> Review the imported data and save it to the database using the Save button.
253
+ </p>
254
+ </div>
255
+ `,
256
+ confirmButtonColor: '#3b82f6',
257
+ confirmButtonText: 'Continue',
258
+ });
259
+ } else {
260
+ throw new Error(result.error);
261
+ }
262
+ } catch (error) {
263
+ Swal.fire({
264
+ icon: 'error',
265
+ title: 'Import Failed',
266
+ html: `
267
+ <div style="text-align: left;">
268
+ <p>Failed to import legacy JSON file:</p>
269
+ <div style="background: #f3f4f6; padding: 10px; border-radius: 4px; margin: 10px 0; font-family: monospace; font-size: 12px; max-height: 150px; overflow-y: auto;">
270
+ ${error.message}
271
+ </div>
272
+ <p style="color: #6b7280; font-size: 14px;">
273
+ Please check that the JSON file contains valid camera placement data.
274
+ </p>
275
+ </div>
276
+ `,
277
+ confirmButtonColor: '#ef4444',
278
+ });
279
+ }
280
+
281
+ // Clear the file input
282
+ event.target.value = '';
283
+ }, []);
284
+
285
+ // Process legacy JSON data and convert to current format
286
+ const processLegacyJsonData = useCallback(async (jsonData) => {
287
+ try {
288
+ // Validate basic structure
289
+ if (!jsonData || typeof jsonData !== 'object') {
290
+ throw new Error('Invalid JSON structure - expected an object');
291
+ }
292
+
293
+ let cameras = [];
294
+ let projectName = '';
295
+ let mode = 'image';
296
+ let uploadedImage = null;
297
+
298
+ // Handle different legacy JSON formats
299
+
300
+ // Format 1: Direct camera placement export with cameras array
301
+ if (jsonData.cameras && Array.isArray(jsonData.cameras)) {
302
+ cameras = jsonData.cameras.map((camera, index) => {
303
+ // Ensure required fields exist with fallbacks
304
+ const id = camera.id || Date.now() + index;
305
+ const name = camera.name || camera.customName || `Camera ${index + 1}`;
306
+ const x = typeof camera.x === 'number' ? camera.x : (camera.position?.x || 0);
307
+ const y = typeof camera.y === 'number' ? camera.y : (camera.position?.y || 0);
308
+ const direction = typeof camera.direction === 'number' ? camera.direction : (camera.rotation || 0);
309
+
310
+ return {
311
+ id: id,
312
+ name: name,
313
+ customName: camera.customName || '',
314
+ x: x,
315
+ y: y,
316
+ direction: direction,
317
+ verkadaModel: camera.verkadaModel || camera.model || null,
318
+ timestamp: camera.timestamp || new Date().toISOString(),
319
+ };
320
+ });
321
+
322
+ projectName = jsonData.projectName || jsonData.name || jsonData.title || 'Imported Legacy Project';
323
+ mode = jsonData.mode || 'image';
324
+
325
+ // Handle different image data formats - extract src string directly
326
+ if (jsonData.image && jsonData.image.src) {
327
+ // Format: { "image": { "src": "data:image/png;base64,..." } }
328
+ uploadedImage = jsonData.image.src;
329
+ } else if (jsonData.uploadedImage && jsonData.uploadedImage.src) {
330
+ // Format: { "uploadedImage": { "src": "...", "name": "..." } }
331
+ uploadedImage = jsonData.uploadedImage.src;
332
+ } else if (jsonData.uploadedImage && typeof jsonData.uploadedImage === 'string') {
333
+ // Format: { "uploadedImage": "data:image/..." }
334
+ uploadedImage = jsonData.uploadedImage;
335
+ } else if (jsonData.backgroundImage) {
336
+ // Format: { "backgroundImage": "data:image/..." }
337
+ uploadedImage = jsonData.backgroundImage;
338
+ } else {
339
+ uploadedImage = null;
340
+ }
341
+ }
342
+
343
+ // Format 2: Legacy format with different structure
344
+ else if (jsonData.placement || jsonData.cameraPlacement) {
345
+ const placement = jsonData.placement || jsonData.cameraPlacement;
346
+
347
+ if (placement.cameras && Array.isArray(placement.cameras)) {
348
+ cameras = placement.cameras.map((camera, index) => ({
349
+ id: camera.id || Date.now() + index,
350
+ name: camera.name || `Camera ${index + 1}`,
351
+ customName: camera.customName || '',
352
+ x: camera.x || 0,
353
+ y: camera.y || 0,
354
+ direction: camera.direction || 0,
355
+ verkadaModel: camera.verkadaModel || null,
356
+ timestamp: new Date().toISOString(),
357
+ }));
358
+ }
359
+
360
+ projectName = placement.projectName || placement.name || 'Imported Legacy Project';
361
+ mode = placement.mode || 'image';
362
+
363
+ // Handle different image data formats - extract src string directly
364
+ if (placement.image && placement.image.src) {
365
+ uploadedImage = placement.image.src;
366
+ } else if (placement.image && typeof placement.image === 'string') {
367
+ uploadedImage = placement.image;
368
+ } else if (placement.backgroundImage) {
369
+ uploadedImage = placement.backgroundImage;
370
+ } else {
371
+ uploadedImage = null;
372
+ }
373
+ }
374
+
375
+ // Format 3: Simple array of cameras
376
+ else if (Array.isArray(jsonData)) {
377
+ cameras = jsonData.map((camera, index) => ({
378
+ id: camera.id || Date.now() + index,
379
+ name: camera.name || `Camera ${index + 1}`,
380
+ customName: camera.customName || '',
381
+ x: camera.x || 0,
382
+ y: camera.y || 0,
383
+ direction: camera.direction || 0,
384
+ verkadaModel: camera.verkadaModel || null,
385
+ timestamp: new Date().toISOString(),
386
+ }));
387
+
388
+ projectName = 'Imported Legacy Project';
389
+ mode = 'image';
390
+ }
391
+
392
+ // Format 4: Check for other common legacy formats
393
+ else {
394
+ // Look for any array that might contain camera data
395
+ const possibleCameraArrays = Object.values(jsonData).filter(value =>
396
+ Array.isArray(value) && value.length > 0 &&
397
+ value[0] && typeof value[0] === 'object'
398
+ );
399
+
400
+ if (possibleCameraArrays.length > 0) {
401
+ const cameraArray = possibleCameraArrays[0];
402
+ cameras = cameraArray.map((camera, index) => ({
403
+ id: camera.id || Date.now() + index,
404
+ name: camera.name || `Camera ${index + 1}`,
405
+ customName: camera.customName || '',
406
+ x: camera.x || 0,
407
+ y: camera.y || 0,
408
+ direction: camera.direction || 0,
409
+ verkadaModel: camera.verkadaModel || null,
410
+ timestamp: new Date().toISOString(),
411
+ }));
412
+
413
+ projectName = jsonData.name || jsonData.title || 'Imported Legacy Project';
414
+ mode = jsonData.mode || 'image';
415
+
416
+ // Handle different image data formats - extract src string directly
417
+ if (jsonData.image && jsonData.image.src) {
418
+ uploadedImage = jsonData.image.src;
419
+ } else if (jsonData.image && typeof jsonData.image === 'string') {
420
+ uploadedImage = jsonData.image;
421
+ } else if (jsonData.backgroundImage) {
422
+ uploadedImage = jsonData.backgroundImage;
423
+ } else {
424
+ uploadedImage = null;
425
+ }
426
+ } else {
427
+ throw new Error('No valid camera data found in JSON file. Expected arrays of camera objects.');
428
+ }
429
+ }
430
+
431
+ // Validate that we have at least some cameras
432
+ if (!cameras || cameras.length === 0) {
433
+ throw new Error('No cameras found in the JSON file');
434
+ }
435
+
436
+ // Validate camera data
437
+ const invalidCameras = cameras.filter(camera =>
438
+ typeof camera.x !== 'number' ||
439
+ typeof camera.y !== 'number' ||
440
+ isNaN(camera.x) ||
441
+ isNaN(camera.y)
442
+ );
443
+
444
+ if (invalidCameras.length > 0) {
445
+ throw new Error(`${invalidCameras.length} cameras have invalid position data (x, y coordinates must be numbers)`);
446
+ }
447
+
448
+ return {
449
+ success: true,
450
+ data: {
451
+ projectName: projectName,
452
+ mode: mode,
453
+ cameras: cameras,
454
+ uploadedImage: uploadedImage
455
+ }
456
+ };
457
+
458
+ } catch (error) {
459
+ return {
460
+ success: false,
461
+ error: error.message
462
+ };
463
+ }
464
+ }, []);
465
+
189
466
  // Initialize Mapbox GL map
190
467
  useEffect(() => {
191
468
  if (!mapContainer.current || mode !== 'map') return;
@@ -575,7 +852,7 @@ const CameraPlacement = ({ userProfile }) => {
575
852
  const loadProject = useCallback(async (lightweightProject) => {
576
853
  try {
577
854
  // Show loading indicator
578
- const loadingToast = Swal.fire({
855
+ Swal.fire({
579
856
  title: 'Loading Project...',
580
857
  text: 'Fetching project data',
581
858
  allowOutsideClick: false,
@@ -1194,6 +1471,12 @@ const CameraPlacement = ({ userProfile }) => {
1194
1471
  tooltip="Upload Image"
1195
1472
  className={styles.uploadBtn}
1196
1473
  />
1474
+ <IconButton
1475
+ icon={FolderOpen}
1476
+ onClick={() => jsonFileInputRef.current?.click()}
1477
+ tooltip="Import Legacy JSON File"
1478
+ className={styles.importBtn}
1479
+ />
1197
1480
  <IconButton
1198
1481
  icon={Save}
1199
1482
  onClick={saveToDatabase}
@@ -2289,7 +2572,7 @@ const CameraPlacement = ({ userProfile }) => {
2289
2572
  </div>
2290
2573
  </div>
2291
2574
 
2292
- {/* Hidden file input */}
2575
+ {/* Hidden file inputs */}
2293
2576
  <input
2294
2577
  type="file"
2295
2578
  ref={fileInputRef}
@@ -2297,6 +2580,13 @@ const CameraPlacement = ({ userProfile }) => {
2297
2580
  accept="image/*"
2298
2581
  style={{ display: 'none' }}
2299
2582
  />
2583
+ <input
2584
+ type="file"
2585
+ ref={jsonFileInputRef}
2586
+ onChange={handleJsonFileUpload}
2587
+ accept=".json,application/json"
2588
+ style={{ display: 'none' }}
2589
+ />
2300
2590
 
2301
2591
  {showModal && selectedCamera && (
2302
2592
  <CameraSelectionModal
@@ -1082,6 +1082,20 @@ const GenericReportImproved = ({
1082
1082
  exportUrl: '',
1083
1083
  });
1084
1084
 
1085
+ // State for unique field selection and row exclusion
1086
+ const [uniqueFieldConfig, setUniqueFieldConfig] = useState({
1087
+ enabled: false,
1088
+ selectedField: '',
1089
+ });
1090
+
1091
+ // Debug logging for uniqueFieldConfig changes
1092
+ useEffect(() => {
1093
+ console.log('🔍 [EFFECT] uniqueFieldConfig changed:', uniqueFieldConfig);
1094
+ }, [uniqueFieldConfig]);
1095
+ const [excludedRows, setExcludedRows] = useState(new Set());
1096
+ const [showRowExclusionModal, setShowRowExclusionModal] = useState(false);
1097
+ const [exportType, setExportType] = useState('regular');
1098
+
1085
1099
  // Note: Removed table search functionality to simplify user experience
1086
1100
 
1087
1101
  // Extract additional settings with defaults
@@ -2291,6 +2305,17 @@ const GenericReportImproved = ({
2291
2305
  let payload;
2292
2306
 
2293
2307
  if (isDynamicTemplate) {
2308
+ // Enhanced unique field configuration for grouped templates
2309
+ const groupedUniqueConfig = uniqueFieldConfig.enabled && uniqueFieldConfig.selectedField ? {
2310
+ enabled: true,
2311
+ field: uniqueFieldConfig.selectedField,
2312
+ distinct: true,
2313
+ distinctField: uniqueFieldConfig.selectedField
2314
+ } : {
2315
+ enabled: false,
2316
+ distinct: false
2317
+ };
2318
+
2294
2319
  // For dynamic templates, send the complete template configuration
2295
2320
  payload = {
2296
2321
  dynamic_template: template,
@@ -2299,7 +2324,16 @@ const GenericReportImproved = ({
2299
2324
  joins: joins,
2300
2325
  filters: flattenFilterCriteria(filterCriteria),
2301
2326
  sorting: sortCriteria,
2327
+ // Enhanced unique field configuration
2328
+ unique: groupedUniqueConfig,
2302
2329
  };
2330
+
2331
+ // Debug log for grouped reports
2332
+ if (uniqueFieldConfig.enabled) {
2333
+ console.group('🔍 [DEBUG] Grouped Report - Unique Field Configuration');
2334
+ console.log('Grouped unique config:', groupedUniqueConfig);
2335
+ console.groupEnd();
2336
+ }
2303
2337
  } else {
2304
2338
  // For predefined templates, use the existing approach
2305
2339
  payload = {
@@ -2337,6 +2371,28 @@ const GenericReportImproved = ({
2337
2371
  const page = Math.floor(skip / limit) + 1;
2338
2372
 
2339
2373
  // Use the same payload format as GenericReport (with query wrapper)
2374
+ // Debug the unique field state at query time
2375
+ console.group('🔍 [DEBUG] DataSource - Unique Field State Check');
2376
+ console.log('uniqueFieldConfig:', uniqueFieldConfig);
2377
+ console.log('uniqueFieldConfig.enabled:', uniqueFieldConfig.enabled);
2378
+ console.log('uniqueFieldConfig.selectedField:', uniqueFieldConfig.selectedField);
2379
+ console.log('Condition evaluation:', uniqueFieldConfig.enabled && uniqueFieldConfig.selectedField);
2380
+
2381
+ // Check DOM state vs React state
2382
+ const checkboxes = document.querySelectorAll('input[type="checkbox"]');
2383
+ console.log('🔍 [DEBUG] All checkboxes in DOM:', Array.from(checkboxes).map((cb, i) => `${i}: ${cb.checked}`));
2384
+ console.groupEnd();
2385
+
2386
+ const uniqueConfig = uniqueFieldConfig.enabled && uniqueFieldConfig.selectedField ? {
2387
+ enabled: true,
2388
+ field: uniqueFieldConfig.selectedField,
2389
+ distinct: true, // Explicit DISTINCT flag
2390
+ distinctField: uniqueFieldConfig.selectedField
2391
+ } : {
2392
+ enabled: false,
2393
+ distinct: false
2394
+ };
2395
+
2340
2396
  const queryConfig = {
2341
2397
  mainTable: selectedTable,
2342
2398
  columns: selectedColumns.map((col) => {
@@ -2368,8 +2424,20 @@ const GenericReportImproved = ({
2368
2424
  })),
2369
2425
  filters: flattenFilterCriteria(filterCriteria),
2370
2426
  sorting: sortCriteria,
2427
+ // Enhanced unique field configuration
2428
+ unique: uniqueConfig,
2371
2429
  };
2372
2430
 
2431
+ // Debug log for unique field configuration
2432
+ if (uniqueFieldConfig.enabled) {
2433
+ console.group('🔍 [DEBUG] Unique Field Configuration');
2434
+ console.log('Unique config enabled:', uniqueFieldConfig.enabled);
2435
+ console.log('Selected field:', uniqueFieldConfig.selectedField);
2436
+ console.log('Query unique config:', uniqueConfig);
2437
+ console.log('Expected SQL: SELECT DISTINCT', uniqueFieldConfig.selectedField, '...');
2438
+ console.groupEnd();
2439
+ }
2440
+
2373
2441
  // Add sorting from DataGrid if provided
2374
2442
  if (sortInfo) {
2375
2443
  queryConfig.sorting = [
@@ -2461,6 +2529,11 @@ const GenericReportImproved = ({
2461
2529
  executeUrl,
2462
2530
  setting,
2463
2531
  });
2532
+
2533
+ // CRITICAL DEBUG: Check uniqueFieldConfig state at execute time
2534
+ console.log('🚨 [CRITICAL] uniqueFieldConfig at executeReport start:', uniqueFieldConfig);
2535
+ console.log('🚨 [CRITICAL] uniqueFieldConfig.enabled:', uniqueFieldConfig.enabled);
2536
+ console.log('🚨 [CRITICAL] uniqueFieldConfig.selectedField:', uniqueFieldConfig.selectedField);
2464
2537
 
2465
2538
  if (!selectedTable || selectedColumns.length === 0) {
2466
2539
  toast.warning(
@@ -2885,6 +2958,12 @@ const GenericReportImproved = ({
2885
2958
  });
2886
2959
  }
2887
2960
 
2961
+ // Restore unique field configuration if it exists
2962
+ if (config.unique) {
2963
+ console.log('🔄 [RESTORE] Restoring unique field config:', config.unique);
2964
+ setUniqueFieldConfig(config.unique);
2965
+ }
2966
+
2888
2967
  if (config.filters) {
2889
2968
  // Ensure filters have the proper structure
2890
2969
  if (config.filters.groups) {
@@ -4181,6 +4260,96 @@ const GenericReportImproved = ({
4181
4260
  )}
4182
4261
  </div>
4183
4262
  )}
4263
+
4264
+ {/* Unique Field Configuration */}
4265
+ {selectedColumns.length > 0 && (
4266
+ <div className={styles.uniqueFieldConfig}>
4267
+ <div className={styles.configHeader}>
4268
+ <h4>
4269
+ <Filter size={16} />
4270
+ Remove Duplicates (Optional)
4271
+ </h4>
4272
+ <p>Use SQL DISTINCT to prevent duplicate rows based on a specific field</p>
4273
+ </div>
4274
+ <div className={styles.configContent}>
4275
+ <label className={styles.checkboxLabel}>
4276
+ <input
4277
+ type="checkbox"
4278
+ checked={uniqueFieldConfig.enabled}
4279
+ onChange={(e) => {
4280
+ console.log('🔍 [DEBUG] Unique field toggled:', e.target.checked);
4281
+ console.log('🔍 [DEBUG] Current uniqueFieldConfig before change:', uniqueFieldConfig);
4282
+ const newConfig = {
4283
+ ...uniqueFieldConfig,
4284
+ enabled: e.target.checked,
4285
+ selectedField: e.target.checked ? uniqueFieldConfig.selectedField : ''
4286
+ };
4287
+ console.log('🔍 [DEBUG] New unique field config:', newConfig);
4288
+ setUniqueFieldConfig(newConfig);
4289
+ // Log immediately after setState
4290
+ setTimeout(() => {
4291
+ console.log('🔍 [DEBUG] State after setTimeout:', uniqueFieldConfig);
4292
+ }, 100);
4293
+ }}
4294
+ />
4295
+ <span className={styles.checkboxText}>
4296
+ Enable DISTINCT filtering
4297
+ {uniqueFieldConfig.enabled && uniqueFieldConfig.selectedField && (
4298
+ <span className={styles.activeIndicator}>
4299
+ <CircleCheck size={12} />
4300
+ Active on {uniqueFieldConfig.selectedField}
4301
+ </span>
4302
+ )}
4303
+ </span>
4304
+ </label>
4305
+
4306
+ {uniqueFieldConfig.enabled && (
4307
+ <div className={styles.uniqueFieldSelector}>
4308
+ <label className={styles.fieldLabel}>
4309
+ Select field to make unique:
4310
+ </label>
4311
+ <select
4312
+ value={uniqueFieldConfig.selectedField}
4313
+ onChange={(e) => {
4314
+ console.log('🔍 [DEBUG] Unique field selected:', e.target.value);
4315
+ console.log('🔍 [DEBUG] Available dropdown options:', Array.from(e.target.options).map(opt => ({value: opt.value, text: opt.text})));
4316
+ console.log('🔍 [DEBUG] Selected option text:', e.target.options[e.target.selectedIndex]?.text);
4317
+ console.log('🔍 [DEBUG] Current uniqueFieldConfig before field change:', uniqueFieldConfig);
4318
+ console.log('🔍 [DEBUG] Current selectedColumns:', selectedColumns);
4319
+ const newConfig = {
4320
+ ...uniqueFieldConfig,
4321
+ selectedField: e.target.value
4322
+ };
4323
+ console.log('🔍 [DEBUG] Updated unique field config with selected field:', newConfig);
4324
+ setUniqueFieldConfig(newConfig);
4325
+ // Log immediately after setState
4326
+ setTimeout(() => {
4327
+ console.log('🔍 [DEBUG] Field state after setTimeout:', uniqueFieldConfig);
4328
+ }, 100);
4329
+ }}
4330
+ className={styles.fieldSelect}
4331
+ required
4332
+ >
4333
+ <option value="">Choose a field...</option>
4334
+ {selectedColumns.map((col) => (
4335
+ <option
4336
+ key={`${col.table}-${col.column}`}
4337
+ value={`${col.table}.${col.column}`}
4338
+ >
4339
+ {col.displayName || col.column} ({col.table})
4340
+ </option>
4341
+ ))}
4342
+ </select>
4343
+ <div className={styles.fieldHelp}>
4344
+ <strong>SQL Impact:</strong> This will add DISTINCT to your query based on the selected field.
4345
+ <br />
4346
+ <code>SELECT DISTINCT {uniqueFieldConfig.selectedField || '[field]'}, ... FROM {selectedTable}</code>
4347
+ </div>
4348
+ </div>
4349
+ )}
4350
+ </div>
4351
+ </div>
4352
+ )}
4184
4353
  </div>
4185
4354
  );
4186
4355
  };
@@ -6483,6 +6652,17 @@ const GenericReportImproved = ({
6483
6652
 
6484
6653
  try {
6485
6654
  // Use exact same payload format as GenericReport
6655
+ // Enhanced unique field configuration for exports
6656
+ const exportUniqueConfig = uniqueFieldConfig.enabled && uniqueFieldConfig.selectedField ? {
6657
+ enabled: true,
6658
+ field: uniqueFieldConfig.selectedField,
6659
+ distinct: true,
6660
+ distinctField: uniqueFieldConfig.selectedField
6661
+ } : {
6662
+ enabled: false,
6663
+ distinct: false
6664
+ };
6665
+
6486
6666
  const queryConfig = {
6487
6667
  mainTable: selectedTable,
6488
6668
  columns: selectedColumns.map((col) => ({
@@ -6503,8 +6683,21 @@ const GenericReportImproved = ({
6503
6683
  })),
6504
6684
  filters: flattenFilterCriteria(filterCriteria),
6505
6685
  sorting: sortCriteria,
6686
+ // Enhanced unique field configuration
6687
+ unique: exportUniqueConfig,
6688
+ // Add excluded rows for server-side filtering
6689
+ excludedRows: Array.from(excludedRows),
6506
6690
  };
6507
6691
 
6692
+ // Debug log for exports
6693
+ if (uniqueFieldConfig.enabled) {
6694
+ console.group('🔍 [DEBUG] Export - Unique Field Configuration');
6695
+ console.log('Export format:', format);
6696
+ console.log('Export unique config:', exportUniqueConfig);
6697
+ console.log('Query config:', queryConfig);
6698
+ console.groupEnd();
6699
+ }
6700
+
6508
6701
  const exportData = {
6509
6702
  query: queryConfig,
6510
6703
  format: format === 'excel' ? 'xlsx' : format, // Convert excel to xlsx
@@ -6663,23 +6856,78 @@ const GenericReportImproved = ({
6663
6856
  }
6664
6857
  };
6665
6858
 
6666
- // Handle grouped report export to Excel
6667
- const handleRegularExport = async () => {
6859
+ // Handle row exclusion modal
6860
+ const handleExportWithExclusion = (exportType = 'regular') => {
6668
6861
  if (previewData.length === 0) {
6669
6862
  toast.error('No data available to export');
6670
6863
  return;
6671
6864
  }
6672
6865
 
6866
+ setShowRowExclusionModal(true);
6867
+ setExportType(exportType);
6868
+ };
6869
+
6870
+ const confirmExportWithExclusion = async () => {
6871
+ setShowRowExclusionModal(false);
6872
+
6873
+ if (exportType === 'grouped') {
6874
+ await performGroupedExport();
6875
+ } else {
6876
+ await performRegularExport();
6877
+ }
6878
+ };
6879
+
6880
+ // Handle grouped report export to Excel
6881
+ const handleRegularExport = async () => {
6882
+ handleExportWithExclusion('regular');
6883
+ };
6884
+
6885
+ // Filter out excluded rows from preview data
6886
+ const getFilteredPreviewData = () => {
6887
+ if (excludedRows.size === 0) {
6888
+ return previewData;
6889
+ }
6890
+
6891
+ return previewData.filter((row, index) => {
6892
+ const rowId = row.id || `preview-row-${index}`;
6893
+ return !excludedRows.has(rowId);
6894
+ });
6895
+ };
6896
+
6897
+ // Perform the actual regular export
6898
+ const performRegularExport = async () => {
6899
+ const filteredData = getFilteredPreviewData();
6900
+
6901
+ if (filteredData.length === 0) {
6902
+ toast.error('No data available to export after exclusions');
6903
+ return;
6904
+ }
6905
+
6673
6906
  try {
6907
+ // Temporarily replace previewData with filtered data for export
6908
+ const originalPreviewData = previewData;
6909
+ setPreviewData(filteredData);
6910
+
6674
6911
  // Use the existing client-side Excel export functionality
6675
6912
  await exportReport('excel');
6913
+
6914
+ // Restore original data
6915
+ setPreviewData(originalPreviewData);
6676
6916
  } catch (error) {
6677
6917
  console.error('Regular export failed:', error);
6678
6918
  toast.error('Export failed. Please try again.');
6919
+
6920
+ // Restore original data on error as well
6921
+ setPreviewData(previewData);
6679
6922
  }
6680
6923
  };
6681
6924
 
6682
6925
  const handleGroupedExport = async () => {
6926
+ handleExportWithExclusion('grouped');
6927
+ };
6928
+
6929
+ // Perform the actual grouped export
6930
+ const performGroupedExport = async () => {
6683
6931
  if (!selectedTemplate) {
6684
6932
  toast.error('No template selected for export');
6685
6933
  return;
@@ -6830,9 +7078,17 @@ const GenericReportImproved = ({
6830
7078
  groupHeaderRow[0] = `${groupDisplayName}: ${group.groupDisplayName}`;
6831
7079
  worksheetData.push(groupHeaderRow);
6832
7080
 
6833
- // Add data rows for this group
7081
+ // Add data rows for this group (excluding excluded rows)
6834
7082
  const allRows = [...group.rows, ...(group.emptyRows || [])];
6835
- allRows.forEach((row) => {
7083
+ allRows.forEach((row, rowIndex) => {
7084
+ // Create a unique identifier for the row
7085
+ const rowId = row.id || `group-${groupIndex}-row-${rowIndex}`;
7086
+
7087
+ // Skip excluded rows
7088
+ if (excludedRows.has(rowId)) {
7089
+ return;
7090
+ }
7091
+
6836
7092
  const dataRow = selectedColumns.map((column) =>
6837
7093
  getCellValue(row, column)
6838
7094
  );
@@ -6977,6 +7233,7 @@ const GenericReportImproved = ({
6977
7233
  isGrouped: groupingConfig.enabled,
6978
7234
  customCalculatedFields: customCalculatedFields,
6979
7235
  customUrls: customUrls,
7236
+ unique: uniqueFieldConfig,
6980
7237
  },
6981
7238
  };
6982
7239
 
@@ -7400,7 +7657,7 @@ const GenericReportImproved = ({
7400
7657
  className={`${styles.btn} ${styles.btnPrimary}`}
7401
7658
  onClick={() => {
7402
7659
  resetPagination();
7403
- setHasAutoExecuted(false); // Reset flag for manual execution
7660
+ // Don't reset hasAutoExecuted to avoid triggering useEffect
7404
7661
  executeReport();
7405
7662
  }}
7406
7663
  disabled={isLoading}
@@ -8597,6 +8854,123 @@ const GenericReportImproved = ({
8597
8854
  </div>
8598
8855
  </div>
8599
8856
  )}
8857
+
8858
+ {/* Row Exclusion Modal */}
8859
+ {showRowExclusionModal && (
8860
+ <div className={styles.modalOverlay}>
8861
+ <div className={styles.rowExclusionModal}>
8862
+ <div className={styles.modalHeader}>
8863
+ <h3>Select Rows to Exclude</h3>
8864
+ <p>Choose which rows you don't want to include in the export</p>
8865
+ <button
8866
+ className={styles.modalCloseButton}
8867
+ onClick={() => setShowRowExclusionModal(false)}
8868
+ >
8869
+
8870
+ </button>
8871
+ </div>
8872
+
8873
+ <div className={styles.modalBody}>
8874
+ <div className={styles.rowExclusionList}>
8875
+ {(exportType === 'grouped' && groupedData?.groupedData?.groups ?
8876
+ groupedData.groupedData.groups.flatMap((group, groupIndex) =>
8877
+ [...group.rows, ...(group.emptyRows || [])].map((row, rowIndex) => {
8878
+ const rowId = row.id || `group-${groupIndex}-row-${rowIndex}`;
8879
+ const isExcluded = excludedRows.has(rowId);
8880
+
8881
+ return (
8882
+ <label key={rowId} className={styles.rowExclusionItem}>
8883
+ <input
8884
+ type="checkbox"
8885
+ checked={isExcluded}
8886
+ onChange={(e) => {
8887
+ const newExcludedRows = new Set(excludedRows);
8888
+ if (e.target.checked) {
8889
+ newExcludedRows.add(rowId);
8890
+ } else {
8891
+ newExcludedRows.delete(rowId);
8892
+ }
8893
+ setExcludedRows(newExcludedRows);
8894
+ }}
8895
+ />
8896
+ <div className={styles.rowPreview}>
8897
+ {selectedColumns.slice(0, 3).map((col) => {
8898
+ const value = row[col.displayName] || row[col.column] || row[`${col.table}_${col.column}`] || '';
8899
+ return (
8900
+ <span key={col.column} className={styles.rowValue}>
8901
+ {String(value).substring(0, 50)}{String(value).length > 50 ? '...' : ''}
8902
+ </span>
8903
+ );
8904
+ })}
8905
+ </div>
8906
+ </label>
8907
+ );
8908
+ })
8909
+ )
8910
+ :
8911
+ previewData.map((row, index) => {
8912
+ const rowId = row.id || `preview-row-${index}`;
8913
+ const isExcluded = excludedRows.has(rowId);
8914
+
8915
+ return (
8916
+ <label key={rowId} className={styles.rowExclusionItem}>
8917
+ <input
8918
+ type="checkbox"
8919
+ checked={isExcluded}
8920
+ onChange={(e) => {
8921
+ const newExcludedRows = new Set(excludedRows);
8922
+ if (e.target.checked) {
8923
+ newExcludedRows.add(rowId);
8924
+ } else {
8925
+ newExcludedRows.delete(rowId);
8926
+ }
8927
+ setExcludedRows(newExcludedRows);
8928
+ }}
8929
+ />
8930
+ <div className={styles.rowPreview}>
8931
+ {selectedColumns.slice(0, 3).map((col) => {
8932
+ const value = row[col.displayName] || row[col.column] || row[`${col.table}_${col.column}`] || '';
8933
+ return (
8934
+ <span key={col.column} className={styles.rowValue}>
8935
+ {String(value).substring(0, 50)}{String(value).length > 50 ? '...' : ''}
8936
+ </span>
8937
+ );
8938
+ })}
8939
+ </div>
8940
+ </label>
8941
+ );
8942
+ })
8943
+ )}
8944
+ </div>
8945
+
8946
+ <div className={styles.exclusionSummary}>
8947
+ <p>
8948
+ {excludedRows.size} of {(exportType === 'grouped' && groupedData?.groupedData?.groups ?
8949
+ groupedData.groupedData.groups.reduce((total, group) => total + group.rows.length + (group.emptyRows?.length || 0), 0)
8950
+ : previewData.length)} rows will be excluded from export
8951
+ </p>
8952
+ </div>
8953
+ </div>
8954
+
8955
+ <div className={styles.modalFooter}>
8956
+ <button
8957
+ className={styles.cancelButton}
8958
+ onClick={() => setShowRowExclusionModal(false)}
8959
+ >
8960
+ Cancel
8961
+ </button>
8962
+ <button
8963
+ className={styles.confirmButton}
8964
+ onClick={confirmExportWithExclusion}
8965
+ >
8966
+ Export ({(exportType === 'grouped' && groupedData?.groupedData?.groups ?
8967
+ groupedData.groupedData.groups.reduce((total, group) => total + group.rows.length + (group.emptyRows?.length || 0), 0) - excludedRows.size
8968
+ : previewData.length - excludedRows.size)} rows)
8969
+ </button>
8970
+ </div>
8971
+ </div>
8972
+ </div>
8973
+ )}
8600
8974
  </div>
8601
8975
  );
8602
8976
  };
@@ -18,7 +18,7 @@
18
18
  border-bottom: 1px solid rgba(var(--primary-rgb), 0.7);
19
19
  z-index: 100;
20
20
  position: relative;
21
- min-width: 1000px;
21
+ min-width: 1060px;
22
22
 
23
23
  .headerContent {
24
24
  display: flex;
@@ -46,7 +46,7 @@
46
46
  gap: 12px;
47
47
  align-items: center;
48
48
  flex-shrink: 0;
49
- min-width: 480px;
49
+ min-width: 540px;
50
50
  }
51
51
 
52
52
  .projectInput {
@@ -169,6 +169,17 @@
169
169
  }
170
170
  }
171
171
 
172
+ &.importBtn {
173
+ background: var(--warning-color, #f59e0b);
174
+ border-color: rgba(var(--warning-rgb, 245, 158, 11), 1);
175
+
176
+ &:hover:not(:disabled) {
177
+ background: rgba(var(--warning-rgb, 245, 158, 11), 0.8);
178
+ transform: translateY(-2px);
179
+ box-shadow: 0 4px 12px rgba(var(--warning-rgb, 245, 158, 11), 0.3);
180
+ }
181
+ }
182
+
172
183
  &.saveFileBtn {
173
184
  background: var(--secondary-color, #6b7280);
174
185
  border-color: rgba(var(--secondary-rgb, 107, 114, 128), 1);
@@ -3831,3 +3831,345 @@
3831
3831
  font-size: 0.9rem;
3832
3832
  }
3833
3833
  }
3834
+
3835
+ /* Unique Field Configuration Styling */
3836
+ .uniqueFieldConfig {
3837
+ margin-top: 24px;
3838
+ background-color: rgba(106, 90, 205, 0.05); // Subtle purple background
3839
+ border: 1px solid rgba(106, 90, 205, 0.15);
3840
+ border-radius: var(--radius);
3841
+ overflow: hidden;
3842
+ transition: all 0.3s ease;
3843
+
3844
+ &:hover {
3845
+ border-color: rgba(106, 90, 205, 0.25);
3846
+ box-shadow: 0 2px 8px rgba(106, 90, 205, 0.1);
3847
+ }
3848
+ }
3849
+
3850
+ .configHeader {
3851
+ padding: 16px 20px 12px 20px;
3852
+ background: linear-gradient(135deg, rgba(106, 90, 205, 0.08) 0%, rgba(106, 90, 205, 0.03) 100%);
3853
+ border-bottom: 1px solid rgba(106, 90, 205, 0.1);
3854
+
3855
+ h4 {
3856
+ margin: 0 0 6px 0;
3857
+ font-size: 1rem;
3858
+ font-weight: 600;
3859
+ color: #4c46a6;
3860
+ display: flex;
3861
+ align-items: center;
3862
+ gap: 8px;
3863
+
3864
+ svg {
3865
+ color: #6a5acd;
3866
+ flex-shrink: 0;
3867
+ }
3868
+
3869
+ &::before {
3870
+ content: '';
3871
+ width: 3px;
3872
+ height: 18px;
3873
+ background: linear-gradient(45deg, #6a5acd, #9370db);
3874
+ border-radius: 2px;
3875
+ }
3876
+ }
3877
+
3878
+ p {
3879
+ margin: 0;
3880
+ font-size: 0.875rem;
3881
+ color: var(--paragraph-color);
3882
+ opacity: 0.8;
3883
+ }
3884
+ }
3885
+
3886
+ .configContent {
3887
+ padding: 20px;
3888
+ }
3889
+
3890
+ .checkboxLabel {
3891
+ display: flex;
3892
+ align-items: flex-start;
3893
+ cursor: pointer;
3894
+ font-size: 0.875rem;
3895
+ color: var(--header-color);
3896
+ margin-bottom: 16px;
3897
+ padding: 12px;
3898
+ background-color: rgba(255, 255, 255, 0.3);
3899
+ border-radius: var(--radius);
3900
+ border: 1px solid rgba(106, 90, 205, 0.1);
3901
+ transition: all 0.2s ease;
3902
+
3903
+ &:hover {
3904
+ background-color: rgba(255, 255, 255, 0.5);
3905
+ border-color: rgba(106, 90, 205, 0.2);
3906
+ transform: translateY(-1px);
3907
+ }
3908
+
3909
+ input[type="checkbox"] {
3910
+ margin-right: 12px;
3911
+ width: 18px;
3912
+ height: 18px;
3913
+ cursor: pointer;
3914
+ accent-color: #6a5acd;
3915
+ margin-top: 1px; // Align with text
3916
+ flex-shrink: 0;
3917
+ }
3918
+ }
3919
+
3920
+ .checkboxText {
3921
+ font-weight: 500;
3922
+ color: var(--header-color);
3923
+ line-height: 1.4;
3924
+
3925
+ .activeIndicator {
3926
+ display: inline-flex;
3927
+ align-items: center;
3928
+ gap: 4px;
3929
+ margin-left: 8px;
3930
+ padding: 2px 8px;
3931
+ background-color: rgba(40, 167, 69, 0.1);
3932
+ color: #28a745;
3933
+ font-size: 0.75rem;
3934
+ font-weight: 600;
3935
+ border-radius: 12px;
3936
+ border: 1px solid rgba(40, 167, 69, 0.2);
3937
+
3938
+ svg {
3939
+ color: #28a745;
3940
+ flex-shrink: 0;
3941
+ }
3942
+ }
3943
+ }
3944
+
3945
+ .uniqueFieldSelector {
3946
+ margin-left: 30px; // Align with checkbox text
3947
+ padding-left: 16px;
3948
+ border-left: 2px solid rgba(106, 90, 205, 0.2);
3949
+ animation: slideDown 0.3s ease-out;
3950
+ }
3951
+
3952
+ @keyframes slideDown {
3953
+ from {
3954
+ opacity: 0;
3955
+ transform: translateY(-10px);
3956
+ }
3957
+ to {
3958
+ opacity: 1;
3959
+ transform: translateY(0);
3960
+ }
3961
+ }
3962
+
3963
+ .fieldLabel {
3964
+ display: block;
3965
+ font-size: 0.875rem;
3966
+ font-weight: 500;
3967
+ color: var(--header-color);
3968
+ margin-bottom: 8px;
3969
+ }
3970
+
3971
+ .fieldSelect {
3972
+ width: 100%;
3973
+ padding: 12px 16px;
3974
+ border: 2px solid rgba(106, 90, 205, 0.2);
3975
+ border-radius: var(--radius);
3976
+ font-size: 0.875rem;
3977
+ background-color: var(--bg-color);
3978
+ color: var(--header-color);
3979
+ transition: all 0.2s ease;
3980
+ cursor: pointer;
3981
+
3982
+ &:focus {
3983
+ outline: none;
3984
+ border-color: #6a5acd;
3985
+ box-shadow: 0 0 0 3px rgba(106, 90, 205, 0.15);
3986
+ }
3987
+
3988
+ &:hover {
3989
+ border-color: rgba(106, 90, 205, 0.3);
3990
+ }
3991
+
3992
+ option {
3993
+ padding: 8px;
3994
+ color: var(--header-color);
3995
+ background-color: var(--bg-color);
3996
+ }
3997
+ }
3998
+
3999
+ .fieldHelp {
4000
+ margin-top: 8px;
4001
+ font-size: 0.75rem;
4002
+ color: var(--paragraph-color);
4003
+ line-height: 1.4;
4004
+ padding: 8px 12px;
4005
+ background-color: rgba(106, 90, 205, 0.05);
4006
+ border-radius: var(--radius);
4007
+ border-left: 3px solid rgba(106, 90, 205, 0.3);
4008
+
4009
+ code {
4010
+ display: block;
4011
+ margin-top: 6px;
4012
+ padding: 6px 8px;
4013
+ background-color: rgba(0, 0, 0, 0.05);
4014
+ border-radius: 4px;
4015
+ font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
4016
+ font-size: 0.7rem;
4017
+ color: #4c46a6;
4018
+ border: 1px solid rgba(106, 90, 205, 0.15);
4019
+ white-space: pre-wrap;
4020
+ word-break: break-word;
4021
+ }
4022
+
4023
+ strong {
4024
+ color: var(--header-color);
4025
+ font-weight: 600;
4026
+ }
4027
+ }
4028
+
4029
+ /* Row Exclusion Modal Styling */
4030
+ .rowExclusionModal {
4031
+ background-color: var(--bg-color);
4032
+ border-radius: var(--radius);
4033
+ box-shadow: 0 20px 40px rgba(0, 0, 0, 0.15);
4034
+ max-width: 800px;
4035
+ width: 90vw;
4036
+ max-height: 80vh;
4037
+ display: flex;
4038
+ flex-direction: column;
4039
+
4040
+ .modalHeader {
4041
+ padding: 24px;
4042
+ border-bottom: 1px solid var(--border-color);
4043
+ background: linear-gradient(135deg, rgba(220, 38, 127, 0.05) 0%, rgba(220, 38, 127, 0.02) 100%);
4044
+
4045
+ h3 {
4046
+ margin: 0 0 8px 0;
4047
+ font-size: 1.25rem;
4048
+ color: var(--header-color);
4049
+ font-weight: 600;
4050
+ }
4051
+
4052
+ p {
4053
+ margin: 0;
4054
+ color: var(--paragraph-color);
4055
+ font-size: 0.875rem;
4056
+ }
4057
+ }
4058
+
4059
+ .modalBody {
4060
+ flex: 1;
4061
+ overflow: hidden;
4062
+ display: flex;
4063
+ flex-direction: column;
4064
+ }
4065
+
4066
+ .rowExclusionList {
4067
+ flex: 1;
4068
+ overflow-y: auto;
4069
+ padding: 16px 24px;
4070
+ max-height: 400px;
4071
+ }
4072
+
4073
+ .rowExclusionItem {
4074
+ display: flex;
4075
+ align-items: center;
4076
+ padding: 12px;
4077
+ margin-bottom: 8px;
4078
+ border-radius: var(--radius);
4079
+ border: 1px solid var(--border-color);
4080
+ background-color: var(--tertiary-color);
4081
+ cursor: pointer;
4082
+ transition: all 0.2s ease;
4083
+
4084
+ &:hover {
4085
+ background-color: rgba(var(--primary-rgb), 0.05);
4086
+ border-color: rgba(var(--primary-rgb), 0.2);
4087
+ transform: translateX(4px);
4088
+ }
4089
+
4090
+ input[type="checkbox"] {
4091
+ margin-right: 12px;
4092
+ width: 16px;
4093
+ height: 16px;
4094
+ cursor: pointer;
4095
+ accent-color: var(--primary-color);
4096
+ flex-shrink: 0;
4097
+ }
4098
+ }
4099
+
4100
+ .rowPreview {
4101
+ display: flex;
4102
+ flex-direction: column;
4103
+ gap: 4px;
4104
+ flex: 1;
4105
+ min-width: 0;
4106
+ }
4107
+
4108
+ .rowValue {
4109
+ font-size: 0.875rem;
4110
+ color: var(--header-color);
4111
+ white-space: nowrap;
4112
+ overflow: hidden;
4113
+ text-overflow: ellipsis;
4114
+
4115
+ &:not(:first-child) {
4116
+ color: var(--paragraph-color);
4117
+ font-size: 0.8rem;
4118
+ }
4119
+ }
4120
+
4121
+ .exclusionSummary {
4122
+ padding: 16px 24px;
4123
+ background-color: rgba(var(--primary-rgb), 0.05);
4124
+ border-top: 1px solid var(--border-color);
4125
+
4126
+ p {
4127
+ margin: 0;
4128
+ font-size: 0.875rem;
4129
+ color: var(--primary-color);
4130
+ text-align: center;
4131
+ font-weight: 500;
4132
+ }
4133
+ }
4134
+
4135
+ .modalFooter {
4136
+ padding: 16px 24px;
4137
+ border-top: 1px solid var(--border-color);
4138
+ display: flex;
4139
+ gap: 12px;
4140
+ justify-content: flex-end;
4141
+ background-color: var(--tertiary-color);
4142
+
4143
+ button {
4144
+ padding: 10px 20px;
4145
+ border-radius: var(--radius);
4146
+ font-size: 0.875rem;
4147
+ font-weight: 500;
4148
+ cursor: pointer;
4149
+ transition: all 0.2s ease;
4150
+ border: 1px solid transparent;
4151
+
4152
+ &.cancelButton {
4153
+ background-color: transparent;
4154
+ color: var(--paragraph-color);
4155
+ border-color: var(--border-color);
4156
+
4157
+ &:hover {
4158
+ background-color: var(--border-color);
4159
+ color: var(--header-color);
4160
+ }
4161
+ }
4162
+
4163
+ &.confirmButton {
4164
+ background-color: var(--primary-color);
4165
+ color: white;
4166
+
4167
+ &:hover {
4168
+ background-color: var(--primary-hover, var(--primary-color));
4169
+ transform: translateY(-1px);
4170
+ box-shadow: 0 4px 12px rgba(var(--primary-rgb), 0.3);
4171
+ }
4172
+ }
4173
+ }
4174
+ }
4175
+ }