@visns-studio/visns-components 5.13.9 → 5.13.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,644 @@
1
+ import React, { useState, useEffect } from 'react';
2
+ import { Plus, Trash2, Move, Type, Edit3, Save, X } from 'lucide-react';
3
+ import { SortableContainer, SortableElement, SortableHandle } from 'react-sortable-hoc';
4
+ import { arrayMoveImmutable } from 'array-move';
5
+ import { toast } from 'react-toastify';
6
+ import { buildUrl, defaultProposalApiEndpoints } from '../../utils/urlBuilder';
7
+ import './AgreementSignatureEditor.css';
8
+
9
+ const DragHandle = SortableHandle(() => (
10
+ <div className="signature-drag-handle">
11
+ <Move size={16} />
12
+ </div>
13
+ ));
14
+
15
+ const SortableField = SortableElement(({ field, onEdit, onDelete }) => (
16
+ <div className="signature-field-item">
17
+ <div className="signature-field-header">
18
+ <DragHandle />
19
+ <div className="signature-field-info">
20
+ <span className="signature-field-label">{field.label}</span>
21
+ <span className="signature-field-type">{field.type}</span>
22
+ </div>
23
+ <div className="signature-field-actions">
24
+ <button
25
+ onClick={() => onEdit(field)}
26
+ className="signature-field-btn edit"
27
+ title="Edit field"
28
+ >
29
+ <Edit3 size={14} />
30
+ </button>
31
+ <button
32
+ onClick={() => onDelete(field.id)}
33
+ className="signature-field-btn delete"
34
+ title="Delete field"
35
+ >
36
+ <Trash2 size={14} />
37
+ </button>
38
+ </div>
39
+ </div>
40
+ {field.placeholder && (
41
+ <div className="signature-field-placeholder">
42
+ Placeholder: {field.placeholder}
43
+ </div>
44
+ )}
45
+ </div>
46
+ ));
47
+
48
+ const SortableFieldList = SortableContainer(({ fields, onEdit, onDelete }) => (
49
+ <div className="signature-fields-list">
50
+ {fields.map((field, index) => (
51
+ <SortableField
52
+ key={field.id}
53
+ index={index}
54
+ field={field}
55
+ onEdit={onEdit}
56
+ onDelete={onDelete}
57
+ />
58
+ ))}
59
+ </div>
60
+ ));
61
+
62
+ const FieldEditor = ({ field, onSave, onCancel }) => {
63
+ const [formData, setFormData] = useState({
64
+ label: field?.label || '',
65
+ type: field?.type || 'text',
66
+ placeholder: field?.placeholder || '',
67
+ required: field?.required || false,
68
+ width: field?.width || 'full'
69
+ });
70
+
71
+ const fieldTypes = [
72
+ { value: 'text', label: 'Text Input' },
73
+ { value: 'signature', label: 'Signature Block' },
74
+ { value: 'date', label: 'Date Field' },
75
+ { value: 'select', label: 'Dropdown' },
76
+ { value: 'textarea', label: 'Text Area' }
77
+ ];
78
+
79
+ const fieldWidths = [
80
+ { value: 'full', label: 'Full Width' },
81
+ { value: 'half', label: 'Half Width' },
82
+ { value: 'third', label: 'Third Width' }
83
+ ];
84
+
85
+ const handleSubmit = (e) => {
86
+ e.preventDefault();
87
+ if (!formData.label.trim()) {
88
+ toast.error('Field label is required');
89
+ return;
90
+ }
91
+ onSave({ ...field, ...formData });
92
+ };
93
+
94
+ return (
95
+ <div className="signature-field-editor">
96
+ <div className="signature-field-editor-header">
97
+ <h4>{field ? 'Edit Field' : 'Add New Field'}</h4>
98
+ <button onClick={onCancel} className="signature-field-editor-close">
99
+ <X size={16} />
100
+ </button>
101
+ </div>
102
+
103
+ <form onSubmit={handleSubmit} className="signature-field-editor-form">
104
+ <div className="signature-field-editor-grid">
105
+ <div className="signature-field-editor-group">
106
+ <label>Field Label *</label>
107
+ <input
108
+ type="text"
109
+ value={formData.label}
110
+ onChange={(e) => setFormData({...formData, label: e.target.value})}
111
+ placeholder="e.g., Name, Company Name, ABN"
112
+ required
113
+ />
114
+ </div>
115
+
116
+ <div className="signature-field-editor-group">
117
+ <label>Field Type</label>
118
+ <select
119
+ value={formData.type}
120
+ onChange={(e) => setFormData({...formData, type: e.target.value})}
121
+ >
122
+ {fieldTypes.map(type => (
123
+ <option key={type.value} value={type.value}>
124
+ {type.label}
125
+ </option>
126
+ ))}
127
+ </select>
128
+ </div>
129
+
130
+ <div className="signature-field-editor-group">
131
+ <label>Placeholder Text</label>
132
+ <input
133
+ type="text"
134
+ value={formData.placeholder}
135
+ onChange={(e) => setFormData({...formData, placeholder: e.target.value})}
136
+ placeholder="Optional placeholder text"
137
+ />
138
+ </div>
139
+
140
+ <div className="signature-field-editor-group">
141
+ <label>Field Width</label>
142
+ <select
143
+ value={formData.width}
144
+ onChange={(e) => setFormData({...formData, width: e.target.value})}
145
+ >
146
+ {fieldWidths.map(width => (
147
+ <option key={width.value} value={width.value}>
148
+ {width.label}
149
+ </option>
150
+ ))}
151
+ </select>
152
+ </div>
153
+ </div>
154
+
155
+ <div className="signature-field-editor-checkbox">
156
+ <label>
157
+ <input
158
+ type="checkbox"
159
+ checked={formData.required}
160
+ onChange={(e) => setFormData({...formData, required: e.target.checked})}
161
+ />
162
+ Required field
163
+ </label>
164
+ </div>
165
+
166
+ <div className="signature-field-editor-actions">
167
+ <button type="button" onClick={onCancel} className="signature-btn secondary">
168
+ Cancel
169
+ </button>
170
+ <button type="submit" className="signature-btn primary">
171
+ <Save size={16} />
172
+ Save Field
173
+ </button>
174
+ </div>
175
+ </form>
176
+ </div>
177
+ );
178
+ };
179
+
180
+ const AgreementSignatureEditor = ({
181
+ section,
182
+ templateId,
183
+ onSave,
184
+ onCancel,
185
+ apiEndpoints = defaultProposalApiEndpoints,
186
+ hideFooter = false,
187
+ onTemplateChange = null
188
+ }) => {
189
+ const [template, setTemplate] = useState({
190
+ headerText: section?.template ? section.template.headerText || '' : 'APPROVAL TO PROCEED',
191
+ bodyText: section?.template?.bodyText || 'By signing the Approval to Proceed, you agree to engage {{company_name}} to complete the Scope of Works outlined within this Fee Proposal in accordance with our Terms of Engagement.',
192
+ fields: section?.template?.fields || []
193
+ });
194
+
195
+ const [editingField, setEditingField] = useState(null);
196
+ const [showFieldEditor, setShowFieldEditor] = useState(false);
197
+ const [isLoading, setIsLoading] = useState(false);
198
+
199
+ // Helper function to replace template variables in URLs
200
+ const buildUrlWithTemplate = (urlTemplate, params = {}) => {
201
+ return buildUrl(urlTemplate, { templateId, ...params });
202
+ };
203
+
204
+ const parseContentToTemplate = (content) => {
205
+ // Parse HTML content to extract template data
206
+ const parser = new DOMParser();
207
+ const doc = parser.parseFromString(content, 'text/html');
208
+
209
+ // Extract header text from h3 tag
210
+ const headerElement = doc.querySelector('h3');
211
+ const headerText = headerElement ? headerElement.textContent.trim() : '';
212
+
213
+ // Extract body text from p tag
214
+ const bodyElement = doc.querySelector('p');
215
+ const bodyText = bodyElement ? bodyElement.textContent.trim() : 'By signing the Approval to Proceed, you agree to engage {{company_name}} to complete the Scope of Works outlined within this Fee Proposal in accordance with our Terms of Engagement.';
216
+
217
+ // Extract fields from the content (basic parsing)
218
+ const defaultFields = [
219
+ { id: 1, label: 'Name', type: 'text', placeholder: '', required: true, width: 'full', order: 1 },
220
+ { id: 2, label: 'Company Name', type: 'text', placeholder: '', required: true, width: 'full', order: 2 },
221
+ { id: 3, label: 'ABN', type: 'text', placeholder: '', required: false, width: 'full', order: 3 },
222
+ { id: 4, label: 'Signature', type: 'signature', placeholder: '', required: true, width: 'full', order: 4 },
223
+ { id: 5, label: 'Date', type: 'date', placeholder: '', required: true, width: 'full', order: 5 }
224
+ ];
225
+
226
+ return {
227
+ headerText: headerText,
228
+ bodyText: bodyText,
229
+ fields: defaultFields
230
+ };
231
+ };
232
+
233
+ useEffect(() => {
234
+ if (section?.template) {
235
+ setTemplate(section.template);
236
+ } else if (section?.content) {
237
+ // Parse existing content to extract template data
238
+ const parsedTemplate = parseContentToTemplate(section.content);
239
+ setTemplate(parsedTemplate);
240
+ } else {
241
+ loadDefaultTemplate();
242
+ }
243
+ }, [section]);
244
+
245
+ // Notify parent when template changes
246
+ useEffect(() => {
247
+ if (onTemplateChange) {
248
+ const updatedSection = {
249
+ ...section,
250
+ template: template,
251
+ content: generatePreviewContent(template)
252
+ };
253
+ onTemplateChange(updatedSection);
254
+ }
255
+ }, [template]);
256
+
257
+ const loadDefaultTemplate = async () => {
258
+ if (!templateId) {
259
+ // Load default template when no templateId is provided
260
+ setTemplate({
261
+ headerText: 'APPROVAL TO PROCEED',
262
+ bodyText: 'By signing the Approval to Proceed, you agree to engage {{company_name}} to complete the Scope of Works outlined within this Fee Proposal in accordance with our Terms of Engagement.',
263
+ fields: [
264
+ { id: 1, label: 'Name', type: 'text', placeholder: '', required: true, width: 'full', order: 1 },
265
+ { id: 2, label: 'Company Name', type: 'text', placeholder: '', required: true, width: 'full', order: 2 },
266
+ { id: 3, label: 'ABN', type: 'text', placeholder: '', required: false, width: 'full', order: 3 },
267
+ { id: 4, label: 'Signature', type: 'signature', placeholder: '', required: true, width: 'full', order: 4 },
268
+ { id: 5, label: 'Date', type: 'date', placeholder: '', required: true, width: 'full', order: 5 }
269
+ ]
270
+ });
271
+ return;
272
+ }
273
+
274
+ try {
275
+ setIsLoading(true);
276
+ const response = await fetch(buildUrlWithTemplate(apiEndpoints.agreementSignatureTemplate || '/ajax/agreement-signature-template/{templateId}'));
277
+
278
+ // Check if the response is ok and contains JSON
279
+ if (!response.ok) {
280
+ throw new Error(`HTTP error! status: ${response.status}`);
281
+ }
282
+
283
+ const contentType = response.headers.get('content-type');
284
+ if (!contentType || !contentType.includes('application/json')) {
285
+ // If the API doesn't exist or returns HTML, use default template
286
+ console.warn('Agreement signature template API not available, using default template');
287
+ setTemplate({
288
+ headerText: 'APPROVAL TO PROCEED',
289
+ bodyText: 'By signing the Approval to Proceed, you agree to engage {{company_name}} to complete the Scope of Works outlined within this Fee Proposal in accordance with our Terms of Engagement.',
290
+ fields: [
291
+ { id: 1, label: 'Name', type: 'text', placeholder: '', required: true, width: 'full', order: 1 },
292
+ { id: 2, label: 'Company Name', type: 'text', placeholder: '', required: true, width: 'full', order: 2 },
293
+ { id: 3, label: 'ABN', type: 'text', placeholder: '', required: false, width: 'full', order: 3 },
294
+ { id: 4, label: 'Signature', type: 'signature', placeholder: '', required: true, width: 'full', order: 4 },
295
+ { id: 5, label: 'Date', type: 'date', placeholder: '', required: true, width: 'full', order: 5 }
296
+ ]
297
+ });
298
+ return;
299
+ }
300
+
301
+ const data = await response.json();
302
+
303
+ if (data.success) {
304
+ setTemplate(data.template);
305
+ } else {
306
+ // Use default template if none exists
307
+ setTemplate({
308
+ headerText: 'APPROVAL TO PROCEED',
309
+ bodyText: 'By signing the Approval to Proceed, you agree to engage {{company_name}} to complete the Scope of Works outlined within this Fee Proposal in accordance with our Terms of Engagement.',
310
+ fields: [
311
+ { id: 1, label: 'Name', type: 'text', placeholder: '', required: true, width: 'full', order: 1 },
312
+ { id: 2, label: 'Company Name', type: 'text', placeholder: '', required: true, width: 'full', order: 2 },
313
+ { id: 3, label: 'ABN', type: 'text', placeholder: '', required: false, width: 'full', order: 3 },
314
+ { id: 4, label: 'Signature', type: 'signature', placeholder: '', required: true, width: 'full', order: 4 },
315
+ { id: 5, label: 'Date', type: 'date', placeholder: '', required: true, width: 'full', order: 5 }
316
+ ]
317
+ });
318
+ }
319
+ } catch (error) {
320
+ console.warn('Error loading agreement signature template, using default:', error.message);
321
+ // Use default template instead of showing error
322
+ setTemplate({
323
+ headerText: 'APPROVAL TO PROCEED',
324
+ bodyText: 'By signing the Approval to Proceed, you agree to engage {{company_name}} to complete the Scope of Works outlined within this Fee Proposal in accordance with our Terms of Engagement.',
325
+ fields: [
326
+ { id: 1, label: 'Name', type: 'text', placeholder: '', required: true, width: 'full', order: 1 },
327
+ { id: 2, label: 'Company Name', type: 'text', placeholder: '', required: true, width: 'full', order: 2 },
328
+ { id: 3, label: 'ABN', type: 'text', placeholder: '', required: false, width: 'full', order: 3 },
329
+ { id: 4, label: 'Signature', type: 'signature', placeholder: '', required: true, width: 'full', order: 4 },
330
+ { id: 5, label: 'Date', type: 'date', placeholder: '', required: true, width: 'full', order: 5 }
331
+ ]
332
+ });
333
+ } finally {
334
+ setIsLoading(false);
335
+ }
336
+ };
337
+
338
+ const handleFieldsReorder = ({ oldIndex, newIndex }) => {
339
+ if (oldIndex === newIndex) return;
340
+
341
+ const newFields = arrayMoveImmutable(template.fields, oldIndex, newIndex);
342
+ const reorderedFields = newFields.map((field, index) => ({
343
+ ...field,
344
+ order: index + 1
345
+ }));
346
+
347
+ setTemplate({
348
+ ...template,
349
+ fields: reorderedFields
350
+ });
351
+ };
352
+
353
+ const handleAddField = () => {
354
+ setEditingField(null);
355
+ setShowFieldEditor(true);
356
+ };
357
+
358
+ const handleEditField = (field) => {
359
+ setEditingField(field);
360
+ setShowFieldEditor(true);
361
+ };
362
+
363
+ const handleSaveField = (fieldData) => {
364
+ if (editingField) {
365
+ // Update existing field
366
+ const updatedFields = template.fields.map(field =>
367
+ field.id === editingField.id ? { ...fieldData, id: editingField.id } : field
368
+ );
369
+ setTemplate({
370
+ ...template,
371
+ fields: updatedFields
372
+ });
373
+ } else {
374
+ // Add new field
375
+ const newField = {
376
+ ...fieldData,
377
+ id: Date.now(), // Temporary ID - backend will assign real ID
378
+ order: template.fields.length + 1
379
+ };
380
+ setTemplate({
381
+ ...template,
382
+ fields: [...template.fields, newField]
383
+ });
384
+ }
385
+ setShowFieldEditor(false);
386
+ setEditingField(null);
387
+ };
388
+
389
+ const handleDeleteField = (fieldId) => {
390
+ if (!confirm('Are you sure you want to delete this field?')) return;
391
+
392
+ const updatedFields = template.fields
393
+ .filter(field => field.id !== fieldId)
394
+ .map((field, index) => ({ ...field, order: index + 1 }));
395
+
396
+ setTemplate({
397
+ ...template,
398
+ fields: updatedFields
399
+ });
400
+ };
401
+
402
+ const handleSaveTemplate = async () => {
403
+ try {
404
+ setIsLoading(true);
405
+
406
+ // For now, if no templateId or API is not available, just save locally
407
+ if (!templateId) {
408
+ // Update section with template reference
409
+ const updatedSection = {
410
+ ...section,
411
+ template: template,
412
+ content: generatePreviewContent(template)
413
+ };
414
+
415
+ onSave(updatedSection);
416
+ toast.success('Agreement signature template saved locally');
417
+ return;
418
+ }
419
+
420
+ // Save template to backend
421
+ const response = await fetch(buildUrlWithTemplate(apiEndpoints.agreementSignatureTemplateSave || '/ajax/agreement-signature-template/{templateId}'), {
422
+ method: 'POST',
423
+ headers: {
424
+ 'Content-Type': 'application/json',
425
+ 'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]')?.getAttribute('content')
426
+ },
427
+ body: JSON.stringify({
428
+ template: template
429
+ })
430
+ });
431
+
432
+ // Check if the response is ok and contains JSON
433
+ if (!response.ok) {
434
+ throw new Error(`HTTP error! status: ${response.status}`);
435
+ }
436
+
437
+ const contentType = response.headers.get('content-type');
438
+ if (!contentType || !contentType.includes('application/json')) {
439
+ // If the API doesn't exist, save locally instead
440
+ console.warn('Agreement signature template save API not available, saving locally');
441
+ const updatedSection = {
442
+ ...section,
443
+ template: template,
444
+ content: generatePreviewContent(template)
445
+ };
446
+
447
+ onSave(updatedSection);
448
+ toast.success('Agreement signature template saved locally');
449
+ return;
450
+ }
451
+
452
+ const data = await response.json();
453
+ if (data.success) {
454
+ // Update section with template reference
455
+ const updatedSection = {
456
+ ...section,
457
+ template: data.template,
458
+ content: generatePreviewContent(data.template)
459
+ };
460
+
461
+ onSave(updatedSection);
462
+ toast.success('Agreement signature template saved');
463
+ } else {
464
+ throw new Error(data.message || 'Failed to save template');
465
+ }
466
+ } catch (error) {
467
+ console.warn('Error saving template to backend, saving locally:', error.message);
468
+
469
+ // Fallback to local save
470
+ const updatedSection = {
471
+ ...section,
472
+ template: template,
473
+ content: generatePreviewContent(template)
474
+ };
475
+
476
+ onSave(updatedSection);
477
+ toast.success('Agreement signature template saved locally');
478
+ } finally {
479
+ setIsLoading(false);
480
+ }
481
+ };
482
+
483
+ const generatePreviewContent = (templateData) => {
484
+ const fieldsHtml = templateData.fields
485
+ .sort((a, b) => a.order - b.order)
486
+ .map(field => {
487
+ const required = field.required ? ' *' : '';
488
+ const width = field.width === 'full' ? '100%' : field.width === 'half' ? '48%' : '30%';
489
+
490
+ switch (field.type) {
491
+ case 'signature':
492
+ return `<div style="margin-bottom: 2rem; width: ${width}; display: inline-block; margin-right: 2%;">
493
+ <strong>${field.label}${required}:</strong>
494
+ <div style="border-bottom: 2px solid #0066cc; height: 3rem; margin-top: 0.5rem;"></div>
495
+ </div>`;
496
+ case 'date':
497
+ return `<div style="margin-bottom: 2rem; width: ${width}; display: inline-block; margin-right: 2%;">
498
+ <strong>${field.label}${required}:</strong>
499
+ <div style="border-bottom: 2px dotted #0066cc; height: 2rem; margin-top: 0.5rem;"></div>
500
+ </div>`;
501
+ default:
502
+ return `<div style="margin-bottom: 2rem; width: ${width}; display: inline-block; margin-right: 2%;">
503
+ <strong>${field.label}${required}:</strong>
504
+ <div style="border-bottom: 2px dotted #0066cc; height: 2rem; margin-top: 0.5rem;"></div>
505
+ </div>`;
506
+ }
507
+ })
508
+ .join('');
509
+
510
+ return `
511
+ <div style="padding: 2rem; border: 1px solid #ddd; border-radius: 8px; background: #fff;">
512
+ ${templateData.headerText ? `<h3 style="margin-bottom: 1rem; color: #333;">${templateData.headerText}</h3>` : ''}
513
+ <p style="margin-bottom: 2rem; line-height: 1.6;">${templateData.bodyText}</p>
514
+ <div style="clear: both;">${fieldsHtml}</div>
515
+ </div>
516
+ `;
517
+ };
518
+
519
+ if (isLoading) {
520
+ return (
521
+ <div className="signature-editor-loading">
522
+ <div className="signature-editor-spinner"></div>
523
+ <p>Loading template...</p>
524
+ </div>
525
+ );
526
+ }
527
+
528
+ return (
529
+ <div className="signature-editor-container">
530
+ <div className="signature-editor-header">
531
+ <h3>Agreement Signature Template</h3>
532
+ <p>Customize the text and fields for your agreement signature section</p>
533
+ </div>
534
+
535
+ <div className="signature-editor-content">
536
+ <div className="signature-editor-section">
537
+ <label className="signature-editor-label">Header Text (Optional)</label>
538
+ <input
539
+ type="text"
540
+ value={template.headerText}
541
+ onChange={(e) => setTemplate({...template, headerText: e.target.value})}
542
+ placeholder="e.g., APPROVAL TO PROCEED (leave empty for no header)"
543
+ className="signature-editor-input"
544
+ />
545
+ <small className="signature-editor-help">
546
+ Leave empty to remove the header completely
547
+ </small>
548
+ </div>
549
+
550
+ <div className="signature-editor-section">
551
+ <label className="signature-editor-label">Body Text</label>
552
+ <textarea
553
+ value={template.bodyText}
554
+ onChange={(e) => setTemplate({...template, bodyText: e.target.value})}
555
+ placeholder="Enter the agreement text. Use {{variable_name}} for dynamic content."
556
+ className="signature-editor-textarea"
557
+ rows="3"
558
+ />
559
+ <small className="signature-editor-help">
560
+ Use variables like {'{{company_name}}'} for dynamic content
561
+ </small>
562
+ </div>
563
+
564
+ <div className="signature-editor-section">
565
+ <div className="signature-editor-fields-header">
566
+ <label className="signature-editor-label">Signature Fields</label>
567
+ <button
568
+ onClick={handleAddField}
569
+ className="signature-btn primary small"
570
+ >
571
+ <Plus size={16} />
572
+ Add Field
573
+ </button>
574
+ </div>
575
+
576
+ {template.fields.length === 0 ? (
577
+ <div className="signature-editor-empty">
578
+ <Type size={48} />
579
+ <p>No fields added yet</p>
580
+ <button onClick={handleAddField} className="signature-btn primary">
581
+ Add Your First Field
582
+ </button>
583
+ </div>
584
+ ) : (
585
+ <SortableFieldList
586
+ fields={template.fields}
587
+ onSortEnd={handleFieldsReorder}
588
+ onEdit={handleEditField}
589
+ onDelete={handleDeleteField}
590
+ useDragHandle
591
+ lockAxis="y"
592
+ helperClass="signature-field-helper"
593
+ />
594
+ )}
595
+ </div>
596
+
597
+ <div className="signature-editor-preview">
598
+ <h4>Preview</h4>
599
+ <div
600
+ className="signature-editor-preview-content"
601
+ dangerouslySetInnerHTML={{ __html: generatePreviewContent(template) }}
602
+ />
603
+ </div>
604
+ </div>
605
+
606
+ {!hideFooter && (
607
+ <div className="signature-editor-actions">
608
+ <button
609
+ onClick={onCancel}
610
+ className="signature-btn secondary"
611
+ disabled={isLoading}
612
+ >
613
+ Cancel
614
+ </button>
615
+ <button
616
+ onClick={handleSaveTemplate}
617
+ className="signature-btn primary"
618
+ disabled={isLoading}
619
+ >
620
+ <Save size={16} />
621
+ {isLoading ? 'Saving...' : 'Save Section'}
622
+ </button>
623
+ </div>
624
+ )}
625
+
626
+ {showFieldEditor && (
627
+ <div className="signature-editor-overlay">
628
+ <div className="signature-editor-modal">
629
+ <FieldEditor
630
+ field={editingField}
631
+ onSave={handleSaveField}
632
+ onCancel={() => {
633
+ setShowFieldEditor(false);
634
+ setEditingField(null);
635
+ }}
636
+ />
637
+ </div>
638
+ </div>
639
+ )}
640
+ </div>
641
+ );
642
+ };
643
+
644
+ export default AgreementSignatureEditor;