@visns-studio/visns-components 5.13.8 → 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.
- package/package.json +1 -1
- package/src/components/generic/GenericDetail.jsx +148 -4
- package/src/components/generic/GenericEditableTable.jsx +40 -29
- package/src/components/proposal/AgreementSignatureEditor.css +491 -0
- package/src/components/proposal/AgreementSignatureEditor.jsx +644 -0
- package/src/components/proposal/BrandingProfileCompanyInfo.css +213 -0
- package/src/components/proposal/BrandingProfileCompanyInfo.jsx +194 -0
- package/src/components/proposal/HeaderConfigEditor.css +412 -0
- package/src/components/proposal/HeaderConfigEditor.jsx +219 -0
- package/src/components/proposal/ProposalTemplatePreview.css +53 -53
- package/src/components/proposal/ProposalTemplateSectionManager.css +69 -50
- package/src/components/proposal/ProposalTemplateSectionManager.jsx +87 -8
- package/src/components/proposal/SectionEditor.css +54 -54
- package/src/components/proposal/SectionEditor.jsx +31 -3
- package/src/components/proposal/SectionTypeSelector.css +29 -43
- package/src/components/proposal/index.js +4 -1
- package/src/components/styles/GenericDetail.module.scss +104 -0
- package/src/utils/urlBuilder.js +5 -1
|
@@ -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;
|