@visns-studio/visns-components 5.11.4 → 5.11.5

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
@@ -951,6 +951,766 @@ File upload component with drag and drop support.
951
951
  />
952
952
  ```
953
953
 
954
+ ### Association Management Components
955
+
956
+ #### AssociationManager
957
+
958
+ A sophisticated component for managing entity relationships and associations within your application. The AssociationManager provides an intuitive interface for creating, viewing, and managing complex many-to-many relationships between different data entities.
959
+
960
+ ```jsx
961
+ <AssociationManager
962
+ config={{
963
+ title: "Client Associations",
964
+ description: "Manage relationships between clients and projects",
965
+ primaryEntity: {
966
+ id: "clients",
967
+ label: "Clients",
968
+ displayKey: "name",
969
+ fetchUrl: "/api/clients"
970
+ },
971
+ secondaryEntity: {
972
+ id: "projects",
973
+ label: "Projects",
974
+ displayKey: "title",
975
+ fetchUrl: "/api/projects"
976
+ },
977
+ association: {
978
+ fetchUrl: "/api/client-project-associations",
979
+ createUrl: "/api/client-project-associations",
980
+ deleteUrl: "/api/client-project-associations",
981
+ pivotTable: "client_project"
982
+ },
983
+ permissions: {
984
+ canCreate: true,
985
+ canDelete: true,
986
+ canView: true
987
+ },
988
+ ui: {
989
+ searchPlaceholder: "Search clients or projects...",
990
+ emptyStateMessage: "No associations found",
991
+ confirmDeleteMessage: "Are you sure you want to remove this association?"
992
+ }
993
+ }}
994
+ userProfile={userProfile}
995
+ onAssociationChange={(data) => console.log('Association changed:', data)}
996
+ />
997
+ ```
998
+
999
+ **Key Features:**
1000
+
1001
+ 1. **Bidirectional Relationship Management**:
1002
+ - Create associations from either entity perspective
1003
+ - Automatic relationship synchronization
1004
+ - Real-time updates across related views
1005
+
1006
+ 2. **Advanced Search and Filtering**:
1007
+ - Multi-entity search capabilities
1008
+ - Filter by entity type, status, or custom attributes
1009
+ - Debounced search to optimize performance
1010
+
1011
+ 3. **Bulk Operations**:
1012
+ - Select multiple entities for batch association
1013
+ - Bulk delete with confirmation dialogs
1014
+ - Mass import/export functionality
1015
+
1016
+ 4. **Visual Association Mapping**:
1017
+ - Interactive relationship visualization
1018
+ - Drag-and-drop association creation
1019
+ - Color-coded entity types
1020
+
1021
+ 5. **Permission-Based Access Control**:
1022
+ - Granular permissions for create, read, update, delete
1023
+ - Role-based access to specific entity types
1024
+ - Custom permission validation
1025
+
1026
+ **Configuration Options:**
1027
+
1028
+ ```jsx
1029
+ const associationConfig = {
1030
+ // Required: Basic entity configuration
1031
+ primaryEntity: {
1032
+ id: "clients", // Unique identifier
1033
+ label: "Clients", // Display name
1034
+ displayKey: "name", // Field to show in lists
1035
+ fetchUrl: "/api/clients", // API endpoint
1036
+ searchFields: ["name", "email"], // Fields to search
1037
+ icon: "Users", // Lucide icon name
1038
+ color: "#3b82f6" // Brand color
1039
+ },
1040
+
1041
+ secondaryEntity: {
1042
+ id: "projects",
1043
+ label: "Projects",
1044
+ displayKey: "title",
1045
+ fetchUrl: "/api/projects",
1046
+ searchFields: ["title", "description"],
1047
+ icon: "FolderOpen",
1048
+ color: "#10b981"
1049
+ },
1050
+
1051
+ // Association configuration
1052
+ association: {
1053
+ fetchUrl: "/api/associations", // Get existing associations
1054
+ createUrl: "/api/associations", // Create new associations
1055
+ updateUrl: "/api/associations", // Update associations (optional)
1056
+ deleteUrl: "/api/associations", // Delete associations
1057
+ pivotTable: "client_project", // Database table name
1058
+
1059
+ // Additional pivot data
1060
+ pivotFields: [
1061
+ {
1062
+ name: "role",
1063
+ label: "Role",
1064
+ type: "select",
1065
+ options: [
1066
+ { value: "manager", label: "Manager" },
1067
+ { value: "contributor", label: "Contributor" }
1068
+ ]
1069
+ },
1070
+ {
1071
+ name: "start_date",
1072
+ label: "Start Date",
1073
+ type: "date"
1074
+ }
1075
+ ]
1076
+ },
1077
+
1078
+ // UI customization
1079
+ ui: {
1080
+ layout: "split", // "split", "tabs", "cards"
1081
+ theme: "default", // "default", "compact", "detailed"
1082
+ showEntityCounts: true, // Show association counts
1083
+ showLastModified: true, // Show modification timestamps
1084
+ enableDragDrop: true, // Enable drag-and-drop
1085
+ confirmActions: true, // Show confirmation dialogs
1086
+
1087
+ // Custom labels and messages
1088
+ labels: {
1089
+ createButton: "Add Association",
1090
+ deleteButton: "Remove",
1091
+ searchPlaceholder: "Search entities...",
1092
+ noResultsMessage: "No entities found",
1093
+ loadingMessage: "Loading associations..."
1094
+ }
1095
+ },
1096
+
1097
+ // Validation rules
1098
+ validation: {
1099
+ preventDuplicates: true, // Prevent duplicate associations
1100
+ requirePivotData: false, // Require additional pivot fields
1101
+ customValidation: (primary, secondary, pivotData) => {
1102
+ // Custom validation logic
1103
+ return { valid: true, message: "" };
1104
+ }
1105
+ },
1106
+
1107
+ // Performance optimization
1108
+ performance: {
1109
+ enableVirtualization: true, // For large datasets
1110
+ pageSize: 50, // Pagination size
1111
+ debounceMs: 300, // Search debounce
1112
+ cacheResults: true // Cache API responses
1113
+ }
1114
+ };
1115
+ ```
1116
+
1117
+ **API Integration:**
1118
+
1119
+ The AssociationManager expects specific API response formats:
1120
+
1121
+ ```javascript
1122
+ // GET /api/clients (Primary Entity)
1123
+ {
1124
+ "data": [
1125
+ {
1126
+ "id": 1,
1127
+ "name": "Acme Corp",
1128
+ "email": "contact@acme.com",
1129
+ "created_at": "2023-01-01T00:00:00Z"
1130
+ }
1131
+ ],
1132
+ "total": 150,
1133
+ "page": 1,
1134
+ "per_page": 50
1135
+ }
1136
+
1137
+ // GET /api/associations (Existing Associations)
1138
+ {
1139
+ "data": [
1140
+ {
1141
+ "id": 1,
1142
+ "primary_id": 1,
1143
+ "secondary_id": 5,
1144
+ "pivot_data": {
1145
+ "role": "manager",
1146
+ "start_date": "2023-01-01"
1147
+ },
1148
+ "primary": {
1149
+ "id": 1,
1150
+ "name": "Acme Corp"
1151
+ },
1152
+ "secondary": {
1153
+ "id": 5,
1154
+ "title": "Website Redesign"
1155
+ },
1156
+ "created_at": "2023-01-01T00:00:00Z"
1157
+ }
1158
+ ]
1159
+ }
1160
+
1161
+ // POST /api/associations (Create Association)
1162
+ {
1163
+ "primary_id": 1,
1164
+ "secondary_id": 5,
1165
+ "pivot_data": {
1166
+ "role": "manager",
1167
+ "start_date": "2023-01-01"
1168
+ }
1169
+ }
1170
+
1171
+ // Response:
1172
+ {
1173
+ "success": true,
1174
+ "data": {
1175
+ "id": 1,
1176
+ "primary_id": 1,
1177
+ "secondary_id": 5,
1178
+ "created_at": "2023-01-01T00:00:00Z"
1179
+ },
1180
+ "message": "Association created successfully"
1181
+ }
1182
+ ```
1183
+
1184
+ **Event Handling:**
1185
+
1186
+ ```jsx
1187
+ <AssociationManager
1188
+ config={associationConfig}
1189
+ onAssociationChange={(event) => {
1190
+ // event.type: 'created', 'updated', 'deleted'
1191
+ // event.data: Association data
1192
+ // event.entities: Affected entities
1193
+ console.log('Association event:', event);
1194
+ }}
1195
+ onEntitySelect={(entity, type) => {
1196
+ // Handle entity selection
1197
+ console.log('Entity selected:', entity, type);
1198
+ }}
1199
+ onError={(error) => {
1200
+ // Handle errors
1201
+ console.error('Association error:', error);
1202
+ }}
1203
+ />
1204
+ ```
1205
+
1206
+ **Advanced Usage Examples:**
1207
+
1208
+ 1. **Client-Project Association with Roles**:
1209
+ ```jsx
1210
+ // Managing client assignments to projects with specific roles
1211
+ <AssociationManager
1212
+ config={{
1213
+ primaryEntity: { id: "clients", label: "Clients", ... },
1214
+ secondaryEntity: { id: "projects", label: "Projects", ... },
1215
+ association: {
1216
+ pivotFields: [
1217
+ {
1218
+ name: "role",
1219
+ label: "Role in Project",
1220
+ type: "select",
1221
+ required: true,
1222
+ options: [
1223
+ { value: "sponsor", label: "Project Sponsor" },
1224
+ { value: "stakeholder", label: "Key Stakeholder" },
1225
+ { value: "user", label: "End User" }
1226
+ ]
1227
+ }
1228
+ ]
1229
+ }
1230
+ }}
1231
+ />
1232
+ ```
1233
+
1234
+ 2. **User-Permission Association**:
1235
+ ```jsx
1236
+ // Managing user permissions with expiration dates
1237
+ <AssociationManager
1238
+ config={{
1239
+ primaryEntity: { id: "users", label: "Users", ... },
1240
+ secondaryEntity: { id: "permissions", label: "Permissions", ... },
1241
+ association: {
1242
+ pivotFields: [
1243
+ {
1244
+ name: "expires_at",
1245
+ label: "Expiration Date",
1246
+ type: "datetime",
1247
+ required: false
1248
+ },
1249
+ {
1250
+ name: "granted_by",
1251
+ label: "Granted By",
1252
+ type: "select",
1253
+ fetchUrl: "/api/users/managers"
1254
+ }
1255
+ ]
1256
+ }
1257
+ }}
1258
+ />
1259
+ ```
1260
+
1261
+ ### Business Card OCR Component
1262
+
1263
+ #### BusinessCardOcr (Enhanced v5.11.1+)
1264
+
1265
+ An advanced OCR (Optical Character Recognition) component specifically designed for scanning business cards and automatically extracting contact information. The component provides intelligent text recognition, client matching, and seamless contact management integration.
1266
+
1267
+ ```jsx
1268
+ <BusinessCardOcr
1269
+ isOpen={showScanner}
1270
+ onClose={() => setShowScanner(false)}
1271
+ onContactSaved={(contact) => {
1272
+ console.log('Contact saved:', contact);
1273
+ // Handle the saved contact data
1274
+ }}
1275
+ fields={[
1276
+ { id: 'name', label: 'Full Name', required: true },
1277
+ { id: 'email', label: 'Email', type: 'email' },
1278
+ { id: 'phone', label: 'Phone', type: 'tel' },
1279
+ { id: 'company', label: 'Company' },
1280
+ { id: 'position', label: 'Position' },
1281
+ { id: 'address', label: 'Address', type: 'textarea' }
1282
+ ]}
1283
+ fetchConfig={{
1284
+ ocrUrl: '/api/ocr/business-card',
1285
+ saveUrl: '/api/contacts',
1286
+ clientSearchUrl: '/api/clients/search',
1287
+ duplicateCheckUrl: '/api/contacts/check-duplicate'
1288
+ }}
1289
+ settings={{
1290
+ enableClientMatching: true,
1291
+ enableDuplicateCheck: true,
1292
+ autoSave: false,
1293
+ imageQuality: 0.8,
1294
+ supportedFormats: ['image/jpeg', 'image/png', 'image/webp']
1295
+ }}
1296
+ />
1297
+ ```
1298
+
1299
+ **Key Features:**
1300
+
1301
+ 1. **Advanced OCR Processing**:
1302
+ - High-accuracy text extraction from business card images
1303
+ - Intelligent field mapping and recognition
1304
+ - Support for multiple languages and formats
1305
+ - Automatic rotation and perspective correction
1306
+
1307
+ 2. **Smart Client Matching**:
1308
+ - Automatic company name recognition
1309
+ - Fuzzy matching against existing client database
1310
+ - Confidence scoring for match suggestions
1311
+ - Manual override and confirmation options
1312
+
1313
+ 3. **Duplicate Detection**:
1314
+ - Email and phone number duplicate checking
1315
+ - Visual comparison with existing contacts
1316
+ - Merge or create new contact options
1317
+ - Conflict resolution interface
1318
+
1319
+ 4. **Interactive Editing Interface**:
1320
+ - Real-time field editing and validation
1321
+ - Drag-and-drop image upload
1322
+ - Live camera capture (mobile devices)
1323
+ - Field confidence indicators
1324
+
1325
+ 5. **Contact Management Integration**:
1326
+ - Direct save to contact database
1327
+ - Custom field mapping and validation
1328
+ - Bulk import capabilities
1329
+ - Export to various formats
1330
+
1331
+ **Configuration Options:**
1332
+
1333
+ ```jsx
1334
+ const businessCardConfig = {
1335
+ // Field definitions for extracted data
1336
+ fields: [
1337
+ {
1338
+ id: 'name',
1339
+ label: 'Full Name',
1340
+ type: 'text',
1341
+ required: true,
1342
+ validation: {
1343
+ minLength: 2,
1344
+ pattern: /^[a-zA-Z\s]+$/
1345
+ },
1346
+ ocrMapping: ['name', 'full_name', 'contact_name']
1347
+ },
1348
+ {
1349
+ id: 'email',
1350
+ label: 'Email Address',
1351
+ type: 'email',
1352
+ required: false,
1353
+ validation: {
1354
+ pattern: /^[^\s@]+@[^\s@]+\.[^\s@]+$/
1355
+ },
1356
+ ocrMapping: ['email', 'e_mail', 'email_address']
1357
+ },
1358
+ {
1359
+ id: 'phone',
1360
+ label: 'Phone Number',
1361
+ type: 'tel',
1362
+ required: false,
1363
+ formatting: {
1364
+ mask: '(000) 000-0000',
1365
+ international: true
1366
+ },
1367
+ ocrMapping: ['phone', 'mobile', 'telephone', 'cell']
1368
+ },
1369
+ {
1370
+ id: 'company',
1371
+ label: 'Company Name',
1372
+ type: 'text',
1373
+ required: false,
1374
+ clientMatching: true, // Enable client matching for this field
1375
+ ocrMapping: ['company', 'organization', 'business_name']
1376
+ },
1377
+ {
1378
+ id: 'position',
1379
+ label: 'Job Title',
1380
+ type: 'text',
1381
+ required: false,
1382
+ ocrMapping: ['title', 'position', 'job_title', 'role']
1383
+ },
1384
+ {
1385
+ id: 'address',
1386
+ label: 'Address',
1387
+ type: 'textarea',
1388
+ required: false,
1389
+ formatting: {
1390
+ multiline: true,
1391
+ rows: 3
1392
+ },
1393
+ ocrMapping: ['address', 'location', 'street_address']
1394
+ },
1395
+ {
1396
+ id: 'website',
1397
+ label: 'Website',
1398
+ type: 'url',
1399
+ required: false,
1400
+ validation: {
1401
+ pattern: /^https?:\/\/.+/
1402
+ },
1403
+ ocrMapping: ['website', 'url', 'web', 'site']
1404
+ }
1405
+ ],
1406
+
1407
+ // API endpoint configuration
1408
+ fetchConfig: {
1409
+ ocrUrl: '/api/ocr/business-card', // OCR processing endpoint
1410
+ saveUrl: '/api/contacts', // Contact save endpoint
1411
+ clientSearchUrl: '/api/clients/search', // Client matching endpoint
1412
+ duplicateCheckUrl: '/api/contacts/check', // Duplicate check endpoint
1413
+ uploadUrl: '/api/files/upload', // Image upload endpoint
1414
+
1415
+ // Request configuration
1416
+ timeout: 30000, // 30 second timeout
1417
+ retries: 3, // Retry failed requests
1418
+ headers: {
1419
+ 'Accept': 'application/json',
1420
+ 'X-Requested-With': 'XMLHttpRequest'
1421
+ }
1422
+ },
1423
+
1424
+ // Component behavior settings
1425
+ settings: {
1426
+ // OCR settings
1427
+ imageQuality: 0.8, // Image compression quality
1428
+ maxImageSize: 5242880, // 5MB max file size
1429
+ supportedFormats: [ // Accepted image formats
1430
+ 'image/jpeg',
1431
+ 'image/png',
1432
+ 'image/webp',
1433
+ 'image/bmp'
1434
+ ],
1435
+
1436
+ // Processing options
1437
+ enableClientMatching: true, // Enable company matching
1438
+ enableDuplicateCheck: true, // Check for duplicates
1439
+ autoSave: false, // Auto-save after OCR
1440
+ requireConfirmation: true, // Confirm before saving
1441
+
1442
+ // UI options
1443
+ showConfidenceScores: true, // Show OCR confidence
1444
+ enableManualEdit: true, // Allow manual editing
1445
+ showImagePreview: true, // Show uploaded image
1446
+ enableCameraCapture: true, // Enable camera on mobile
1447
+
1448
+ // Validation options
1449
+ validateOnBlur: true, // Validate fields on blur
1450
+ highlightErrors: true, // Highlight validation errors
1451
+ showTooltips: true, // Show field tooltips
1452
+
1453
+ // Performance options
1454
+ debounceMs: 500, // Debounce for auto-save
1455
+ cacheResults: true, // Cache OCR results
1456
+ enablePreprocessing: true // Image preprocessing
1457
+ },
1458
+
1459
+ // UI customization
1460
+ ui: {
1461
+ theme: 'default', // 'default', 'compact', 'minimal'
1462
+ layout: 'modal', // 'modal', 'inline', 'drawer'
1463
+ size: 'large', // 'small', 'medium', 'large'
1464
+
1465
+ // Labels and messages
1466
+ labels: {
1467
+ uploadButton: 'Upload Business Card',
1468
+ scanButton: 'Scan with Camera',
1469
+ saveButton: 'Save Contact',
1470
+ cancelButton: 'Cancel',
1471
+ editButton: 'Edit Information',
1472
+ processButton: 'Process Image'
1473
+ },
1474
+
1475
+ messages: {
1476
+ uploadPrompt: 'Drop an image here or click to upload',
1477
+ processing: 'Processing business card...',
1478
+ ocrSuccess: 'Text extracted successfully',
1479
+ ocrError: 'Failed to process image',
1480
+ saveSuccess: 'Contact saved successfully',
1481
+ duplicateFound: 'Duplicate contact detected'
1482
+ },
1483
+
1484
+ // Style customization
1485
+ styles: {
1486
+ primaryColor: '#3b82f6',
1487
+ successColor: '#10b981',
1488
+ errorColor: '#ef4444',
1489
+ borderRadius: '8px',
1490
+ boxShadow: '0 4px 6px rgba(0, 0, 0, 0.1)'
1491
+ }
1492
+ }
1493
+ };
1494
+ ```
1495
+
1496
+ **API Integration:**
1497
+
1498
+ The BusinessCardOcr component expects specific API response formats:
1499
+
1500
+ ```javascript
1501
+ // POST /api/ocr/business-card (OCR Processing)
1502
+ // Request: FormData with image file
1503
+ {
1504
+ "image": File
1505
+ }
1506
+
1507
+ // Response:
1508
+ {
1509
+ "success": true,
1510
+ "data": {
1511
+ "extracted_text": "John Smith\nSenior Developer\nAcme Corp\nj.smith@acme.com\n(555) 123-4567",
1512
+ "fields": {
1513
+ "name": {
1514
+ "value": "John Smith",
1515
+ "confidence": 0.95,
1516
+ "position": { "x": 100, "y": 50, "width": 200, "height": 30 }
1517
+ },
1518
+ "email": {
1519
+ "value": "j.smith@acme.com",
1520
+ "confidence": 0.92,
1521
+ "position": { "x": 100, "y": 120, "width": 250, "height": 25 }
1522
+ },
1523
+ "company": {
1524
+ "value": "Acme Corp",
1525
+ "confidence": 0.88,
1526
+ "position": { "x": 100, "y": 80, "width": 150, "height": 25 }
1527
+ }
1528
+ },
1529
+ "processing_time": 2.3,
1530
+ "image_url": "/storage/uploads/card_123.jpg"
1531
+ }
1532
+ }
1533
+
1534
+ // GET /api/clients/search (Client Matching)
1535
+ {
1536
+ "query": "Acme Corp"
1537
+ }
1538
+
1539
+ // Response:
1540
+ {
1541
+ "data": [
1542
+ {
1543
+ "id": 5,
1544
+ "name": "Acme Corporation",
1545
+ "match_score": 0.85,
1546
+ "address": "123 Business St",
1547
+ "phone": "(555) 987-6543",
1548
+ "website": "https://acme.com"
1549
+ }
1550
+ ]
1551
+ }
1552
+
1553
+ // POST /api/contacts/check (Duplicate Check)
1554
+ {
1555
+ "email": "j.smith@acme.com",
1556
+ "phone": "(555) 123-4567"
1557
+ }
1558
+
1559
+ // Response:
1560
+ {
1561
+ "duplicates": [
1562
+ {
1563
+ "id": 10,
1564
+ "name": "John Smith",
1565
+ "email": "j.smith@acme.com",
1566
+ "match_type": "email",
1567
+ "match_score": 1.0,
1568
+ "created_at": "2023-01-01T00:00:00Z"
1569
+ }
1570
+ ]
1571
+ }
1572
+
1573
+ // POST /api/contacts (Save Contact)
1574
+ {
1575
+ "name": "John Smith",
1576
+ "email": "j.smith@acme.com",
1577
+ "phone": "(555) 123-4567",
1578
+ "company": "Acme Corp",
1579
+ "position": "Senior Developer",
1580
+ "client_id": 5,
1581
+ "source": "business_card_ocr",
1582
+ "confidence_scores": {
1583
+ "name": 0.95,
1584
+ "email": 0.92,
1585
+ "company": 0.88
1586
+ }
1587
+ }
1588
+
1589
+ // Response:
1590
+ {
1591
+ "success": true,
1592
+ "data": {
1593
+ "id": 15,
1594
+ "name": "John Smith",
1595
+ "created_at": "2023-01-01T00:00:00Z"
1596
+ },
1597
+ "message": "Contact created successfully"
1598
+ }
1599
+ ```
1600
+
1601
+ **Event Handling:**
1602
+
1603
+ ```jsx
1604
+ <BusinessCardOcr
1605
+ isOpen={showScanner}
1606
+ onClose={() => setShowScanner(false)}
1607
+ onContactSaved={(contact) => {
1608
+ // Handle successful contact save
1609
+ console.log('New contact saved:', contact);
1610
+
1611
+ // Optional: Update parent component state
1612
+ setContacts(prev => [...prev, contact]);
1613
+
1614
+ // Optional: Show success notification
1615
+ toast.success(`Contact ${contact.name} saved successfully`);
1616
+ }}
1617
+ onOcrComplete={(extractedData) => {
1618
+ // Handle OCR completion
1619
+ console.log('OCR extraction completed:', extractedData);
1620
+
1621
+ // Optional: Custom processing of extracted data
1622
+ if (extractedData.confidence < 0.7) {
1623
+ toast.warning('Low confidence OCR results. Please review carefully.');
1624
+ }
1625
+ }}
1626
+ onClientMatch={(matches) => {
1627
+ // Handle client matching results
1628
+ console.log('Client matches found:', matches);
1629
+
1630
+ // Optional: Auto-select high confidence matches
1631
+ if (matches.length === 1 && matches[0].match_score > 0.9) {
1632
+ // Auto-select the best match
1633
+ return matches[0];
1634
+ }
1635
+ }}
1636
+ onDuplicateDetected={(duplicates) => {
1637
+ // Handle duplicate detection
1638
+ console.log('Duplicate contacts found:', duplicates);
1639
+
1640
+ // Optional: Custom duplicate handling logic
1641
+ return new Promise((resolve) => {
1642
+ // Show custom duplicate resolution UI
1643
+ showDuplicateDialog(duplicates, resolve);
1644
+ });
1645
+ }}
1646
+ onError={(error) => {
1647
+ // Handle errors
1648
+ console.error('OCR error:', error);
1649
+
1650
+ // Optional: Custom error handling
1651
+ if (error.type === 'network') {
1652
+ toast.error('Network error. Please check your connection.');
1653
+ } else if (error.type === 'validation') {
1654
+ toast.error('Please correct the highlighted fields.');
1655
+ }
1656
+ }}
1657
+ />
1658
+ ```
1659
+
1660
+ **Advanced Usage Examples:**
1661
+
1662
+ 1. **Integration with Existing Contact Form**:
1663
+ ```jsx
1664
+ const ContactForm = () => {
1665
+ const [showOcr, setShowOcr] = useState(false);
1666
+ const [formData, setFormData] = useState({});
1667
+
1668
+ return (
1669
+ <div>
1670
+ <Form data={formData} onChange={setFormData}>
1671
+ {/* Regular form fields */}
1672
+ </Form>
1673
+
1674
+ <button onClick={() => setShowOcr(true)}>
1675
+ Scan Business Card
1676
+ </button>
1677
+
1678
+ <BusinessCardOcr
1679
+ isOpen={showOcr}
1680
+ onClose={() => setShowOcr(false)}
1681
+ onContactSaved={(contact) => {
1682
+ // Pre-fill form with OCR data
1683
+ setFormData(contact);
1684
+ setShowOcr(false);
1685
+ }}
1686
+ />
1687
+ </div>
1688
+ );
1689
+ };
1690
+ ```
1691
+
1692
+ 2. **Bulk Business Card Processing**:
1693
+ ```jsx
1694
+ const BulkCardProcessor = () => {
1695
+ const [queue, setQueue] = useState([]);
1696
+
1697
+ return (
1698
+ <BusinessCardOcr
1699
+ settings={{
1700
+ autoSave: true,
1701
+ enableBulkMode: true
1702
+ }}
1703
+ onContactSaved={(contact) => {
1704
+ setQueue(prev => [...prev, contact]);
1705
+ }}
1706
+ onBulkComplete={(contacts) => {
1707
+ console.log(`Processed ${contacts.length} business cards`);
1708
+ }}
1709
+ />
1710
+ );
1711
+ };
1712
+ ```
1713
+
954
1714
  ### Utility Components
955
1715
 
956
1716
  #### CustomFetch
@@ -1309,9 +2069,9 @@ Response:
1309
2069
 
1310
2070
  ### Report Builder Component
1311
2071
 
1312
- #### GenericReport
2072
+ #### GenericReport (Enhanced v5.10.11+)
1313
2073
 
1314
- A powerful report building component that allows users to create custom reports by selecting tables, columns, and configuring joins, filters, and sorting.
2074
+ A powerful and enhanced report building component that provides an intuitive wizard-based interface for creating custom reports. The component has been significantly improved with new features and better user experience.
1315
2075
 
1316
2076
  ```jsx
1317
2077
  <GenericReport
@@ -1322,19 +2082,108 @@ A powerful report building component that allows users to create custom reports
1322
2082
  executeUrl: '/ajax/reportBuilder/execute',
1323
2083
  suggestedJoinsUrl: '/ajax/reportBuilder/getSuggestedJoins',
1324
2084
  }}
2085
+ userProfile={userProfile}
2086
+ businessTemplates={businessTemplates} // Optional pre-built templates
1325
2087
  />
1326
2088
  ```
1327
2089
 
1328
- The GenericReport component provides these features:
2090
+ **Major Enhancements:**
2091
+
2092
+ - **5-Step Wizard Interface**: Guided workflow with visual progress indicators
2093
+ - **Business Templates**: Pre-built report templates for common business scenarios
2094
+ - **Calculated Fields**: Create custom formulas and calculations within reports
2095
+ - **Smart Column Selection**: Automatic suggestions based on table relationships
2096
+ - **Enhanced Filtering**: Advanced filter options with AND/OR logic and grouping
2097
+ - **Live Preview**: Real-time preview of report data as you build
2098
+ - **Intelligent Currency Formatting**: Automatic detection and formatting of monetary fields
2099
+ - **Expandable Field Summary**: View selected fields in a collapsible overview
2100
+ - **Template Management**: Save, load, and share report templates
2101
+ - **Multiple Export Formats**: Export to Excel, CSV, PDF with formatting options
2102
+
2103
+ **New Features in Recent Updates:**
2104
+
2105
+ 1. **Calculated Fields (v5.10.12+)**:
2106
+ ```jsx
2107
+ // Users can create custom calculated fields
2108
+ {
2109
+ id: 'profit_margin',
2110
+ displayName: 'Profit Margin %',
2111
+ formula: 'ROUND(((revenue - cost) / revenue) * 100, 2)',
2112
+ type: 'decimal'
2113
+ }
2114
+ ```
2115
+
2116
+ 2. **Business Templates (v5.11.0+)**:
2117
+ ```jsx
2118
+ // Pre-built templates for common scenarios
2119
+ const businessTemplates = [
2120
+ {
2121
+ id: 'utilization_planning',
2122
+ name: 'Utilization Planning',
2123
+ description: 'Weekly resource allocation and laydown utilization',
2124
+ icon: 'Calendar',
2125
+ mainTable: 'leads',
2126
+ defaultColumns: [...],
2127
+ calculatedFields: [...]
2128
+ }
2129
+ ];
2130
+ ```
2131
+
2132
+ 3. **Smart Currency Detection**:
2133
+ - Automatically formats fields containing 'value', 'price', 'cost', 'amount', etc.
2134
+ - Supports Australian Dollar (AUD) formatting by default
2135
+ - Configurable currency formatting options
2136
+
2137
+ 4. **Enhanced UI/UX**:
2138
+ - Lucide React icons throughout for consistency
2139
+ - Larger calculated field display for better visibility
2140
+ - Expandable selected fields summary
2141
+ - Step-by-step guidance with tooltips and help text
2142
+
2143
+ **The 5-Step Wizard Process:**
2144
+
2145
+ 1. **Select Data Source**: Choose main table and review business templates
2146
+ 2. **Choose Columns**: Select specific data fields with drag-and-drop reordering
2147
+ 3. **Add Relationships**: Connect related tables with automatic join suggestions
2148
+ 4. **Apply Filters**: Set conditions and date ranges with advanced logic
2149
+ 5. **Generate & Export**: Preview results and export in multiple formats
2150
+
2151
+ **API Integration:**
2152
+
2153
+ The component expects these endpoint responses:
2154
+
2155
+ ```javascript
2156
+ // Tables endpoint
2157
+ {
2158
+ "tables": [
2159
+ {
2160
+ "id": "clients",
2161
+ "label": "Clients",
2162
+ "description": "Customer information"
2163
+ }
2164
+ ]
2165
+ }
1329
2166
 
1330
- - **Table Selection**: Choose a main table for the report
1331
- - **Column Selection**: Select columns to include in the report
1332
- - **Table Joins**: Add related tables with automatic relationship detection
1333
- - **Filtering**: Apply filters to the report data
1334
- - **Sorting**: Sort the report results
1335
- - **Report Preview**: View a preview of the report data
1336
- - **Save/Load Reports**: Save reports for future use and load saved reports
1337
- - **Export**: Export reports in various formats
2167
+ // Columns endpoint
2168
+ {
2169
+ "columns": [
2170
+ {
2171
+ "name": "id",
2172
+ "type": "integer",
2173
+ "nullable": false,
2174
+ "key": "primary"
2175
+ }
2176
+ ]
2177
+ }
2178
+
2179
+ // Execute endpoint
2180
+ {
2181
+ "success": true,
2182
+ "data": [...],
2183
+ "total": 150,
2184
+ "query": "SELECT ... FROM ..."
2185
+ }
2186
+ ```
1338
2187
 
1339
2188
  ### Sketch Field Component
1340
2189
 
package/package.json CHANGED
@@ -87,7 +87,7 @@
87
87
  "react-dom": "^17.0.0 || ^18.0.0"
88
88
  },
89
89
  "name": "@visns-studio/visns-components",
90
- "version": "5.11.4",
90
+ "version": "5.11.5",
91
91
  "description": "Various packages to assist in the development of our Custom Applications.",
92
92
  "main": "src/index.js",
93
93
  "files": [
@@ -13,6 +13,7 @@ const AssociationManager = ({
13
13
  }) => {
14
14
  const [associations, setAssociations] = useState([]);
15
15
  const [availableOptions, setAvailableOptions] = useState([]);
16
+ const [childOptions, setChildOptions] = useState({});
16
17
  const [loading, setLoading] = useState(true);
17
18
 
18
19
  // Dynamic state based on config
@@ -49,23 +50,42 @@ const AssociationManager = ({
49
50
 
50
51
  // Load existing associations
51
52
  const loadAssociations = async () => {
52
- if (!endpoints.list) return;
53
+ console.log('🔍 AssociationManager - loadAssociations called');
54
+ console.log('📡 Endpoints:', endpoints);
55
+ console.log('⚙️ Config:', config);
56
+ console.log('🆔 DataId:', dataId);
57
+
58
+ if (!endpoints.list) {
59
+ console.error('❌ No endpoints.list defined');
60
+ return;
61
+ }
53
62
 
54
63
  try {
55
64
  setLoading(true);
56
65
  const url = processTemplate(endpoints.list);
66
+ console.log('🌐 Loading associations from URL:', url);
67
+
57
68
  const method = endpoints.listMethod || 'GET';
69
+ console.log('📤 HTTP Method:', method);
70
+
58
71
  const response = await CustomFetch(url, method, endpoints.listParams || {});
72
+ console.log('📥 Raw API Response:', response);
59
73
 
60
74
  const dataPath = config.dataPath || 'data';
75
+ console.log('🗂️ Using dataPath:', dataPath);
76
+
61
77
  const associationsData = dataPath.split('.').reduce((obj, key) => obj?.[key], response);
78
+ console.log('📊 Extracted associations data:', associationsData);
62
79
 
63
80
  // Ensure associationsData is always an array
64
81
  const validAssociations = Array.isArray(associationsData) ? associationsData : [];
82
+ console.log('✅ Valid associations (final):', validAssociations);
83
+ console.log('📈 Associations count:', validAssociations.length);
84
+
65
85
  setAssociations(validAssociations);
66
86
  } catch (error) {
67
87
  toast.error(config.messages?.loadError || 'Failed to load associations');
68
- console.error('Error loading associations:', error);
88
+ console.error('Error loading associations:', error);
69
89
  setAssociations([]); // Set empty array on error
70
90
  } finally {
71
91
  setLoading(false);
@@ -74,20 +94,102 @@ const AssociationManager = ({
74
94
 
75
95
  // Load available options for dropdown/select fields
76
96
  const loadAvailableOptions = async () => {
77
- if (!config.optionsEndpoint) return;
97
+ console.log('🔍 AssociationManager - loadAvailableOptions called');
98
+ console.log('⚙️ Config.optionsEndpoint:', config.optionsEndpoint);
99
+
100
+ if (!config.optionsEndpoint) {
101
+ console.error('❌ No config.optionsEndpoint defined');
102
+ return;
103
+ }
78
104
 
79
105
  try {
80
106
  const url = processTemplate(config.optionsEndpoint.url);
107
+ console.log('🌐 Loading options from URL:', url);
108
+
81
109
  const method = config.optionsEndpoint.method || 'POST';
110
+ console.log('📤 HTTP Method:', method);
111
+
82
112
  const params = config.optionsEndpoint.params || {};
113
+ console.log('📋 Request params:', params);
83
114
 
84
115
  const response = await CustomFetch(url, method, params);
116
+ console.log('📥 Raw options API Response:', response);
117
+
85
118
  const dataPath = config.optionsEndpoint.dataPath || 'data';
119
+ console.log('🗂️ Using options dataPath:', dataPath);
120
+
86
121
  const optionsData = dataPath.split('.').reduce((obj, key) => obj?.[key], response) || [];
122
+ console.log('📊 Extracted options data:', optionsData);
123
+ console.log('📈 Options count:', optionsData.length);
87
124
 
88
125
  setAvailableOptions(optionsData);
89
126
  } catch (error) {
90
- console.error('Failed to load options:', error);
127
+ console.error('Failed to load options:', error);
128
+ }
129
+ };
130
+
131
+ // Load child dropdown options based on parent selection
132
+ const loadChildOptions = async (parentFieldId, parentValue, childConfig) => {
133
+ if (!parentValue || !childConfig.url) return [];
134
+
135
+ try {
136
+ console.log(`🔍 Loading child options for ${childConfig.id} based on ${parentFieldId}:`, parentValue);
137
+
138
+ const filter = {
139
+ where: [
140
+ {
141
+ id: parentFieldId,
142
+ value: parentValue,
143
+ ...(childConfig.whereHas && { whereHas: childConfig.whereHas })
144
+ }
145
+ ]
146
+ };
147
+
148
+ if (childConfig.fields) {
149
+ filter.fields = childConfig.fields;
150
+ }
151
+
152
+ console.log('📋 Child filter params:', filter);
153
+
154
+ const response = await CustomFetch(childConfig.url, 'POST', filter);
155
+ console.log('📥 Child options response:', response);
156
+
157
+ // Extract child options - response structure is {data: {data: [...]}}
158
+ const childData = response.data?.data || response.data || [];
159
+ console.log('📊 Extracted child options:', childData);
160
+
161
+ return childData;
162
+ } catch (error) {
163
+ console.error(`❌ Failed to load child options for ${childConfig.id}:`, error);
164
+ return [];
165
+ }
166
+ };
167
+
168
+ // Handle parent field changes and load child options
169
+ const handleFieldChange = async (fieldId, value, field) => {
170
+ // Update the field value
171
+ setNewAssociation(prev => ({ ...prev, [fieldId]: value }));
172
+
173
+ // Load child options if this field has children
174
+ if (field.child && Array.isArray(field.child)) {
175
+ console.log(`🔗 Parent field ${fieldId} changed, loading child options...`);
176
+
177
+ const newChildOptions = { ...childOptions };
178
+
179
+ for (const childConfig of field.child) {
180
+ const childFieldOptions = await loadChildOptions(fieldId, value?.value || value, childConfig);
181
+ newChildOptions[childConfig.id] = childFieldOptions;
182
+ console.log(`✅ Loaded ${childFieldOptions.length} options for ${childConfig.id}`);
183
+ }
184
+
185
+ setChildOptions(newChildOptions);
186
+
187
+ // Clear child field values when parent changes
188
+ const updatedAssociation = { ...newAssociation, [fieldId]: value };
189
+ field.child.forEach(childConfig => {
190
+ updatedAssociation[childConfig.id] = '';
191
+ });
192
+ setNewAssociation(updatedAssociation);
91
193
  }
92
194
  };
93
195
 
@@ -164,21 +266,29 @@ const AssociationManager = ({
164
266
  };
165
267
 
166
268
  // Set primary handler
167
- const handleSetPrimary = async (itemId) => {
269
+ const handleSetPrimary = async (association) => {
168
270
  if (!endpoints.setPrimary) return;
169
271
 
170
272
  const idField = config.primaryTargetField || config.deleteField || 'id';
273
+ const targetValue = association[idField] || association.customer_id || association.id;
171
274
 
172
- await handleAction('setPrimary', { [idField]: itemId });
275
+ console.log('🌟 Setting primary for association:', association);
276
+ console.log('🎯 Using field:', idField, 'with value:', targetValue);
277
+
278
+ await handleAction('setPrimary', { [idField]: targetValue });
173
279
  };
174
280
 
175
281
  // Remove primary handler
176
- const handleRemovePrimary = async (itemId) => {
282
+ const handleRemovePrimary = async (association) => {
177
283
  if (!endpoints.removePrimary) return;
178
284
 
179
285
  const idField = config.primaryTargetField || config.deleteField || 'id';
286
+ const targetValue = association[idField] || association.customer_id || association.id;
287
+
288
+ console.log('🌟 Removing primary for association:', association);
289
+ console.log('🎯 Using field:', idField, 'with value:', targetValue);
180
290
 
181
- await handleAction('removePrimary', { [idField]: itemId });
291
+ await handleAction('removePrimary', { [idField]: targetValue });
182
292
  };
183
293
 
184
294
  // Clear all primary statuses (useful after merge)
@@ -195,9 +305,17 @@ const AssociationManager = ({
195
305
  };
196
306
 
197
307
  useEffect(() => {
308
+ console.log('🚀 AssociationManager - Component initialized');
309
+ console.log('🆔 DataId:', dataId);
310
+ console.log('📡 Endpoints:', endpoints);
311
+ console.log('⚙️ Config:', config);
312
+
198
313
  if (dataId) {
314
+ console.log('✅ DataId exists, loading data...');
199
315
  loadAssociations();
200
316
  loadAvailableOptions();
317
+ } else {
318
+ console.warn('⚠️ No dataId provided, skipping data load');
201
319
  }
202
320
  }, [dataId]);
203
321
 
@@ -245,21 +363,31 @@ const AssociationManager = ({
245
363
  ) : (
246
364
  <div className={styles.associationsList}>
247
365
  {associations.map((association) => {
366
+ console.log('🔍 Rendering association:', association);
367
+
248
368
  const nameField = config.displayFields?.name || 'name';
249
369
  const primaryField = config.displayFields?.primary || 'is_primary';
250
370
  const relationshipField = config.displayFields?.relationship || 'relationship_type';
251
371
  const notesField = config.displayFields?.notes || 'notes';
252
372
  const dateField = config.displayFields?.date || 'associated_at';
253
373
 
374
+ console.log('📊 Field values:', {
375
+ name: association[nameField],
376
+ primary: association[primaryField],
377
+ relationship: association[relationshipField],
378
+ notes: association[notesField],
379
+ date: association[dateField]
380
+ });
381
+
254
382
  return (
255
383
  <div
256
384
  key={association.id}
257
- className={`${styles.associationItem} ${association[primaryField] ? styles.primary : ''}`}
385
+ className={`${styles.associationItem} ${Boolean(association[primaryField]) ? styles.primary : ''}`}
258
386
  >
259
387
  <div className={styles.associationInfo}>
260
388
  <div className={styles.entityName}>
261
389
  {association[nameField]}
262
- {association[primaryField] && (
390
+ {Boolean(association[primaryField]) && (
263
391
  <span className={styles.primaryBadge}>
264
392
  <Star size={12} style={{ marginRight: '4px' }} />
265
393
  {config.labels?.primaryBadge || 'Primary'}
@@ -288,21 +416,22 @@ const AssociationManager = ({
288
416
  )}
289
417
  </div>
290
418
  <div className={styles.associationActions}>
291
- {!association[primaryField] && endpoints.setPrimary && (
419
+ {console.log('🎯 Actions for:', association[nameField], 'Primary:', association[primaryField], 'Boolean:', Boolean(association[primaryField]))}
420
+ {!Boolean(association[primaryField]) && endpoints.setPrimary && (
292
421
  <button
293
422
  type="button"
294
423
  className={styles.setPrimaryBtn}
295
- onClick={() => handleSetPrimary(association.id)}
424
+ onClick={() => handleSetPrimary(association)}
296
425
  >
297
426
  <Star size={14} style={{ marginRight: '4px' }} />
298
427
  {config.labels?.setPrimary || 'Set Primary'}
299
428
  </button>
300
429
  )}
301
- {association[primaryField] && endpoints.removePrimary && (
430
+ {Boolean(association[primaryField]) && endpoints.removePrimary && (
302
431
  <button
303
432
  type="button"
304
433
  className={styles.removePrimaryBtn}
305
- onClick={() => handleRemovePrimary(association.id)}
434
+ onClick={() => handleRemovePrimary(association)}
306
435
  title={config.labels?.removePrimaryTooltip || 'Remove primary status from this association'}
307
436
  >
308
437
  <X size={14} style={{ marginRight: '4px' }} />
@@ -343,9 +472,9 @@ const AssociationManager = ({
343
472
  <MultiSelect
344
473
  settings={{ id: field.id }}
345
474
  multi={field.multi || false}
346
- onChange={(value) => setNewAssociation(prev => ({ ...prev, [field.id]: value }))}
475
+ onChange={(value) => handleFieldChange(field.id, value, field)}
347
476
  inputValue={fieldValue}
348
- options={availableOptions}
477
+ options={field.id === 'customer_id' ? availableOptions : (childOptions[field.id] || [])}
349
478
  placeholder={field.placeholder || `Select ${field.label}...`}
350
479
  />
351
480
  )}
@@ -353,13 +482,11 @@ const AssociationManager = ({
353
482
  {field.type === 'select' && (
354
483
  <select
355
484
  value={fieldValue}
356
- onChange={(e) => setNewAssociation(prev => ({
357
- ...prev,
358
- [field.id]: e.target.value
359
- }))}
485
+ onChange={(e) => handleFieldChange(field.id, e.target.value, field)}
360
486
  className={styles.selectField}
361
487
  >
362
- {field.options && field.options.map(option => (
488
+ <option value="">Select {field.label}...</option>
489
+ {(field.options || childOptions[field.id] || []).map(option => (
363
490
  <option key={option.value || option} value={option.value || option}>
364
491
  {option.label || option}
365
492
  </option>
@@ -372,10 +499,7 @@ const AssociationManager = ({
372
499
  <input
373
500
  type="checkbox"
374
501
  checked={fieldValue}
375
- onChange={(e) => setNewAssociation(prev => ({
376
- ...prev,
377
- [field.id]: e.target.checked
378
- }))}
502
+ onChange={(e) => handleFieldChange(field.id, e.target.checked, field)}
379
503
  />
380
504
  {field.checkboxLabel || field.label}
381
505
  </label>
@@ -384,10 +508,7 @@ const AssociationManager = ({
384
508
  {field.type === 'textarea' && (
385
509
  <textarea
386
510
  value={fieldValue}
387
- onChange={(e) => setNewAssociation(prev => ({
388
- ...prev,
389
- [field.id]: e.target.value
390
- }))}
511
+ onChange={(e) => handleFieldChange(field.id, e.target.value, field)}
391
512
  placeholder={field.placeholder || ''}
392
513
  className={styles.textareaField}
393
514
  rows={field.rows || 3}
@@ -398,10 +519,7 @@ const AssociationManager = ({
398
519
  <input
399
520
  type="text"
400
521
  value={fieldValue}
401
- onChange={(e) => setNewAssociation(prev => ({
402
- ...prev,
403
- [field.id]: e.target.value
404
- }))}
522
+ onChange={(e) => handleFieldChange(field.id, e.target.value, field)}
405
523
  placeholder={field.placeholder || ''}
406
524
  className={styles.textField}
407
525
  />
@@ -157,6 +157,33 @@ function Field({
157
157
  fetchAutoValue();
158
158
  }, [settings.url, settings.type, settings.id, settings.autoValueKey]);
159
159
 
160
+ // Handle profileId parameter - auto-populate field value with userProfile.id
161
+ useEffect(() => {
162
+ if (
163
+ settings.param === 'profileId' &&
164
+ (!settings.value || settings.value === '') &&
165
+ userProfile?.id &&
166
+ (!inputValue || inputValue === '')
167
+ ) {
168
+ // Simulate an onChange event to update the form data with userProfile.id
169
+ const simulatedEvent = {
170
+ target: {
171
+ dataset: { name: settings.id },
172
+ value: userProfile.id,
173
+ },
174
+ };
175
+ onChange(simulatedEvent);
176
+
177
+ // Also directly update the form data to ensure it's synchronized
178
+ if (setFormData) {
179
+ setFormData((prevState) => ({
180
+ ...prevState,
181
+ [settings.id]: userProfile.id,
182
+ }));
183
+ }
184
+ }
185
+ }, [settings.param, settings.value, settings.id, userProfile?.id, inputValue, onChange, setFormData]);
186
+
160
187
  useEffect(() => {
161
188
  const fetchOptions = () => {
162
189
  let filter = {};