@visns-studio/visns-components 5.11.3 → 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/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.3",
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
  />
@@ -18,6 +18,8 @@ import {
18
18
  Zap,
19
19
  ZapOff,
20
20
  Focus,
21
+ Bot,
22
+ Play,
21
23
  } from 'lucide-react';
22
24
 
23
25
  import CustomFetch from './Fetch';
@@ -50,10 +52,20 @@ const BusinessCardOcr = ({
50
52
  const [contactDecision, setContactDecision] = useState(null); // 'create', 'update', 'merge'
51
53
  const [isConsulting, setIsConsulting] = useState(false);
52
54
  const [isSaving, setIsSaving] = useState(false);
55
+
56
+ // Auto-capture states
57
+ const [autoCapture, setAutoCapture] = useState(true);
58
+ const [focusScore, setFocusScore] = useState(0);
59
+ const [isCountingDown, setIsCountingDown] = useState(false);
60
+ const [countdown, setCountdown] = useState(0);
61
+ const [focusThreshold] = useState(0.15); // Adjustable focus threshold
53
62
 
54
63
  const videoRef = useRef(null);
55
64
  const canvasRef = useRef(null);
56
65
  const fileInputRef = useRef(null);
66
+ const focusDetectionRef = useRef(null);
67
+ const countdownRef = useRef(null);
68
+ const capturePhotoRef = useRef(null);
57
69
 
58
70
  // Detect available cameras when component opens
59
71
  useEffect(() => {
@@ -203,6 +215,121 @@ const BusinessCardOcr = ({
203
215
  }
204
216
  };
205
217
 
218
+ // Calculate image sharpness/focus score using Laplacian variance
219
+ const calculateFocusScore = useCallback((videoElement) => {
220
+ if (!videoElement || videoElement.videoWidth === 0) return 0;
221
+
222
+ const canvas = document.createElement('canvas');
223
+ const ctx = canvas.getContext('2d');
224
+
225
+ // Use smaller canvas for performance
226
+ const width = 320;
227
+ const height = 240;
228
+ canvas.width = width;
229
+ canvas.height = height;
230
+
231
+ ctx.drawImage(videoElement, 0, 0, width, height);
232
+ const imageData = ctx.getImageData(0, 0, width, height);
233
+ const data = imageData.data;
234
+
235
+ // Convert to grayscale and apply Laplacian operator
236
+ let sum = 0;
237
+ let count = 0;
238
+
239
+ for (let y = 1; y < height - 1; y++) {
240
+ for (let x = 1; x < width - 1; x++) {
241
+ const i = (y * width + x) * 4;
242
+
243
+ // Convert to grayscale
244
+ const gray = data[i] * 0.299 + data[i + 1] * 0.587 + data[i + 2] * 0.114;
245
+
246
+ // Apply Laplacian operator (edge detection)
247
+ const laplacian = Math.abs(
248
+ -4 * gray +
249
+ data[((y - 1) * width + x) * 4] * 0.299 + data[((y - 1) * width + x) * 4 + 1] * 0.587 + data[((y - 1) * width + x) * 4 + 2] * 0.114 +
250
+ data[((y + 1) * width + x) * 4] * 0.299 + data[((y + 1) * width + x) * 4 + 1] * 0.587 + data[((y + 1) * width + x) * 4 + 2] * 0.114 +
251
+ data[(y * width + (x - 1)) * 4] * 0.299 + data[(y * width + (x - 1)) * 4 + 1] * 0.587 + data[(y * width + (x - 1)) * 4 + 2] * 0.114 +
252
+ data[(y * width + (x + 1)) * 4] * 0.299 + data[(y * width + (x + 1)) * 4 + 1] * 0.587 + data[(y * width + (x + 1)) * 4 + 2] * 0.114
253
+ );
254
+
255
+ sum += laplacian * laplacian;
256
+ count++;
257
+ }
258
+ }
259
+
260
+ // Return normalized variance (higher = sharper)
261
+ return count > 0 ? Math.sqrt(sum / count) / 255 : 0;
262
+ }, []);
263
+
264
+ // Auto-capture countdown logic
265
+ const startCountdown = useCallback(async () => {
266
+ if (isCountingDown || isFocusing) return;
267
+
268
+ setIsCountingDown(true);
269
+
270
+ for (let i = 3; i > 0; i--) {
271
+ setCountdown(i);
272
+ await new Promise(resolve => {
273
+ countdownRef.current = setTimeout(resolve, 1000);
274
+ });
275
+
276
+ // Check if still focused during countdown
277
+ if (videoRef.current) {
278
+ const currentFocus = calculateFocusScore(videoRef.current);
279
+ if (currentFocus < focusThreshold) {
280
+ setIsCountingDown(false);
281
+ setCountdown(0);
282
+ return; // Cancel if focus lost
283
+ }
284
+ }
285
+ }
286
+
287
+ setCountdown(0);
288
+ setIsCountingDown(false);
289
+
290
+ // Trigger capture using ref
291
+ if (capturePhotoRef.current) {
292
+ await capturePhotoRef.current();
293
+ }
294
+ }, [isCountingDown, isFocusing, calculateFocusScore, focusThreshold]);
295
+
296
+ // Focus detection loop
297
+ useEffect(() => {
298
+ if (!videoRef.current || !cameraReady || !autoCapture || currentStep !== 'camera') {
299
+ return;
300
+ }
301
+
302
+ const detectFocus = () => {
303
+ if (videoRef.current && !isCountingDown && !isFocusing) {
304
+ const score = calculateFocusScore(videoRef.current);
305
+ setFocusScore(score);
306
+
307
+ // Start countdown if image is in focus
308
+ if (score > focusThreshold && !isCountingDown) {
309
+ startCountdown();
310
+ }
311
+ }
312
+ };
313
+
314
+ // Check focus every 500ms
315
+ focusDetectionRef.current = setInterval(detectFocus, 500);
316
+
317
+ return () => {
318
+ if (focusDetectionRef.current) {
319
+ clearInterval(focusDetectionRef.current);
320
+ }
321
+ };
322
+ }, [cameraReady, autoCapture, currentStep, isCountingDown, isFocusing, calculateFocusScore, focusThreshold, startCountdown]);
323
+
324
+ // Cleanup countdown on unmount
325
+ useEffect(() => {
326
+ return () => {
327
+ if (countdownRef.current) {
328
+ clearTimeout(countdownRef.current);
329
+ }
330
+ };
331
+ }, []);
332
+
206
333
  // Image preprocessing pipeline
207
334
  const preprocessImage = async (imageBlob) => {
208
335
  return new Promise((resolve) => {
@@ -274,6 +401,11 @@ const BusinessCardOcr = ({
274
401
  );
275
402
  }, [cameraStream]);
276
403
 
404
+ // Assign capturePhoto to ref for use in startCountdown
405
+ useEffect(() => {
406
+ capturePhotoRef.current = capturePhoto;
407
+ }, [capturePhoto]);
408
+
277
409
  const switchCamera = () => {
278
410
  if (availableCameras.length > 1) {
279
411
  setCurrentCameraIndex(
@@ -1652,7 +1784,20 @@ const BusinessCardOcr = ({
1652
1784
  {currentStep === 'camera' && (
1653
1785
  <div className={ocrStyles.cameraContainer}>
1654
1786
  <div className={ocrStyles.captureGuide}>
1655
- <p>Position business card within the frame and tap to focus</p>
1787
+ <p>
1788
+ {autoCapture
1789
+ ? 'Position business card within frame - auto-capture when focused'
1790
+ : 'Position business card within the frame and tap to focus'
1791
+ }
1792
+ </p>
1793
+ <button
1794
+ onClick={() => setAutoCapture(!autoCapture)}
1795
+ className={`${ocrStyles.autoCaptureToggle} ${autoCapture ? ocrStyles.active : ''}`}
1796
+ title={autoCapture ? 'Disable auto-capture' : 'Enable auto-capture'}
1797
+ >
1798
+ {autoCapture ? <Bot size={16} /> : <Play size={16} />}
1799
+ {autoCapture ? 'Auto' : 'Manual'}
1800
+ </button>
1656
1801
  </div>
1657
1802
  <div className={ocrStyles.videoWrapper}>
1658
1803
  <video
@@ -1663,15 +1808,54 @@ const BusinessCardOcr = ({
1663
1808
  className={ocrStyles.cameraVideo}
1664
1809
  onClick={triggerAutofocus}
1665
1810
  />
1811
+ <canvas
1812
+ ref={canvasRef}
1813
+ style={{ display: 'none' }}
1814
+ />
1666
1815
  {/* Enhanced camera overlay with focus frame */}
1667
1816
  <div className={ocrStyles.cameraOverlay}>
1668
- <div className={ocrStyles.focusFrame}>
1817
+ <div className={`${ocrStyles.focusFrame} ${
1818
+ focusScore > focusThreshold ? ocrStyles.focused : ''
1819
+ } ${isCountingDown ? ocrStyles.countingDown : ''}`}>
1820
+
1821
+ {/* Focus corners */}
1822
+ <div className={`${ocrStyles.focusCorner} ${ocrStyles.topLeft}`}></div>
1823
+ <div className={`${ocrStyles.focusCorner} ${ocrStyles.topRight}`}></div>
1824
+ <div className={`${ocrStyles.focusCorner} ${ocrStyles.bottomLeft}`}></div>
1825
+ <div className={`${ocrStyles.focusCorner} ${ocrStyles.bottomRight}`}></div>
1826
+
1827
+ {/* Countdown display */}
1828
+ {isCountingDown && countdown > 0 && (
1829
+ <div className={ocrStyles.countdownDisplay}>
1830
+ <div className={ocrStyles.countdownNumber}>{countdown}</div>
1831
+ <div className={ocrStyles.countdownText}>Auto-capturing...</div>
1832
+ </div>
1833
+ )}
1834
+
1835
+ {/* Focus indicator */}
1669
1836
  {isFocusing && (
1670
1837
  <div className={ocrStyles.focusIndicator}>
1671
1838
  <Focus className={ocrStyles.focusIcon} />
1672
1839
  <span>Focusing...</span>
1673
1840
  </div>
1674
1841
  )}
1842
+
1843
+ {/* Focus status */}
1844
+ {autoCapture && !isFocusing && !isCountingDown && (
1845
+ <div className={ocrStyles.focusStatus}>
1846
+ <div className={`${ocrStyles.focusBar} ${
1847
+ focusScore > focusThreshold ? ocrStyles.good : ocrStyles.poor
1848
+ }`}>
1849
+ <div
1850
+ className={ocrStyles.focusBarFill}
1851
+ style={{ width: `${Math.min(focusScore * 300, 100)}%` }}
1852
+ ></div>
1853
+ </div>
1854
+ <span className={ocrStyles.focusText}>
1855
+ {focusScore > focusThreshold ? '✓ In Focus' : 'Move closer or adjust angle'}
1856
+ </span>
1857
+ </div>
1858
+ )}
1675
1859
  </div>
1676
1860
  </div>
1677
1861
  </div>
@@ -1704,8 +1888,9 @@ const BusinessCardOcr = ({
1704
1888
  fileInputRef.current?.click()
1705
1889
  }
1706
1890
  className={ocrStyles.secondaryButton}
1891
+ title="Upload image from device"
1707
1892
  >
1708
- <ImagePlus size={20} />
1893
+ <ImagePlus size={18} />
1709
1894
  Upload
1710
1895
  </button>
1711
1896
 
@@ -1713,9 +1898,10 @@ const BusinessCardOcr = ({
1713
1898
  onClick={capturePhoto}
1714
1899
  className={`${ocrStyles.captureButton} ${isFocusing ? ocrStyles.focusing : ''}`}
1715
1900
  disabled={isFocusing}
1901
+ title={isFocusing ? 'Focusing camera...' : 'Capture business card'}
1716
1902
  >
1717
1903
  <Camera size={28} />
1718
- {isFocusing ? 'Focusing...' : 'Capture'}
1904
+ {isFocusing ? 'Focus...' : 'Capture'}
1719
1905
  </button>
1720
1906
 
1721
1907
  <input
@@ -1725,8 +1911,6 @@ const BusinessCardOcr = ({
1725
1911
  accept="image/jpeg,image/png,image/jpg,image/gif,image/webp"
1726
1912
  style={{ display: 'none' }}
1727
1913
  />
1728
-
1729
- <div className={ocrStyles.placeholder}></div>
1730
1914
  </div>
1731
1915
  </div>
1732
1916
  </div>
@@ -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 = {};