@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 +860 -11
- package/package.json +1 -1
- package/src/components/crm/AssociationManager.jsx +151 -33
- package/src/components/crm/Field.jsx +27 -0
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
|
|
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
|
-
|
|
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
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
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 (
|
|
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]:
|
|
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
|
-
{
|
|
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
|
|
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
|
|
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) =>
|
|
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) =>
|
|
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
|
-
|
|
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) =>
|
|
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) =>
|
|
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) =>
|
|
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 = {};
|