@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.
|
|
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% {
|
|
325
|
-
|
|
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=
|
|
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 {
|
|
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
|
|
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
|
|
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) =>
|
|
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) =>
|
|
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) =>
|
|
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) =>
|
|
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) =>
|
|
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
|
|
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
|
|
191
|
-
|
|
192
|
-
|
|
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
|
|
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
|
|
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
|
-
{
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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:
|
|
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
|
-
{
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
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(
|
|
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(
|
|
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:
|
|
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
|
-
{
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
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:
|
|
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
|
-
{
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
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(
|
|
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:
|
|
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
|
-
{
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
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(
|
|
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
|
|
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(
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
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(
|
|
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(
|
|
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 =
|
|
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
|
-
|
|
512
|
-
|
|
513
|
-
|
|
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
|
-
|
|
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>
|
|
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">
|
|
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) =>
|
|
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
|
-
<
|
|
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) =>
|
|
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
|
|
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">
|
|
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
|
|
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={{
|
|
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;
|