@visns-studio/visns-components 5.13.13 → 5.13.15

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.13",
91
+ "version": "5.13.15",
92
92
  "description": "Various packages to assist in the development of our Custom Applications.",
93
93
  "main": "src/index.js",
94
94
  "files": [
@@ -1249,85 +1249,77 @@ const GenericEditableTable = ({
1249
1249
  if (!hasMarginRow) return null;
1250
1250
 
1251
1251
  const totalProjectCost = calculateTotalProjectCost(localData);
1252
+ const gst = totalProjectCost * 0.1;
1253
+ const totalWithGst = totalProjectCost + gst;
1252
1254
  const marginAmount = calculateMarginAmount(
1253
1255
  totalProjectCost,
1254
1256
  marginPercentage
1255
1257
  );
1256
- const totalWithMargin = totalProjectCost + marginAmount;
1258
+ const totalLkdProjectCost = totalProjectCost + marginAmount;
1259
+ const totalLkdGst = totalLkdProjectCost * 0.1;
1260
+ const totalLkdWithGst = totalLkdProjectCost + totalLkdGst;
1257
1261
 
1258
1262
  return (
1259
1263
  <>
1260
- {/* Margin row category header */}
1261
- <tr className={styles.categoryRow}>
1264
+ {/* Total Project Cost */}
1265
+ <tr style={{ backgroundColor: '#e9ecef', fontWeight: 'bold' }}>
1266
+ <td colSpan={6} style={{ padding: '8px' }}>
1267
+ Total Project Cost
1268
+ </td>
1262
1269
  <td
1263
- colSpan={columns.length + 2}
1264
- style={{
1265
- backgroundColor: '#e9ecef',
1266
- fontWeight: 'bold',
1267
- padding: '8px 12px',
1268
- }}
1270
+ colSpan={4}
1271
+ style={{ padding: '8px', textAlign: 'right' }}
1269
1272
  >
1270
- {rows.marginRow}
1273
+ {totalProjectCost.toLocaleString('en-AU', {
1274
+ style: 'currency',
1275
+ currency: 'AUD',
1276
+ })}
1271
1277
  </td>
1278
+ <td></td>
1272
1279
  </tr>
1273
1280
 
1274
- {/* Margin row column headers - only show Description, Margin %, and Total */}
1275
- <tr>
1276
- {/* Description column - larger width */}
1277
- <th
1278
- style={{
1279
- color: 'white',
1280
- fontWeight: 'bold',
1281
- padding: '8px',
1282
- textAlign: 'left',
1283
- width: '60%',
1284
- }}
1285
- colSpan={6}
1286
- >
1287
- Description
1288
- </th>
1289
- {/* Margin % column - smaller width */}
1290
- <th
1291
- style={{
1292
- color: 'white',
1293
- fontWeight: 'bold',
1294
- padding: '8px',
1295
- textAlign: 'left',
1296
- width: '20%',
1297
- }}
1298
- colSpan={2}
1299
- >
1300
- Margin %
1301
- </th>
1302
- {/* Total column */}
1303
- <th
1304
- style={{
1305
- color: 'white',
1306
- fontWeight: 'bold',
1307
- padding: '8px',
1308
- textAlign: 'right',
1309
- width: '20%',
1310
- }}
1311
- colSpan={2}
1281
+ {/* GST */}
1282
+ <tr style={{ backgroundColor: '#e9ecef' }}>
1283
+ <td colSpan={6} style={{ padding: '8px' }}>
1284
+ GST
1285
+ </td>
1286
+ <td
1287
+ colSpan={4}
1288
+ style={{ padding: '8px', textAlign: 'right' }}
1312
1289
  >
1313
- Total
1314
- </th>
1315
- <th></th>
1290
+ {gst.toLocaleString('en-AU', {
1291
+ style: 'currency',
1292
+ currency: 'AUD',
1293
+ })}
1294
+ </td>
1295
+ <td></td>
1316
1296
  </tr>
1317
1297
 
1318
- {/* Margin row content */}
1319
- <tr style={{ backgroundColor: '#f8f9fa' }}>
1320
- {/* Description */}
1298
+ {/* Total Project Cost (Including GST) */}
1299
+ <tr style={{ backgroundColor: '#e9ecef', fontWeight: 'bold' }}>
1300
+ <td colSpan={6} style={{ padding: '8px' }}>
1301
+ Total Project Cost (Including GST)
1302
+ </td>
1321
1303
  <td
1322
- style={{ fontWeight: 'bold', padding: '8px' }}
1323
- colSpan={6}
1304
+ colSpan={4}
1305
+ style={{ padding: '8px', textAlign: 'right' }}
1324
1306
  >
1325
- {rows.marginRow}
1307
+ {totalWithGst.toLocaleString('en-AU', {
1308
+ style: 'currency',
1309
+ currency: 'AUD',
1310
+ })}
1311
+ </td>
1312
+ <td></td>
1313
+ </tr>
1314
+
1315
+ {/* LKD Margin */}
1316
+ <tr style={{ backgroundColor: '#f8f9fa' }}>
1317
+ <td colSpan={6} style={{ padding: '8px' }}>
1318
+ LKD Margin (in %)
1326
1319
  </td>
1327
- {/* Margin % input */}
1328
1320
  <td
1329
- style={{ textAlign: 'center', padding: '8px' }}
1330
1321
  colSpan={2}
1322
+ style={{ padding: '8px', textAlign: 'center' }}
1331
1323
  >
1332
1324
  <input
1333
1325
  type="number"
@@ -1337,25 +1329,72 @@ const GenericEditableTable = ({
1337
1329
  parseFloat(e.target.value) || 0
1338
1330
  )
1339
1331
  }
1340
- placeholder="% margin"
1332
+ placeholder="0"
1341
1333
  style={{
1342
- width: '80px',
1343
- padding: '6px',
1334
+ padding: '4px',
1344
1335
  border: '1px solid #ccc',
1345
1336
  borderRadius: '3px',
1337
+ textAlign: 'center',
1338
+ marginRight: '4px',
1346
1339
  }}
1347
1340
  />
1348
1341
  </td>
1349
- {/* Total (subtotal of all categories + margin) */}
1350
1342
  <td
1351
- style={{
1352
- textAlign: 'right',
1353
- fontWeight: 'bold',
1354
- padding: '8px',
1355
- }}
1356
1343
  colSpan={2}
1344
+ style={{ padding: '8px', textAlign: 'right' }}
1345
+ >
1346
+ {marginAmount.toLocaleString('en-AU', {
1347
+ style: 'currency',
1348
+ currency: 'AUD',
1349
+ })}
1350
+ </td>
1351
+ <td></td>
1352
+ </tr>
1353
+
1354
+ {/* Total LKD Project Cost */}
1355
+ <tr style={{ backgroundColor: '#e9ecef', fontWeight: 'bold' }}>
1356
+ <td colSpan={6} style={{ padding: '8px' }}>
1357
+ Total LKD Project Cost
1358
+ </td>
1359
+ <td
1360
+ colSpan={4}
1361
+ style={{ padding: '8px', textAlign: 'right' }}
1362
+ >
1363
+ {totalLkdProjectCost.toLocaleString('en-AU', {
1364
+ style: 'currency',
1365
+ currency: 'AUD',
1366
+ })}
1367
+ </td>
1368
+ <td></td>
1369
+ </tr>
1370
+
1371
+ {/* Total GST */}
1372
+ <tr style={{ backgroundColor: '#e9ecef' }}>
1373
+ <td colSpan={6} style={{ padding: '8px' }}>
1374
+ Total GST
1375
+ </td>
1376
+ <td
1377
+ colSpan={4}
1378
+ style={{ padding: '8px', textAlign: 'right' }}
1379
+ >
1380
+ {totalLkdGst.toLocaleString('en-AU', {
1381
+ style: 'currency',
1382
+ currency: 'AUD',
1383
+ })}
1384
+ </td>
1385
+ <td></td>
1386
+ </tr>
1387
+
1388
+ {/* Total LKD Project Cost (Including GST) */}
1389
+ <tr style={{ backgroundColor: '#e9ecef', fontWeight: 'bold' }}>
1390
+ <td colSpan={6} style={{ padding: '8px' }}>
1391
+ Total LKD Project Cost (Including GST)
1392
+ </td>
1393
+ <td
1394
+ colSpan={4}
1395
+ style={{ padding: '8px', textAlign: 'right' }}
1357
1396
  >
1358
- {totalWithMargin.toLocaleString('en-AU', {
1397
+ {totalLkdWithGst.toLocaleString('en-AU', {
1359
1398
  style: 'currency',
1360
1399
  currency: 'AUD',
1361
1400
  })}
@@ -291,7 +291,6 @@
291
291
  }
292
292
 
293
293
  .signature-editor-preview-content {
294
- border: 1px solid var(--paragraph-color, #e2e8f0);
295
294
  border-radius: 6px;
296
295
  overflow: hidden;
297
296
  }
@@ -328,8 +327,12 @@
328
327
  }
329
328
 
330
329
  @keyframes spin {
331
- 0% { transform: rotate(0deg); }
332
- 100% { transform: rotate(360deg); }
330
+ 0% {
331
+ transform: rotate(0deg);
332
+ }
333
+ 100% {
334
+ transform: rotate(360deg);
335
+ }
333
336
  }
334
337
 
335
338
  /* Field Editor Modal */
@@ -448,7 +451,7 @@
448
451
  cursor: pointer;
449
452
  }
450
453
 
451
- .signature-field-editor-checkbox input[type="checkbox"] {
454
+ .signature-field-editor-checkbox input[type='checkbox'] {
452
455
  accent-color: var(--primary-color, #3cbf7d);
453
456
  }
454
457
 
@@ -465,34 +468,34 @@
465
468
  .signature-editor-container {
466
469
  padding: 1rem;
467
470
  }
468
-
471
+
469
472
  .signature-editor-fields-header {
470
473
  flex-direction: column;
471
474
  align-items: flex-start;
472
475
  gap: 1rem;
473
476
  }
474
-
477
+
475
478
  .signature-field-header {
476
479
  flex-direction: column;
477
480
  align-items: flex-start;
478
481
  gap: 0.5rem;
479
482
  }
480
-
483
+
481
484
  .signature-field-actions {
482
485
  width: 100%;
483
486
  justify-content: flex-end;
484
487
  }
485
-
488
+
486
489
  .signature-editor-actions {
487
490
  flex-direction: column;
488
491
  }
489
-
492
+
490
493
  .signature-field-editor-grid {
491
494
  grid-template-columns: 1fr;
492
495
  }
493
-
496
+
494
497
  .signature-editor-modal {
495
498
  width: 95%;
496
499
  margin: 1rem;
497
500
  }
498
- }
501
+ }
@@ -1,6 +1,10 @@
1
1
  import React, { useState, useEffect, useRef } from 'react';
2
2
  import { Plus, Trash2, Move, Type, Edit3, Save, X, Tag } from 'lucide-react';
3
- import { SortableContainer, SortableElement, SortableHandle } from 'react-sortable-hoc';
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';
@@ -66,7 +70,7 @@ const FieldEditor = ({ field, onSave, onCancel }) => {
66
70
  type: field?.type || 'text',
67
71
  placeholder: field?.placeholder || '',
68
72
  required: field?.required || false,
69
- width: field?.width || 'full'
73
+ width: field?.width || 'full',
70
74
  });
71
75
 
72
76
  const fieldTypes = [
@@ -74,13 +78,13 @@ const FieldEditor = ({ field, onSave, onCancel }) => {
74
78
  { value: 'signature', label: 'Signature Block' },
75
79
  { value: 'date', label: 'Date Field' },
76
80
  { value: 'select', label: 'Dropdown' },
77
- { value: 'textarea', label: 'Text Area' }
81
+ { value: 'textarea', label: 'Text Area' },
78
82
  ];
79
83
 
80
84
  const fieldWidths = [
81
85
  { value: 'full', label: 'Full Width' },
82
86
  { value: 'half', label: 'Half Width' },
83
- { value: 'third', label: 'Third Width' }
87
+ { value: 'third', label: 'Third Width' },
84
88
  ];
85
89
 
86
90
  const handleSubmit = (e) => {
@@ -96,55 +100,81 @@ const FieldEditor = ({ field, onSave, onCancel }) => {
96
100
  <div className="signature-field-editor">
97
101
  <div className="signature-field-editor-header">
98
102
  <h4>{field ? 'Edit Field' : 'Add New Field'}</h4>
99
- <button onClick={onCancel} className="signature-field-editor-close">
103
+ <button
104
+ onClick={onCancel}
105
+ className="signature-field-editor-close"
106
+ >
100
107
  <X size={16} />
101
108
  </button>
102
109
  </div>
103
-
104
- <form onSubmit={handleSubmit} className="signature-field-editor-form">
110
+
111
+ <form
112
+ onSubmit={handleSubmit}
113
+ className="signature-field-editor-form"
114
+ >
105
115
  <div className="signature-field-editor-grid">
106
116
  <div className="signature-field-editor-group">
107
117
  <label>Field Label *</label>
108
118
  <input
109
119
  type="text"
110
120
  value={formData.label}
111
- onChange={(e) => setFormData({...formData, label: e.target.value})}
121
+ onChange={(e) =>
122
+ setFormData({
123
+ ...formData,
124
+ label: e.target.value,
125
+ })
126
+ }
112
127
  placeholder="e.g., Name, Company Name, ABN"
113
128
  required
114
129
  />
115
130
  </div>
116
-
131
+
117
132
  <div className="signature-field-editor-group">
118
133
  <label>Field Type</label>
119
134
  <select
120
135
  value={formData.type}
121
- onChange={(e) => setFormData({...formData, type: e.target.value})}
136
+ onChange={(e) =>
137
+ setFormData({
138
+ ...formData,
139
+ type: e.target.value,
140
+ })
141
+ }
122
142
  >
123
- {fieldTypes.map(type => (
143
+ {fieldTypes.map((type) => (
124
144
  <option key={type.value} value={type.value}>
125
145
  {type.label}
126
146
  </option>
127
147
  ))}
128
148
  </select>
129
149
  </div>
130
-
150
+
131
151
  <div className="signature-field-editor-group">
132
152
  <label>Placeholder Text</label>
133
153
  <input
134
154
  type="text"
135
155
  value={formData.placeholder}
136
- onChange={(e) => setFormData({...formData, placeholder: e.target.value})}
156
+ onChange={(e) =>
157
+ setFormData({
158
+ ...formData,
159
+ placeholder: e.target.value,
160
+ })
161
+ }
137
162
  placeholder="Optional placeholder text"
138
163
  />
139
164
  </div>
140
-
165
+
141
166
  <div className="signature-field-editor-group">
142
167
  <label>Field Width</label>
143
168
  <select
144
169
  value={formData.width}
145
- onChange={(e) => setFormData({...formData, width: e.target.value})}
170
+ onChange={(e) =>
171
+ setFormData({
172
+ ...formData,
173
+ width: e.target.value,
174
+ })
175
+ }
146
176
  >
147
- {fieldWidths.map(width => (
177
+ {fieldWidths.map((width) => (
148
178
  <option key={width.value} value={width.value}>
149
179
  {width.label}
150
180
  </option>
@@ -152,20 +182,29 @@ const FieldEditor = ({ field, onSave, onCancel }) => {
152
182
  </select>
153
183
  </div>
154
184
  </div>
155
-
185
+
156
186
  <div className="signature-field-editor-checkbox">
157
187
  <label>
158
188
  <input
159
189
  type="checkbox"
160
190
  checked={formData.required}
161
- onChange={(e) => setFormData({...formData, required: e.target.checked})}
191
+ onChange={(e) =>
192
+ setFormData({
193
+ ...formData,
194
+ required: e.target.checked,
195
+ })
196
+ }
162
197
  />
163
198
  Required field
164
199
  </label>
165
200
  </div>
166
-
201
+
167
202
  <div className="signature-field-editor-actions">
168
- <button type="button" onClick={onCancel} className="signature-btn secondary">
203
+ <button
204
+ type="button"
205
+ onClick={onCancel}
206
+ className="signature-btn secondary"
207
+ >
169
208
  Cancel
170
209
  </button>
171
210
  <button type="submit" className="signature-btn primary">
@@ -178,21 +217,25 @@ const FieldEditor = ({ field, onSave, onCancel }) => {
178
217
  );
179
218
  };
180
219
 
181
- const AgreementSignatureEditor = ({
182
- section,
220
+ const AgreementSignatureEditor = ({
221
+ section,
183
222
  templateId,
184
- onSave,
223
+ onSave,
185
224
  onCancel,
186
225
  apiEndpoints = defaultProposalApiEndpoints,
187
226
  hideFooter = false,
188
- onTemplateChange = null
227
+ onTemplateChange = null,
189
228
  }) => {
190
229
  const [template, setTemplate] = useState({
191
- headerText: section?.template ? section.template.headerText || '' : 'APPROVAL TO PROCEED',
192
- 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.',
193
- 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 || [],
194
237
  });
195
-
238
+
196
239
  const [editingField, setEditingField] = useState(null);
197
240
  const [showFieldEditor, setShowFieldEditor] = useState(false);
198
241
  const [isLoading, setIsLoading] = useState(false);
@@ -208,28 +251,72 @@ const AgreementSignatureEditor = ({
208
251
  // Parse HTML content to extract template data
209
252
  const parser = new DOMParser();
210
253
  const doc = parser.parseFromString(content, 'text/html');
211
-
254
+
212
255
  // Extract header text from h3 tag
213
256
  const headerElement = doc.querySelector('h3');
214
- const headerText = headerElement ? headerElement.textContent.trim() : '';
215
-
257
+ const headerText = headerElement
258
+ ? headerElement.textContent.trim()
259
+ : '';
260
+
216
261
  // Extract body text from p tag
217
262
  const bodyElement = doc.querySelector('p');
218
- 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.';
219
-
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
+
220
267
  // Extract fields from the content (basic parsing)
221
268
  const defaultFields = [
222
- { id: 1, label: 'Name', type: 'text', placeholder: '', required: true, width: 'full', order: 1 },
223
- { id: 2, label: 'Company Name', type: 'text', placeholder: '', required: true, width: 'full', order: 2 },
224
- { id: 3, label: 'ABN', type: 'text', placeholder: '', required: false, width: 'full', order: 3 },
225
- { id: 4, label: 'Signature', type: 'signature', placeholder: '', required: true, width: 'full', order: 4 },
226
- { 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
+ },
227
314
  ];
228
-
315
+
229
316
  return {
230
317
  headerText: headerText,
231
318
  bodyText: bodyText,
232
- fields: defaultFields
319
+ fields: defaultFields,
233
320
  };
234
321
  };
235
322
 
@@ -251,7 +338,7 @@ const AgreementSignatureEditor = ({
251
338
  const updatedSection = {
252
339
  ...section,
253
340
  template: template,
254
- content: generatePreviewContent(template)
341
+ content: generatePreviewContent(template),
255
342
  };
256
343
  onTemplateChange(updatedSection);
257
344
  }
@@ -262,76 +349,250 @@ const AgreementSignatureEditor = ({
262
349
  // Load default template when no templateId is provided
263
350
  setTemplate({
264
351
  headerText: 'APPROVAL TO PROCEED',
265
- 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.',
266
354
  fields: [
267
- { id: 1, label: 'Name', type: 'text', placeholder: '', required: true, width: 'full', order: 1 },
268
- { id: 2, label: 'Company Name', type: 'text', placeholder: '', required: true, width: 'full', order: 2 },
269
- { id: 3, label: 'ABN', type: 'text', placeholder: '', required: false, width: 'full', order: 3 },
270
- { id: 4, label: 'Signature', type: 'signature', placeholder: '', required: true, width: 'full', order: 4 },
271
- { id: 5, label: 'Date', type: 'date', placeholder: '', required: true, width: 'full', order: 5 }
272
- ]
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
+ ],
273
401
  });
274
402
  return;
275
403
  }
276
-
404
+
277
405
  try {
278
406
  setIsLoading(true);
279
- const response = await fetch(buildUrlWithTemplate(apiEndpoints.agreementSignatureTemplate || '/ajax/agreement-signature-template/{templateId}'));
280
-
407
+ const response = await fetch(
408
+ buildUrlWithTemplate(
409
+ apiEndpoints.agreementSignatureTemplate ||
410
+ '/ajax/agreement-signature-template/{templateId}'
411
+ )
412
+ );
413
+
281
414
  // Check if the response is ok and contains JSON
282
415
  if (!response.ok) {
283
416
  throw new Error(`HTTP error! status: ${response.status}`);
284
417
  }
285
-
418
+
286
419
  const contentType = response.headers.get('content-type');
287
420
  if (!contentType || !contentType.includes('application/json')) {
288
421
  // If the API doesn't exist or returns HTML, use default template
289
- 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
+ );
290
425
  setTemplate({
291
426
  headerText: 'APPROVAL TO PROCEED',
292
- 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.',
293
429
  fields: [
294
- { id: 1, label: 'Name', type: 'text', placeholder: '', required: true, width: 'full', order: 1 },
295
- { id: 2, label: 'Company Name', type: 'text', placeholder: '', required: true, width: 'full', order: 2 },
296
- { id: 3, label: 'ABN', type: 'text', placeholder: '', required: false, width: 'full', order: 3 },
297
- { id: 4, label: 'Signature', type: 'signature', placeholder: '', required: true, width: 'full', order: 4 },
298
- { id: 5, label: 'Date', type: 'date', placeholder: '', required: true, width: 'full', order: 5 }
299
- ]
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
+ ],
300
476
  });
301
477
  return;
302
478
  }
303
-
479
+
304
480
  const data = await response.json();
305
-
481
+
306
482
  if (data.success) {
307
483
  setTemplate(data.template);
308
484
  } else {
309
485
  // Use default template if none exists
310
486
  setTemplate({
311
487
  headerText: 'APPROVAL TO PROCEED',
312
- 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.',
313
490
  fields: [
314
- { id: 1, label: 'Name', type: 'text', placeholder: '', required: true, width: 'full', order: 1 },
315
- { id: 2, label: 'Company Name', type: 'text', placeholder: '', required: true, width: 'full', order: 2 },
316
- { id: 3, label: 'ABN', type: 'text', placeholder: '', required: false, width: 'full', order: 3 },
317
- { id: 4, label: 'Signature', type: 'signature', placeholder: '', required: true, width: 'full', order: 4 },
318
- { id: 5, label: 'Date', type: 'date', placeholder: '', required: true, width: 'full', order: 5 }
319
- ]
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
+ ],
320
537
  });
321
538
  }
322
539
  } catch (error) {
323
- 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
+ );
324
544
  // Use default template instead of showing error
325
545
  setTemplate({
326
546
  headerText: 'APPROVAL TO PROCEED',
327
- 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.',
328
549
  fields: [
329
- { id: 1, label: 'Name', type: 'text', placeholder: '', required: true, width: 'full', order: 1 },
330
- { id: 2, label: 'Company Name', type: 'text', placeholder: '', required: true, width: 'full', order: 2 },
331
- { id: 3, label: 'ABN', type: 'text', placeholder: '', required: false, width: 'full', order: 3 },
332
- { id: 4, label: 'Signature', type: 'signature', placeholder: '', required: true, width: 'full', order: 4 },
333
- { id: 5, label: 'Date', type: 'date', placeholder: '', required: true, width: 'full', order: 5 }
334
- ]
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
+ ],
335
596
  });
336
597
  } finally {
337
598
  setIsLoading(false);
@@ -341,15 +602,19 @@ const AgreementSignatureEditor = ({
341
602
  const handleFieldsReorder = ({ oldIndex, newIndex }) => {
342
603
  if (oldIndex === newIndex) return;
343
604
 
344
- const newFields = arrayMoveImmutable(template.fields, oldIndex, newIndex);
605
+ const newFields = arrayMoveImmutable(
606
+ template.fields,
607
+ oldIndex,
608
+ newIndex
609
+ );
345
610
  const reorderedFields = newFields.map((field, index) => ({
346
611
  ...field,
347
- order: index + 1
612
+ order: index + 1,
348
613
  }));
349
614
 
350
615
  setTemplate({
351
616
  ...template,
352
- fields: reorderedFields
617
+ fields: reorderedFields,
353
618
  });
354
619
  };
355
620
 
@@ -366,23 +631,25 @@ const AgreementSignatureEditor = ({
366
631
  const handleSaveField = (fieldData) => {
367
632
  if (editingField) {
368
633
  // Update existing field
369
- const updatedFields = template.fields.map(field =>
370
- 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
371
638
  );
372
639
  setTemplate({
373
640
  ...template,
374
- fields: updatedFields
641
+ fields: updatedFields,
375
642
  });
376
643
  } else {
377
644
  // Add new field
378
645
  const newField = {
379
646
  ...fieldData,
380
647
  id: Date.now(), // Temporary ID - backend will assign real ID
381
- order: template.fields.length + 1
648
+ order: template.fields.length + 1,
382
649
  };
383
650
  setTemplate({
384
651
  ...template,
385
- fields: [...template.fields, newField]
652
+ fields: [...template.fields, newField],
386
653
  });
387
654
  }
388
655
  setShowFieldEditor(false);
@@ -391,14 +658,14 @@ const AgreementSignatureEditor = ({
391
658
 
392
659
  const handleDeleteField = (fieldId) => {
393
660
  if (!confirm('Are you sure you want to delete this field?')) return;
394
-
661
+
395
662
  const updatedFields = template.fields
396
- .filter(field => field.id !== fieldId)
663
+ .filter((field) => field.id !== fieldId)
397
664
  .map((field, index) => ({ ...field, order: index + 1 }));
398
-
665
+
399
666
  setTemplate({
400
667
  ...template,
401
- fields: updatedFields
668
+ fields: updatedFields,
402
669
  });
403
670
  };
404
671
 
@@ -412,68 +679,81 @@ const AgreementSignatureEditor = ({
412
679
  const before = text.substring(0, start);
413
680
  const after = text.substring(end, text.length);
414
681
  const variableText = `{{${variableName}}}`;
415
-
682
+
416
683
  const newText = before + variableText + after;
417
-
684
+
418
685
  setTemplate({
419
686
  ...template,
420
- bodyText: newText
687
+ bodyText: newText,
421
688
  });
422
689
 
423
690
  // Reset cursor position after the inserted variable
424
691
  setTimeout(() => {
425
692
  textarea.focus();
426
- textarea.setSelectionRange(start + variableText.length, start + variableText.length);
693
+ textarea.setSelectionRange(
694
+ start + variableText.length,
695
+ start + variableText.length
696
+ );
427
697
  }, 0);
428
-
698
+
429
699
  setShowVariableInserter(false);
430
700
  };
431
701
 
432
702
  const handleSaveTemplate = async () => {
433
703
  try {
434
704
  setIsLoading(true);
435
-
705
+
436
706
  // For now, if no templateId or API is not available, just save locally
437
707
  if (!templateId) {
438
708
  // Update section with template reference
439
709
  const updatedSection = {
440
710
  ...section,
441
711
  template: template,
442
- content: generatePreviewContent(template)
712
+ content: generatePreviewContent(template),
443
713
  };
444
-
714
+
445
715
  onSave(updatedSection);
446
716
  toast.success('Agreement signature template saved locally');
447
717
  return;
448
718
  }
449
-
719
+
450
720
  // Save template to backend
451
- const response = await fetch(buildUrlWithTemplate(apiEndpoints.agreementSignatureTemplateSave || '/ajax/agreement-signature-template/{templateId}'), {
452
- method: 'POST',
453
- headers: {
454
- 'Content-Type': 'application/json',
455
- 'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]')?.getAttribute('content')
456
- },
457
- body: JSON.stringify({
458
- template: template
459
- })
460
- });
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
+ );
461
739
 
462
740
  // Check if the response is ok and contains JSON
463
741
  if (!response.ok) {
464
742
  throw new Error(`HTTP error! status: ${response.status}`);
465
743
  }
466
-
744
+
467
745
  const contentType = response.headers.get('content-type');
468
746
  if (!contentType || !contentType.includes('application/json')) {
469
747
  // If the API doesn't exist, save locally instead
470
- 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
+ );
471
751
  const updatedSection = {
472
752
  ...section,
473
753
  template: template,
474
- content: generatePreviewContent(template)
754
+ content: generatePreviewContent(template),
475
755
  };
476
-
756
+
477
757
  onSave(updatedSection);
478
758
  toast.success('Agreement signature template saved locally');
479
759
  return;
@@ -485,24 +765,27 @@ const AgreementSignatureEditor = ({
485
765
  const updatedSection = {
486
766
  ...section,
487
767
  template: data.template,
488
- content: generatePreviewContent(data.template)
768
+ content: generatePreviewContent(data.template),
489
769
  };
490
-
770
+
491
771
  onSave(updatedSection);
492
772
  toast.success('Agreement signature template saved');
493
773
  } else {
494
774
  throw new Error(data.message || 'Failed to save template');
495
775
  }
496
776
  } catch (error) {
497
- console.warn('Error saving template to backend, saving locally:', error.message);
498
-
777
+ console.warn(
778
+ 'Error saving template to backend, saving locally:',
779
+ error.message
780
+ );
781
+
499
782
  // Fallback to local save
500
783
  const updatedSection = {
501
784
  ...section,
502
785
  template: template,
503
- content: generatePreviewContent(template)
786
+ content: generatePreviewContent(template),
504
787
  };
505
-
788
+
506
789
  onSave(updatedSection);
507
790
  toast.success('Agreement signature template saved locally');
508
791
  } finally {
@@ -513,10 +796,15 @@ const AgreementSignatureEditor = ({
513
796
  const generatePreviewContent = (templateData) => {
514
797
  const fieldsHtml = templateData.fields
515
798
  .sort((a, b) => a.order - b.order)
516
- .map(field => {
799
+ .map((field) => {
517
800
  const required = field.required ? ' *' : '';
518
- const width = field.width === 'full' ? '100%' : field.width === 'half' ? '48%' : '30%';
519
-
801
+ const width =
802
+ field.width === 'full'
803
+ ? '100%'
804
+ : field.width === 'half'
805
+ ? '48%'
806
+ : '30%';
807
+
520
808
  switch (field.type) {
521
809
  case 'signature':
522
810
  return `<div style="margin-bottom: 2rem; width: ${width}; display: inline-block; margin-right: 2%;">
@@ -538,11 +826,16 @@ const AgreementSignatureEditor = ({
538
826
  .join('');
539
827
 
540
828
  return `
541
- <div style="padding: 2rem;">
542
- ${templateData.headerText ? `<h3 style="margin-bottom: 1rem; color: #333;">${templateData.headerText}</h3>` : ''}
543
- <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>
544
837
  <div style="clear: both;">${fieldsHtml}</div>
545
- </div>
838
+
546
839
  `;
547
840
  };
548
841
 
@@ -559,16 +852,26 @@ const AgreementSignatureEditor = ({
559
852
  <div className="signature-editor-container">
560
853
  <div className="signature-editor-header">
561
854
  <h3>Agreement Signature Template</h3>
562
- <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>
563
859
  </div>
564
860
 
565
861
  <div className="signature-editor-content">
566
862
  <div className="signature-editor-section">
567
- <label className="signature-editor-label">Header Text (Optional)</label>
863
+ <label className="signature-editor-label">
864
+ Header Text (Optional)
865
+ </label>
568
866
  <input
569
867
  type="text"
570
868
  value={template.headerText}
571
- onChange={(e) => setTemplate({...template, headerText: e.target.value})}
869
+ onChange={(e) =>
870
+ setTemplate({
871
+ ...template,
872
+ headerText: e.target.value,
873
+ })
874
+ }
572
875
  placeholder="e.g., APPROVAL TO PROCEED (leave empty for no header)"
573
876
  className="signature-editor-input"
574
877
  />
@@ -579,7 +882,9 @@ const AgreementSignatureEditor = ({
579
882
 
580
883
  <div className="signature-editor-section">
581
884
  <div className="signature-editor-label-with-button">
582
- <label className="signature-editor-label">Body Text</label>
885
+ <label className="signature-editor-label">
886
+ Body Text
887
+ </label>
583
888
  <button
584
889
  type="button"
585
890
  onClick={() => setShowVariableInserter(true)}
@@ -593,19 +898,28 @@ const AgreementSignatureEditor = ({
593
898
  <textarea
594
899
  ref={bodyTextRef}
595
900
  value={template.bodyText}
596
- onChange={(e) => setTemplate({...template, bodyText: e.target.value})}
901
+ onChange={(e) =>
902
+ setTemplate({
903
+ ...template,
904
+ bodyText: e.target.value,
905
+ })
906
+ }
597
907
  placeholder="Enter the agreement text. Use {{variable_name}} for dynamic content."
598
908
  className="signature-editor-textarea"
599
909
  rows="3"
600
910
  />
601
911
  <small className="signature-editor-help">
602
- Use variables like {'{{company_name}}'} for dynamic content. Click "Insert Variable" to browse available variables.
912
+ Use variables like {'{{company_name}}'} for dynamic
913
+ content. Click "Insert Variable" to browse available
914
+ variables.
603
915
  </small>
604
916
  </div>
605
917
 
606
918
  <div className="signature-editor-section">
607
919
  <div className="signature-editor-fields-header">
608
- <label className="signature-editor-label">Signature Fields</label>
920
+ <label className="signature-editor-label">
921
+ Signature Fields
922
+ </label>
609
923
  <button
610
924
  onClick={handleAddField}
611
925
  className="signature-btn primary small"
@@ -619,7 +933,10 @@ const AgreementSignatureEditor = ({
619
933
  <div className="signature-editor-empty">
620
934
  <Type size={48} />
621
935
  <p>No fields added yet</p>
622
- <button onClick={handleAddField} className="signature-btn primary">
936
+ <button
937
+ onClick={handleAddField}
938
+ className="signature-btn primary"
939
+ >
623
940
  Add Your First Field
624
941
  </button>
625
942
  </div>
@@ -638,9 +955,11 @@ const AgreementSignatureEditor = ({
638
955
 
639
956
  <div className="signature-editor-preview">
640
957
  <h4>Preview</h4>
641
- <div
958
+ <div
642
959
  className="signature-editor-preview-content"
643
- dangerouslySetInnerHTML={{ __html: generatePreviewContent(template) }}
960
+ dangerouslySetInnerHTML={{
961
+ __html: generatePreviewContent(template),
962
+ }}
644
963
  />
645
964
  </div>
646
965
  </div>
@@ -691,4 +1010,4 @@ const AgreementSignatureEditor = ({
691
1010
  );
692
1011
  };
693
1012
 
694
- export default AgreementSignatureEditor;
1013
+ export default AgreementSignatureEditor;