@visns-studio/visns-components 5.13.12 → 5.13.14

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
@@ -88,7 +88,7 @@
88
88
  "react-dom": "^17.0.0 || ^18.0.0"
89
89
  },
90
90
  "name": "@visns-studio/visns-components",
91
- "version": "5.13.12",
91
+ "version": "5.13.14",
92
92
  "description": "Various packages to assist in the development of our Custom Applications.",
93
93
  "main": "src/index.js",
94
94
  "files": [
@@ -87,6 +87,13 @@
87
87
  margin-bottom: 1rem;
88
88
  }
89
89
 
90
+ .signature-editor-label-with-button {
91
+ display: flex;
92
+ align-items: center;
93
+ justify-content: space-between;
94
+ margin-bottom: 0.5rem;
95
+ }
96
+
90
97
  .signature-btn {
91
98
  display: inline-flex;
92
99
  align-items: center;
@@ -284,7 +291,6 @@
284
291
  }
285
292
 
286
293
  .signature-editor-preview-content {
287
- border: 1px solid var(--paragraph-color, #e2e8f0);
288
294
  border-radius: 6px;
289
295
  overflow: hidden;
290
296
  }
@@ -321,8 +327,12 @@
321
327
  }
322
328
 
323
329
  @keyframes spin {
324
- 0% { transform: rotate(0deg); }
325
- 100% { transform: rotate(360deg); }
330
+ 0% {
331
+ transform: rotate(0deg);
332
+ }
333
+ 100% {
334
+ transform: rotate(360deg);
335
+ }
326
336
  }
327
337
 
328
338
  /* Field Editor Modal */
@@ -441,7 +451,7 @@
441
451
  cursor: pointer;
442
452
  }
443
453
 
444
- .signature-field-editor-checkbox input[type="checkbox"] {
454
+ .signature-field-editor-checkbox input[type='checkbox'] {
445
455
  accent-color: var(--primary-color, #3cbf7d);
446
456
  }
447
457
 
@@ -458,34 +468,34 @@
458
468
  .signature-editor-container {
459
469
  padding: 1rem;
460
470
  }
461
-
471
+
462
472
  .signature-editor-fields-header {
463
473
  flex-direction: column;
464
474
  align-items: flex-start;
465
475
  gap: 1rem;
466
476
  }
467
-
477
+
468
478
  .signature-field-header {
469
479
  flex-direction: column;
470
480
  align-items: flex-start;
471
481
  gap: 0.5rem;
472
482
  }
473
-
483
+
474
484
  .signature-field-actions {
475
485
  width: 100%;
476
486
  justify-content: flex-end;
477
487
  }
478
-
488
+
479
489
  .signature-editor-actions {
480
490
  flex-direction: column;
481
491
  }
482
-
492
+
483
493
  .signature-field-editor-grid {
484
494
  grid-template-columns: 1fr;
485
495
  }
486
-
496
+
487
497
  .signature-editor-modal {
488
498
  width: 95%;
489
499
  margin: 1rem;
490
500
  }
491
- }
501
+ }
@@ -1,9 +1,14 @@
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';
1
+ import React, { useState, useEffect, useRef } from 'react';
2
+ import { Plus, Trash2, Move, Type, Edit3, Save, X, Tag } from 'lucide-react';
3
+ import {
4
+ SortableContainer,
5
+ SortableElement,
6
+ SortableHandle,
7
+ } from 'react-sortable-hoc';
4
8
  import { arrayMoveImmutable } from 'array-move';
5
9
  import { toast } from 'react-toastify';
6
10
  import { buildUrl, defaultProposalApiEndpoints } from '../../utils/urlBuilder';
11
+ import VariableInserter from './VariableInserter';
7
12
  import './AgreementSignatureEditor.css';
8
13
 
9
14
  const DragHandle = SortableHandle(() => (
@@ -65,7 +70,7 @@ const FieldEditor = ({ field, onSave, onCancel }) => {
65
70
  type: field?.type || 'text',
66
71
  placeholder: field?.placeholder || '',
67
72
  required: field?.required || false,
68
- width: field?.width || 'full'
73
+ width: field?.width || 'full',
69
74
  });
70
75
 
71
76
  const fieldTypes = [
@@ -73,13 +78,13 @@ const FieldEditor = ({ field, onSave, onCancel }) => {
73
78
  { value: 'signature', label: 'Signature Block' },
74
79
  { value: 'date', label: 'Date Field' },
75
80
  { value: 'select', label: 'Dropdown' },
76
- { value: 'textarea', label: 'Text Area' }
81
+ { value: 'textarea', label: 'Text Area' },
77
82
  ];
78
83
 
79
84
  const fieldWidths = [
80
85
  { value: 'full', label: 'Full Width' },
81
86
  { value: 'half', label: 'Half Width' },
82
- { value: 'third', label: 'Third Width' }
87
+ { value: 'third', label: 'Third Width' },
83
88
  ];
84
89
 
85
90
  const handleSubmit = (e) => {
@@ -95,55 +100,81 @@ const FieldEditor = ({ field, onSave, onCancel }) => {
95
100
  <div className="signature-field-editor">
96
101
  <div className="signature-field-editor-header">
97
102
  <h4>{field ? 'Edit Field' : 'Add New Field'}</h4>
98
- <button onClick={onCancel} className="signature-field-editor-close">
103
+ <button
104
+ onClick={onCancel}
105
+ className="signature-field-editor-close"
106
+ >
99
107
  <X size={16} />
100
108
  </button>
101
109
  </div>
102
-
103
- <form onSubmit={handleSubmit} className="signature-field-editor-form">
110
+
111
+ <form
112
+ onSubmit={handleSubmit}
113
+ className="signature-field-editor-form"
114
+ >
104
115
  <div className="signature-field-editor-grid">
105
116
  <div className="signature-field-editor-group">
106
117
  <label>Field Label *</label>
107
118
  <input
108
119
  type="text"
109
120
  value={formData.label}
110
- onChange={(e) => setFormData({...formData, label: e.target.value})}
121
+ onChange={(e) =>
122
+ setFormData({
123
+ ...formData,
124
+ label: e.target.value,
125
+ })
126
+ }
111
127
  placeholder="e.g., Name, Company Name, ABN"
112
128
  required
113
129
  />
114
130
  </div>
115
-
131
+
116
132
  <div className="signature-field-editor-group">
117
133
  <label>Field Type</label>
118
134
  <select
119
135
  value={formData.type}
120
- onChange={(e) => setFormData({...formData, type: e.target.value})}
136
+ onChange={(e) =>
137
+ setFormData({
138
+ ...formData,
139
+ type: e.target.value,
140
+ })
141
+ }
121
142
  >
122
- {fieldTypes.map(type => (
143
+ {fieldTypes.map((type) => (
123
144
  <option key={type.value} value={type.value}>
124
145
  {type.label}
125
146
  </option>
126
147
  ))}
127
148
  </select>
128
149
  </div>
129
-
150
+
130
151
  <div className="signature-field-editor-group">
131
152
  <label>Placeholder Text</label>
132
153
  <input
133
154
  type="text"
134
155
  value={formData.placeholder}
135
- onChange={(e) => setFormData({...formData, placeholder: e.target.value})}
156
+ onChange={(e) =>
157
+ setFormData({
158
+ ...formData,
159
+ placeholder: e.target.value,
160
+ })
161
+ }
136
162
  placeholder="Optional placeholder text"
137
163
  />
138
164
  </div>
139
-
165
+
140
166
  <div className="signature-field-editor-group">
141
167
  <label>Field Width</label>
142
168
  <select
143
169
  value={formData.width}
144
- onChange={(e) => setFormData({...formData, width: e.target.value})}
170
+ onChange={(e) =>
171
+ setFormData({
172
+ ...formData,
173
+ width: e.target.value,
174
+ })
175
+ }
145
176
  >
146
- {fieldWidths.map(width => (
177
+ {fieldWidths.map((width) => (
147
178
  <option key={width.value} value={width.value}>
148
179
  {width.label}
149
180
  </option>
@@ -151,20 +182,29 @@ const FieldEditor = ({ field, onSave, onCancel }) => {
151
182
  </select>
152
183
  </div>
153
184
  </div>
154
-
185
+
155
186
  <div className="signature-field-editor-checkbox">
156
187
  <label>
157
188
  <input
158
189
  type="checkbox"
159
190
  checked={formData.required}
160
- onChange={(e) => setFormData({...formData, required: e.target.checked})}
191
+ onChange={(e) =>
192
+ setFormData({
193
+ ...formData,
194
+ required: e.target.checked,
195
+ })
196
+ }
161
197
  />
162
198
  Required field
163
199
  </label>
164
200
  </div>
165
-
201
+
166
202
  <div className="signature-field-editor-actions">
167
- <button type="button" onClick={onCancel} className="signature-btn secondary">
203
+ <button
204
+ type="button"
205
+ onClick={onCancel}
206
+ className="signature-btn secondary"
207
+ >
168
208
  Cancel
169
209
  </button>
170
210
  <button type="submit" className="signature-btn primary">
@@ -177,24 +217,30 @@ const FieldEditor = ({ field, onSave, onCancel }) => {
177
217
  );
178
218
  };
179
219
 
180
- const AgreementSignatureEditor = ({
181
- section,
220
+ const AgreementSignatureEditor = ({
221
+ section,
182
222
  templateId,
183
- onSave,
223
+ onSave,
184
224
  onCancel,
185
225
  apiEndpoints = defaultProposalApiEndpoints,
186
226
  hideFooter = false,
187
- onTemplateChange = null
227
+ onTemplateChange = null,
188
228
  }) => {
189
229
  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 || []
230
+ headerText: section?.template
231
+ ? section.template.headerText || ''
232
+ : 'APPROVAL TO PROCEED',
233
+ bodyText:
234
+ section?.template?.bodyText ||
235
+ '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.',
236
+ fields: section?.template?.fields || [],
193
237
  });
194
-
238
+
195
239
  const [editingField, setEditingField] = useState(null);
196
240
  const [showFieldEditor, setShowFieldEditor] = useState(false);
197
241
  const [isLoading, setIsLoading] = useState(false);
242
+ const [showVariableInserter, setShowVariableInserter] = useState(false);
243
+ const bodyTextRef = useRef(null);
198
244
 
199
245
  // Helper function to replace template variables in URLs
200
246
  const buildUrlWithTemplate = (urlTemplate, params = {}) => {
@@ -205,28 +251,72 @@ const AgreementSignatureEditor = ({
205
251
  // Parse HTML content to extract template data
206
252
  const parser = new DOMParser();
207
253
  const doc = parser.parseFromString(content, 'text/html');
208
-
254
+
209
255
  // Extract header text from h3 tag
210
256
  const headerElement = doc.querySelector('h3');
211
- const headerText = headerElement ? headerElement.textContent.trim() : '';
212
-
257
+ const headerText = headerElement
258
+ ? headerElement.textContent.trim()
259
+ : '';
260
+
213
261
  // Extract body text from p tag
214
262
  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
-
263
+ const bodyText = bodyElement
264
+ ? bodyElement.textContent.trim()
265
+ : '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.';
266
+
217
267
  // Extract fields from the content (basic parsing)
218
268
  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 }
269
+ {
270
+ id: 1,
271
+ label: 'Name',
272
+ type: 'text',
273
+ placeholder: '',
274
+ required: true,
275
+ width: 'full',
276
+ order: 1,
277
+ },
278
+ {
279
+ id: 2,
280
+ label: 'Company Name',
281
+ type: 'text',
282
+ placeholder: '',
283
+ required: true,
284
+ width: 'full',
285
+ order: 2,
286
+ },
287
+ {
288
+ id: 3,
289
+ label: 'ABN',
290
+ type: 'text',
291
+ placeholder: '',
292
+ required: false,
293
+ width: 'full',
294
+ order: 3,
295
+ },
296
+ {
297
+ id: 4,
298
+ label: 'Signature',
299
+ type: 'signature',
300
+ placeholder: '',
301
+ required: true,
302
+ width: 'full',
303
+ order: 4,
304
+ },
305
+ {
306
+ id: 5,
307
+ label: 'Date',
308
+ type: 'date',
309
+ placeholder: '',
310
+ required: true,
311
+ width: 'full',
312
+ order: 5,
313
+ },
224
314
  ];
225
-
315
+
226
316
  return {
227
317
  headerText: headerText,
228
318
  bodyText: bodyText,
229
- fields: defaultFields
319
+ fields: defaultFields,
230
320
  };
231
321
  };
232
322
 
@@ -248,7 +338,7 @@ const AgreementSignatureEditor = ({
248
338
  const updatedSection = {
249
339
  ...section,
250
340
  template: template,
251
- content: generatePreviewContent(template)
341
+ content: generatePreviewContent(template),
252
342
  };
253
343
  onTemplateChange(updatedSection);
254
344
  }
@@ -259,76 +349,250 @@ const AgreementSignatureEditor = ({
259
349
  // Load default template when no templateId is provided
260
350
  setTemplate({
261
351
  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.',
352
+ bodyText:
353
+ '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
354
  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
- ]
355
+ {
356
+ id: 1,
357
+ label: 'Name',
358
+ type: 'text',
359
+ placeholder: '',
360
+ required: true,
361
+ width: 'full',
362
+ order: 1,
363
+ },
364
+ {
365
+ id: 2,
366
+ label: 'Company Name',
367
+ type: 'text',
368
+ placeholder: '',
369
+ required: true,
370
+ width: 'full',
371
+ order: 2,
372
+ },
373
+ {
374
+ id: 3,
375
+ label: 'ABN',
376
+ type: 'text',
377
+ placeholder: '',
378
+ required: false,
379
+ width: 'full',
380
+ order: 3,
381
+ },
382
+ {
383
+ id: 4,
384
+ label: 'Signature',
385
+ type: 'signature',
386
+ placeholder: '',
387
+ required: true,
388
+ width: 'full',
389
+ order: 4,
390
+ },
391
+ {
392
+ id: 5,
393
+ label: 'Date',
394
+ type: 'date',
395
+ placeholder: '',
396
+ required: true,
397
+ width: 'full',
398
+ order: 5,
399
+ },
400
+ ],
270
401
  });
271
402
  return;
272
403
  }
273
-
404
+
274
405
  try {
275
406
  setIsLoading(true);
276
- const response = await fetch(buildUrlWithTemplate(apiEndpoints.agreementSignatureTemplate || '/ajax/agreement-signature-template/{templateId}'));
277
-
407
+ const response = await fetch(
408
+ buildUrlWithTemplate(
409
+ apiEndpoints.agreementSignatureTemplate ||
410
+ '/ajax/agreement-signature-template/{templateId}'
411
+ )
412
+ );
413
+
278
414
  // Check if the response is ok and contains JSON
279
415
  if (!response.ok) {
280
416
  throw new Error(`HTTP error! status: ${response.status}`);
281
417
  }
282
-
418
+
283
419
  const contentType = response.headers.get('content-type');
284
420
  if (!contentType || !contentType.includes('application/json')) {
285
421
  // If the API doesn't exist or returns HTML, use default template
286
- console.warn('Agreement signature template API not available, using default template');
422
+ console.warn(
423
+ 'Agreement signature template API not available, using default template'
424
+ );
287
425
  setTemplate({
288
426
  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.',
427
+ bodyText:
428
+ '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
429
  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
- ]
430
+ {
431
+ id: 1,
432
+ label: 'Name',
433
+ type: 'text',
434
+ placeholder: '',
435
+ required: true,
436
+ width: 'full',
437
+ order: 1,
438
+ },
439
+ {
440
+ id: 2,
441
+ label: 'Company Name',
442
+ type: 'text',
443
+ placeholder: '',
444
+ required: true,
445
+ width: 'full',
446
+ order: 2,
447
+ },
448
+ {
449
+ id: 3,
450
+ label: 'ABN',
451
+ type: 'text',
452
+ placeholder: '',
453
+ required: false,
454
+ width: 'full',
455
+ order: 3,
456
+ },
457
+ {
458
+ id: 4,
459
+ label: 'Signature',
460
+ type: 'signature',
461
+ placeholder: '',
462
+ required: true,
463
+ width: 'full',
464
+ order: 4,
465
+ },
466
+ {
467
+ id: 5,
468
+ label: 'Date',
469
+ type: 'date',
470
+ placeholder: '',
471
+ required: true,
472
+ width: 'full',
473
+ order: 5,
474
+ },
475
+ ],
297
476
  });
298
477
  return;
299
478
  }
300
-
479
+
301
480
  const data = await response.json();
302
-
481
+
303
482
  if (data.success) {
304
483
  setTemplate(data.template);
305
484
  } else {
306
485
  // Use default template if none exists
307
486
  setTemplate({
308
487
  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.',
488
+ bodyText:
489
+ '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
490
  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
- ]
491
+ {
492
+ id: 1,
493
+ label: 'Name',
494
+ type: 'text',
495
+ placeholder: '',
496
+ required: true,
497
+ width: 'full',
498
+ order: 1,
499
+ },
500
+ {
501
+ id: 2,
502
+ label: 'Company Name',
503
+ type: 'text',
504
+ placeholder: '',
505
+ required: true,
506
+ width: 'full',
507
+ order: 2,
508
+ },
509
+ {
510
+ id: 3,
511
+ label: 'ABN',
512
+ type: 'text',
513
+ placeholder: '',
514
+ required: false,
515
+ width: 'full',
516
+ order: 3,
517
+ },
518
+ {
519
+ id: 4,
520
+ label: 'Signature',
521
+ type: 'signature',
522
+ placeholder: '',
523
+ required: true,
524
+ width: 'full',
525
+ order: 4,
526
+ },
527
+ {
528
+ id: 5,
529
+ label: 'Date',
530
+ type: 'date',
531
+ placeholder: '',
532
+ required: true,
533
+ width: 'full',
534
+ order: 5,
535
+ },
536
+ ],
317
537
  });
318
538
  }
319
539
  } catch (error) {
320
- console.warn('Error loading agreement signature template, using default:', error.message);
540
+ console.warn(
541
+ 'Error loading agreement signature template, using default:',
542
+ error.message
543
+ );
321
544
  // Use default template instead of showing error
322
545
  setTemplate({
323
546
  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.',
547
+ bodyText:
548
+ '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
549
  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
- ]
550
+ {
551
+ id: 1,
552
+ label: 'Name',
553
+ type: 'text',
554
+ placeholder: '',
555
+ required: true,
556
+ width: 'full',
557
+ order: 1,
558
+ },
559
+ {
560
+ id: 2,
561
+ label: 'Company Name',
562
+ type: 'text',
563
+ placeholder: '',
564
+ required: true,
565
+ width: 'full',
566
+ order: 2,
567
+ },
568
+ {
569
+ id: 3,
570
+ label: 'ABN',
571
+ type: 'text',
572
+ placeholder: '',
573
+ required: false,
574
+ width: 'full',
575
+ order: 3,
576
+ },
577
+ {
578
+ id: 4,
579
+ label: 'Signature',
580
+ type: 'signature',
581
+ placeholder: '',
582
+ required: true,
583
+ width: 'full',
584
+ order: 4,
585
+ },
586
+ {
587
+ id: 5,
588
+ label: 'Date',
589
+ type: 'date',
590
+ placeholder: '',
591
+ required: true,
592
+ width: 'full',
593
+ order: 5,
594
+ },
595
+ ],
332
596
  });
333
597
  } finally {
334
598
  setIsLoading(false);
@@ -338,15 +602,19 @@ const AgreementSignatureEditor = ({
338
602
  const handleFieldsReorder = ({ oldIndex, newIndex }) => {
339
603
  if (oldIndex === newIndex) return;
340
604
 
341
- const newFields = arrayMoveImmutable(template.fields, oldIndex, newIndex);
605
+ const newFields = arrayMoveImmutable(
606
+ template.fields,
607
+ oldIndex,
608
+ newIndex
609
+ );
342
610
  const reorderedFields = newFields.map((field, index) => ({
343
611
  ...field,
344
- order: index + 1
612
+ order: index + 1,
345
613
  }));
346
614
 
347
615
  setTemplate({
348
616
  ...template,
349
- fields: reorderedFields
617
+ fields: reorderedFields,
350
618
  });
351
619
  };
352
620
 
@@ -363,23 +631,25 @@ const AgreementSignatureEditor = ({
363
631
  const handleSaveField = (fieldData) => {
364
632
  if (editingField) {
365
633
  // Update existing field
366
- const updatedFields = template.fields.map(field =>
367
- field.id === editingField.id ? { ...fieldData, id: editingField.id } : field
634
+ const updatedFields = template.fields.map((field) =>
635
+ field.id === editingField.id
636
+ ? { ...fieldData, id: editingField.id }
637
+ : field
368
638
  );
369
639
  setTemplate({
370
640
  ...template,
371
- fields: updatedFields
641
+ fields: updatedFields,
372
642
  });
373
643
  } else {
374
644
  // Add new field
375
645
  const newField = {
376
646
  ...fieldData,
377
647
  id: Date.now(), // Temporary ID - backend will assign real ID
378
- order: template.fields.length + 1
648
+ order: template.fields.length + 1,
379
649
  };
380
650
  setTemplate({
381
651
  ...template,
382
- fields: [...template.fields, newField]
652
+ fields: [...template.fields, newField],
383
653
  });
384
654
  }
385
655
  setShowFieldEditor(false);
@@ -388,62 +658,102 @@ const AgreementSignatureEditor = ({
388
658
 
389
659
  const handleDeleteField = (fieldId) => {
390
660
  if (!confirm('Are you sure you want to delete this field?')) return;
391
-
661
+
392
662
  const updatedFields = template.fields
393
- .filter(field => field.id !== fieldId)
663
+ .filter((field) => field.id !== fieldId)
394
664
  .map((field, index) => ({ ...field, order: index + 1 }));
395
-
665
+
396
666
  setTemplate({
397
667
  ...template,
398
- fields: updatedFields
668
+ fields: updatedFields,
399
669
  });
400
670
  };
401
671
 
672
+ const handleVariableInsert = (variableName) => {
673
+ const textarea = bodyTextRef.current;
674
+ if (!textarea) return;
675
+
676
+ const start = textarea.selectionStart;
677
+ const end = textarea.selectionEnd;
678
+ const text = textarea.value;
679
+ const before = text.substring(0, start);
680
+ const after = text.substring(end, text.length);
681
+ const variableText = `{{${variableName}}}`;
682
+
683
+ const newText = before + variableText + after;
684
+
685
+ setTemplate({
686
+ ...template,
687
+ bodyText: newText,
688
+ });
689
+
690
+ // Reset cursor position after the inserted variable
691
+ setTimeout(() => {
692
+ textarea.focus();
693
+ textarea.setSelectionRange(
694
+ start + variableText.length,
695
+ start + variableText.length
696
+ );
697
+ }, 0);
698
+
699
+ setShowVariableInserter(false);
700
+ };
701
+
402
702
  const handleSaveTemplate = async () => {
403
703
  try {
404
704
  setIsLoading(true);
405
-
705
+
406
706
  // For now, if no templateId or API is not available, just save locally
407
707
  if (!templateId) {
408
708
  // Update section with template reference
409
709
  const updatedSection = {
410
710
  ...section,
411
711
  template: template,
412
- content: generatePreviewContent(template)
712
+ content: generatePreviewContent(template),
413
713
  };
414
-
714
+
415
715
  onSave(updatedSection);
416
716
  toast.success('Agreement signature template saved locally');
417
717
  return;
418
718
  }
419
-
719
+
420
720
  // 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
- });
721
+ const response = await fetch(
722
+ buildUrlWithTemplate(
723
+ apiEndpoints.agreementSignatureTemplateSave ||
724
+ '/ajax/agreement-signature-template/{templateId}'
725
+ ),
726
+ {
727
+ method: 'POST',
728
+ headers: {
729
+ 'Content-Type': 'application/json',
730
+ 'X-CSRF-TOKEN': document
731
+ .querySelector('meta[name="csrf-token"]')
732
+ ?.getAttribute('content'),
733
+ },
734
+ body: JSON.stringify({
735
+ template: template,
736
+ }),
737
+ }
738
+ );
431
739
 
432
740
  // Check if the response is ok and contains JSON
433
741
  if (!response.ok) {
434
742
  throw new Error(`HTTP error! status: ${response.status}`);
435
743
  }
436
-
744
+
437
745
  const contentType = response.headers.get('content-type');
438
746
  if (!contentType || !contentType.includes('application/json')) {
439
747
  // If the API doesn't exist, save locally instead
440
- console.warn('Agreement signature template save API not available, saving locally');
748
+ console.warn(
749
+ 'Agreement signature template save API not available, saving locally'
750
+ );
441
751
  const updatedSection = {
442
752
  ...section,
443
753
  template: template,
444
- content: generatePreviewContent(template)
754
+ content: generatePreviewContent(template),
445
755
  };
446
-
756
+
447
757
  onSave(updatedSection);
448
758
  toast.success('Agreement signature template saved locally');
449
759
  return;
@@ -455,24 +765,27 @@ const AgreementSignatureEditor = ({
455
765
  const updatedSection = {
456
766
  ...section,
457
767
  template: data.template,
458
- content: generatePreviewContent(data.template)
768
+ content: generatePreviewContent(data.template),
459
769
  };
460
-
770
+
461
771
  onSave(updatedSection);
462
772
  toast.success('Agreement signature template saved');
463
773
  } else {
464
774
  throw new Error(data.message || 'Failed to save template');
465
775
  }
466
776
  } catch (error) {
467
- console.warn('Error saving template to backend, saving locally:', error.message);
468
-
777
+ console.warn(
778
+ 'Error saving template to backend, saving locally:',
779
+ error.message
780
+ );
781
+
469
782
  // Fallback to local save
470
783
  const updatedSection = {
471
784
  ...section,
472
785
  template: template,
473
- content: generatePreviewContent(template)
786
+ content: generatePreviewContent(template),
474
787
  };
475
-
788
+
476
789
  onSave(updatedSection);
477
790
  toast.success('Agreement signature template saved locally');
478
791
  } finally {
@@ -483,10 +796,15 @@ const AgreementSignatureEditor = ({
483
796
  const generatePreviewContent = (templateData) => {
484
797
  const fieldsHtml = templateData.fields
485
798
  .sort((a, b) => a.order - b.order)
486
- .map(field => {
799
+ .map((field) => {
487
800
  const required = field.required ? ' *' : '';
488
- const width = field.width === 'full' ? '100%' : field.width === 'half' ? '48%' : '30%';
489
-
801
+ const width =
802
+ field.width === 'full'
803
+ ? '100%'
804
+ : field.width === 'half'
805
+ ? '48%'
806
+ : '30%';
807
+
490
808
  switch (field.type) {
491
809
  case 'signature':
492
810
  return `<div style="margin-bottom: 2rem; width: ${width}; display: inline-block; margin-right: 2%;">
@@ -508,11 +826,16 @@ const AgreementSignatureEditor = ({
508
826
  .join('');
509
827
 
510
828
  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>
829
+ ${
830
+ templateData.headerText
831
+ ? `<h3 style="margin-bottom: 1rem; color: #333;">${templateData.headerText}</h3>`
832
+ : ''
833
+ }
834
+ <p style="margin-bottom: 2rem; line-height: 1.6;">${
835
+ templateData.bodyText
836
+ }</p>
514
837
  <div style="clear: both;">${fieldsHtml}</div>
515
- </div>
838
+
516
839
  `;
517
840
  };
518
841
 
@@ -529,16 +852,26 @@ const AgreementSignatureEditor = ({
529
852
  <div className="signature-editor-container">
530
853
  <div className="signature-editor-header">
531
854
  <h3>Agreement Signature Template</h3>
532
- <p>Customize the text and fields for your agreement signature section</p>
855
+ <p>
856
+ Customize the text and fields for your agreement signature
857
+ section
858
+ </p>
533
859
  </div>
534
860
 
535
861
  <div className="signature-editor-content">
536
862
  <div className="signature-editor-section">
537
- <label className="signature-editor-label">Header Text (Optional)</label>
863
+ <label className="signature-editor-label">
864
+ Header Text (Optional)
865
+ </label>
538
866
  <input
539
867
  type="text"
540
868
  value={template.headerText}
541
- onChange={(e) => setTemplate({...template, headerText: e.target.value})}
869
+ onChange={(e) =>
870
+ setTemplate({
871
+ ...template,
872
+ headerText: e.target.value,
873
+ })
874
+ }
542
875
  placeholder="e.g., APPROVAL TO PROCEED (leave empty for no header)"
543
876
  className="signature-editor-input"
544
877
  />
@@ -548,22 +881,45 @@ const AgreementSignatureEditor = ({
548
881
  </div>
549
882
 
550
883
  <div className="signature-editor-section">
551
- <label className="signature-editor-label">Body Text</label>
884
+ <div className="signature-editor-label-with-button">
885
+ <label className="signature-editor-label">
886
+ Body Text
887
+ </label>
888
+ <button
889
+ type="button"
890
+ onClick={() => setShowVariableInserter(true)}
891
+ className="signature-btn secondary small"
892
+ title="Insert Variable"
893
+ >
894
+ <Tag size={16} />
895
+ Insert Variable
896
+ </button>
897
+ </div>
552
898
  <textarea
899
+ ref={bodyTextRef}
553
900
  value={template.bodyText}
554
- onChange={(e) => setTemplate({...template, bodyText: e.target.value})}
901
+ onChange={(e) =>
902
+ setTemplate({
903
+ ...template,
904
+ bodyText: e.target.value,
905
+ })
906
+ }
555
907
  placeholder="Enter the agreement text. Use {{variable_name}} for dynamic content."
556
908
  className="signature-editor-textarea"
557
909
  rows="3"
558
910
  />
559
911
  <small className="signature-editor-help">
560
- Use variables like {'{{company_name}}'} for dynamic content
912
+ Use variables like {'{{company_name}}'} for dynamic
913
+ content. Click "Insert Variable" to browse available
914
+ variables.
561
915
  </small>
562
916
  </div>
563
917
 
564
918
  <div className="signature-editor-section">
565
919
  <div className="signature-editor-fields-header">
566
- <label className="signature-editor-label">Signature Fields</label>
920
+ <label className="signature-editor-label">
921
+ Signature Fields
922
+ </label>
567
923
  <button
568
924
  onClick={handleAddField}
569
925
  className="signature-btn primary small"
@@ -577,7 +933,10 @@ const AgreementSignatureEditor = ({
577
933
  <div className="signature-editor-empty">
578
934
  <Type size={48} />
579
935
  <p>No fields added yet</p>
580
- <button onClick={handleAddField} className="signature-btn primary">
936
+ <button
937
+ onClick={handleAddField}
938
+ className="signature-btn primary"
939
+ >
581
940
  Add Your First Field
582
941
  </button>
583
942
  </div>
@@ -596,9 +955,11 @@ const AgreementSignatureEditor = ({
596
955
 
597
956
  <div className="signature-editor-preview">
598
957
  <h4>Preview</h4>
599
- <div
958
+ <div
600
959
  className="signature-editor-preview-content"
601
- dangerouslySetInnerHTML={{ __html: generatePreviewContent(template) }}
960
+ dangerouslySetInnerHTML={{
961
+ __html: generatePreviewContent(template),
962
+ }}
602
963
  />
603
964
  </div>
604
965
  </div>
@@ -637,8 +998,16 @@ const AgreementSignatureEditor = ({
637
998
  </div>
638
999
  </div>
639
1000
  )}
1001
+
1002
+ {showVariableInserter && (
1003
+ <VariableInserter
1004
+ onInsert={handleVariableInsert}
1005
+ onCancel={() => setShowVariableInserter(false)}
1006
+ apiEndpoints={apiEndpoints}
1007
+ />
1008
+ )}
640
1009
  </div>
641
1010
  );
642
1011
  };
643
1012
 
644
- export default AgreementSignatureEditor;
1013
+ export default AgreementSignatureEditor;