@visns-studio/visns-components 5.13.11 → 5.13.13
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/README.md +4 -3
- package/package.json +1 -1
- package/src/components/proposal/AgreementSignatureEditor.css +7 -0
- package/src/components/proposal/AgreementSignatureEditor.jsx +55 -5
- package/src/components/proposal/BrandingProfileCompanyInfo.jsx +18 -1
- package/src/components/proposal/ProposalTemplatePreview.jsx +31 -3
- package/src/components/proposal/VariableInserter.css +77 -0
- package/src/components/proposal/VariableInserter.jsx +165 -45
- package/src/utils/urlBuilder.js +2 -0
package/README.md
CHANGED
|
@@ -2937,7 +2937,8 @@ const proposalEndpoints = {
|
|
|
2937
2937
|
brandingCSS: '/ajax/branding-profiles/{id}/css',
|
|
2938
2938
|
|
|
2939
2939
|
// PDF generation
|
|
2940
|
-
generatePDF: '/ajax/pdf/generate-proposal',
|
|
2940
|
+
generatePDF: '/ajax/pdf/generate-proposal', // Legacy DomPDF
|
|
2941
|
+
generatePDFSpatie: '/ajax/pdf/generate-proposal-spatie', // Recommended: Spatie PDF
|
|
2941
2942
|
previewHTML: '/ajax/pdf/preview-proposal',
|
|
2942
2943
|
|
|
2943
2944
|
// Variable helpers
|
|
@@ -3092,8 +3093,8 @@ const QuoteProposalManager = () => {
|
|
|
3092
3093
|
// Generate PDF or handle proposal data
|
|
3093
3094
|
console.log('Generating proposal PDF:', proposalData);
|
|
3094
3095
|
|
|
3095
|
-
// Example: Download PDF
|
|
3096
|
-
window.open(`/ajax/pdf/generate-proposal?quote_id=${quoteData.id}`, '_blank');
|
|
3096
|
+
// Example: Download PDF (using Spatie - recommended)
|
|
3097
|
+
window.open(`/ajax/pdf/generate-proposal-spatie?quote_id=${quoteData.id}`, '_blank');
|
|
3097
3098
|
}}
|
|
3098
3099
|
/>
|
|
3099
3100
|
</div>
|
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.13",
|
|
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;
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import React, { useState, useEffect } from 'react';
|
|
2
|
-
import { Plus, Trash2, Move, Type, Edit3, Save, X } from 'lucide-react';
|
|
1
|
+
import React, { useState, useEffect, useRef } from 'react';
|
|
2
|
+
import { Plus, Trash2, Move, Type, Edit3, Save, X, Tag } from 'lucide-react';
|
|
3
3
|
import { SortableContainer, SortableElement, SortableHandle } from 'react-sortable-hoc';
|
|
4
4
|
import { arrayMoveImmutable } from 'array-move';
|
|
5
5
|
import { toast } from 'react-toastify';
|
|
6
6
|
import { buildUrl, defaultProposalApiEndpoints } from '../../utils/urlBuilder';
|
|
7
|
+
import VariableInserter from './VariableInserter';
|
|
7
8
|
import './AgreementSignatureEditor.css';
|
|
8
9
|
|
|
9
10
|
const DragHandle = SortableHandle(() => (
|
|
@@ -195,6 +196,8 @@ const AgreementSignatureEditor = ({
|
|
|
195
196
|
const [editingField, setEditingField] = useState(null);
|
|
196
197
|
const [showFieldEditor, setShowFieldEditor] = useState(false);
|
|
197
198
|
const [isLoading, setIsLoading] = useState(false);
|
|
199
|
+
const [showVariableInserter, setShowVariableInserter] = useState(false);
|
|
200
|
+
const bodyTextRef = useRef(null);
|
|
198
201
|
|
|
199
202
|
// Helper function to replace template variables in URLs
|
|
200
203
|
const buildUrlWithTemplate = (urlTemplate, params = {}) => {
|
|
@@ -399,6 +402,33 @@ const AgreementSignatureEditor = ({
|
|
|
399
402
|
});
|
|
400
403
|
};
|
|
401
404
|
|
|
405
|
+
const handleVariableInsert = (variableName) => {
|
|
406
|
+
const textarea = bodyTextRef.current;
|
|
407
|
+
if (!textarea) return;
|
|
408
|
+
|
|
409
|
+
const start = textarea.selectionStart;
|
|
410
|
+
const end = textarea.selectionEnd;
|
|
411
|
+
const text = textarea.value;
|
|
412
|
+
const before = text.substring(0, start);
|
|
413
|
+
const after = text.substring(end, text.length);
|
|
414
|
+
const variableText = `{{${variableName}}}`;
|
|
415
|
+
|
|
416
|
+
const newText = before + variableText + after;
|
|
417
|
+
|
|
418
|
+
setTemplate({
|
|
419
|
+
...template,
|
|
420
|
+
bodyText: newText
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
// Reset cursor position after the inserted variable
|
|
424
|
+
setTimeout(() => {
|
|
425
|
+
textarea.focus();
|
|
426
|
+
textarea.setSelectionRange(start + variableText.length, start + variableText.length);
|
|
427
|
+
}, 0);
|
|
428
|
+
|
|
429
|
+
setShowVariableInserter(false);
|
|
430
|
+
};
|
|
431
|
+
|
|
402
432
|
const handleSaveTemplate = async () => {
|
|
403
433
|
try {
|
|
404
434
|
setIsLoading(true);
|
|
@@ -508,7 +538,7 @@ const AgreementSignatureEditor = ({
|
|
|
508
538
|
.join('');
|
|
509
539
|
|
|
510
540
|
return `
|
|
511
|
-
<div style="padding: 2rem;
|
|
541
|
+
<div style="padding: 2rem;">
|
|
512
542
|
${templateData.headerText ? `<h3 style="margin-bottom: 1rem; color: #333;">${templateData.headerText}</h3>` : ''}
|
|
513
543
|
<p style="margin-bottom: 2rem; line-height: 1.6;">${templateData.bodyText}</p>
|
|
514
544
|
<div style="clear: both;">${fieldsHtml}</div>
|
|
@@ -548,8 +578,20 @@ const AgreementSignatureEditor = ({
|
|
|
548
578
|
</div>
|
|
549
579
|
|
|
550
580
|
<div className="signature-editor-section">
|
|
551
|
-
<
|
|
581
|
+
<div className="signature-editor-label-with-button">
|
|
582
|
+
<label className="signature-editor-label">Body Text</label>
|
|
583
|
+
<button
|
|
584
|
+
type="button"
|
|
585
|
+
onClick={() => setShowVariableInserter(true)}
|
|
586
|
+
className="signature-btn secondary small"
|
|
587
|
+
title="Insert Variable"
|
|
588
|
+
>
|
|
589
|
+
<Tag size={16} />
|
|
590
|
+
Insert Variable
|
|
591
|
+
</button>
|
|
592
|
+
</div>
|
|
552
593
|
<textarea
|
|
594
|
+
ref={bodyTextRef}
|
|
553
595
|
value={template.bodyText}
|
|
554
596
|
onChange={(e) => setTemplate({...template, bodyText: e.target.value})}
|
|
555
597
|
placeholder="Enter the agreement text. Use {{variable_name}} for dynamic content."
|
|
@@ -557,7 +599,7 @@ const AgreementSignatureEditor = ({
|
|
|
557
599
|
rows="3"
|
|
558
600
|
/>
|
|
559
601
|
<small className="signature-editor-help">
|
|
560
|
-
Use variables like {'{{company_name}}'} for dynamic content
|
|
602
|
+
Use variables like {'{{company_name}}'} for dynamic content. Click "Insert Variable" to browse available variables.
|
|
561
603
|
</small>
|
|
562
604
|
</div>
|
|
563
605
|
|
|
@@ -637,6 +679,14 @@ const AgreementSignatureEditor = ({
|
|
|
637
679
|
</div>
|
|
638
680
|
</div>
|
|
639
681
|
)}
|
|
682
|
+
|
|
683
|
+
{showVariableInserter && (
|
|
684
|
+
<VariableInserter
|
|
685
|
+
onInsert={handleVariableInsert}
|
|
686
|
+
onCancel={() => setShowVariableInserter(false)}
|
|
687
|
+
apiEndpoints={apiEndpoints}
|
|
688
|
+
/>
|
|
689
|
+
)}
|
|
640
690
|
</div>
|
|
641
691
|
);
|
|
642
692
|
};
|
|
@@ -14,6 +14,7 @@ const BrandingProfileCompanyInfo = ({
|
|
|
14
14
|
address: '',
|
|
15
15
|
website: '',
|
|
16
16
|
phone: '',
|
|
17
|
+
email: '',
|
|
17
18
|
abn: '',
|
|
18
19
|
...brandingProfile?.company_info || {}
|
|
19
20
|
});
|
|
@@ -95,12 +96,17 @@ const BrandingProfileCompanyInfo = ({
|
|
|
95
96
|
<strong>Phone:</strong> {companyInfo.phone}
|
|
96
97
|
</div>
|
|
97
98
|
)}
|
|
99
|
+
{companyInfo.email && (
|
|
100
|
+
<div className="branding-company-info-item">
|
|
101
|
+
<strong>Email:</strong> {companyInfo.email}
|
|
102
|
+
</div>
|
|
103
|
+
)}
|
|
98
104
|
{companyInfo.abn && (
|
|
99
105
|
<div className="branding-company-info-item">
|
|
100
106
|
<strong>ABN:</strong> {companyInfo.abn}
|
|
101
107
|
</div>
|
|
102
108
|
)}
|
|
103
|
-
{!companyInfo.address && !companyInfo.website && !companyInfo.phone && !companyInfo.abn && (
|
|
109
|
+
{!companyInfo.address && !companyInfo.website && !companyInfo.phone && !companyInfo.email && !companyInfo.abn && (
|
|
104
110
|
<div className="branding-company-info-empty">
|
|
105
111
|
<Info size={16} />
|
|
106
112
|
<p>No company information added yet. Click Edit to add details.</p>
|
|
@@ -157,6 +163,17 @@ const BrandingProfileCompanyInfo = ({
|
|
|
157
163
|
/>
|
|
158
164
|
</div>
|
|
159
165
|
|
|
166
|
+
<div className="branding-company-info-field">
|
|
167
|
+
<label htmlFor="company-email">Email Address</label>
|
|
168
|
+
<input
|
|
169
|
+
type="email"
|
|
170
|
+
id="company-email"
|
|
171
|
+
value={companyInfo.email}
|
|
172
|
+
onChange={(e) => handleInputChange('email', e.target.value)}
|
|
173
|
+
placeholder="info@yourcompany.com"
|
|
174
|
+
/>
|
|
175
|
+
</div>
|
|
176
|
+
|
|
160
177
|
<div className="branding-company-info-field">
|
|
161
178
|
<label htmlFor="company-abn">ABN</label>
|
|
162
179
|
<input
|
|
@@ -128,12 +128,33 @@ const ProposalTemplatePreview = ({
|
|
|
128
128
|
|
|
129
129
|
const handleDownloadPDF = async () => {
|
|
130
130
|
try {
|
|
131
|
-
|
|
131
|
+
// Use Spatie PDF endpoint with proper payload structure
|
|
132
|
+
const payload = {
|
|
133
|
+
proposal_data: previewData?.template || {},
|
|
134
|
+
template_id: parseInt(templateId),
|
|
135
|
+
branding_id: selectedBrandingProfile ? parseInt(selectedBrandingProfile) : null,
|
|
136
|
+
sections: previewData?.sections || [],
|
|
137
|
+
header_config: {
|
|
138
|
+
enabled: true,
|
|
139
|
+
show_phone: true,
|
|
140
|
+
show_email: true,
|
|
141
|
+
show_website: true,
|
|
142
|
+
show_address: true,
|
|
143
|
+
show_abn: false
|
|
144
|
+
},
|
|
145
|
+
filename: `proposal-template-${templateId}.pdf`,
|
|
146
|
+
download: true
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
console.log('ProposalTemplatePreview: Generating PDF with payload', payload);
|
|
150
|
+
|
|
151
|
+
const response = await fetch(apiEndpoints.templatePdfSpatie, {
|
|
132
152
|
method: 'POST',
|
|
133
153
|
headers: {
|
|
134
154
|
'Content-Type': 'application/json',
|
|
135
155
|
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]')?.getAttribute('content')
|
|
136
|
-
}
|
|
156
|
+
},
|
|
157
|
+
body: JSON.stringify(payload)
|
|
137
158
|
});
|
|
138
159
|
|
|
139
160
|
if (response.ok) {
|
|
@@ -146,8 +167,15 @@ const ProposalTemplatePreview = ({
|
|
|
146
167
|
a.click();
|
|
147
168
|
window.URL.revokeObjectURL(url);
|
|
148
169
|
document.body.removeChild(a);
|
|
170
|
+
console.log('ProposalTemplatePreview: PDF generated successfully');
|
|
149
171
|
} else {
|
|
150
|
-
|
|
172
|
+
const errorText = await response.text();
|
|
173
|
+
console.error('ProposalTemplatePreview: PDF generation failed', {
|
|
174
|
+
status: response.status,
|
|
175
|
+
statusText: response.statusText,
|
|
176
|
+
error: errorText
|
|
177
|
+
});
|
|
178
|
+
throw new Error(`Failed to generate PDF: ${response.status} - ${response.statusText}`);
|
|
151
179
|
}
|
|
152
180
|
} catch (error) {
|
|
153
181
|
console.error('Error downloading PDF:', error);
|
|
@@ -109,6 +109,19 @@
|
|
|
109
109
|
color: white;
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
+
.variable-inserter-mode-buttons {
|
|
113
|
+
display: flex;
|
|
114
|
+
gap: 0.5rem;
|
|
115
|
+
align-items: center;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
.variable-inserter-toggle-btn.active {
|
|
119
|
+
background: rgba(60, 191, 125, 0.4);
|
|
120
|
+
border-color: rgba(60, 191, 125, 0.6);
|
|
121
|
+
color: white;
|
|
122
|
+
box-shadow: 0 0 0 2px rgba(60, 191, 125, 0.2);
|
|
123
|
+
}
|
|
124
|
+
|
|
112
125
|
.variable-inserter-intelligent-indicator {
|
|
113
126
|
color: #3cbf7d;
|
|
114
127
|
font-weight: 600;
|
|
@@ -240,8 +253,32 @@
|
|
|
240
253
|
.variable-inserter-category-header {
|
|
241
254
|
display: flex;
|
|
242
255
|
align-items: center;
|
|
256
|
+
justify-content: space-between;
|
|
243
257
|
gap: 0.5rem;
|
|
244
258
|
margin-bottom: 1rem;
|
|
259
|
+
padding: 0.75rem;
|
|
260
|
+
border-radius: 8px;
|
|
261
|
+
background: #f8fafc;
|
|
262
|
+
border: 1px solid #e2e8f0;
|
|
263
|
+
transition: all 0.2s;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
.variable-inserter-category-header.clickable {
|
|
267
|
+
cursor: pointer;
|
|
268
|
+
user-select: none;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
.variable-inserter-category-header.clickable:hover {
|
|
272
|
+
background: #f1f5f9;
|
|
273
|
+
border-color: #cbd5e1;
|
|
274
|
+
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
.variable-inserter-category-header-content {
|
|
278
|
+
display: flex;
|
|
279
|
+
align-items: center;
|
|
280
|
+
gap: 0.75rem;
|
|
281
|
+
flex: 1;
|
|
245
282
|
}
|
|
246
283
|
|
|
247
284
|
.variable-inserter-category-icon {
|
|
@@ -254,6 +291,30 @@
|
|
|
254
291
|
font-weight: 600;
|
|
255
292
|
color: #1f2937;
|
|
256
293
|
margin: 0;
|
|
294
|
+
display: flex;
|
|
295
|
+
align-items: center;
|
|
296
|
+
gap: 0.5rem;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
.variable-inserter-field-count {
|
|
300
|
+
font-size: 0.75rem;
|
|
301
|
+
color: #64748b;
|
|
302
|
+
font-weight: 500;
|
|
303
|
+
background: #e2e8f0;
|
|
304
|
+
padding: 0.125rem 0.375rem;
|
|
305
|
+
border-radius: 4px;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
.variable-inserter-expand-icon {
|
|
309
|
+
color: #64748b;
|
|
310
|
+
display: flex;
|
|
311
|
+
align-items: center;
|
|
312
|
+
justify-content: center;
|
|
313
|
+
transition: transform 0.2s;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
.variable-inserter-category-header.clickable:hover .variable-inserter-expand-icon {
|
|
317
|
+
color: #475569;
|
|
257
318
|
}
|
|
258
319
|
|
|
259
320
|
.variable-inserter-variables-grid {
|
|
@@ -304,6 +365,22 @@
|
|
|
304
365
|
color: #1b3933;
|
|
305
366
|
margin: 0 0 0.25rem 0;
|
|
306
367
|
word-break: break-all;
|
|
368
|
+
display: flex;
|
|
369
|
+
align-items: center;
|
|
370
|
+
gap: 0.5rem;
|
|
371
|
+
flex-wrap: wrap;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
.variable-inserter-calc-badge {
|
|
375
|
+
background: #f59e0b;
|
|
376
|
+
color: white;
|
|
377
|
+
font-size: 0.625rem;
|
|
378
|
+
font-weight: 700;
|
|
379
|
+
padding: 0.125rem 0.25rem;
|
|
380
|
+
border-radius: 3px;
|
|
381
|
+
letter-spacing: 0.025em;
|
|
382
|
+
font-family: 'Arial', sans-serif;
|
|
383
|
+
flex-shrink: 0;
|
|
307
384
|
}
|
|
308
385
|
|
|
309
386
|
.variable-inserter-variable-button:hover .variable-inserter-variable-name {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React, { useState, useEffect, useMemo } from 'react';
|
|
2
|
-
import { X, Search, Tag, User, Building, Calendar, DollarSign, FileText } from 'lucide-react';
|
|
2
|
+
import { X, Search, Tag, User, Building, Calendar, DollarSign, FileText, ChevronDown, ChevronRight } from 'lucide-react';
|
|
3
3
|
import { defaultProposalApiEndpoints } from '../../utils/urlBuilder';
|
|
4
4
|
import './VariableInserter.css';
|
|
5
5
|
|
|
@@ -73,8 +73,12 @@ const VariableInserter = ({
|
|
|
73
73
|
const [searchTerm, setSearchTerm] = useState('');
|
|
74
74
|
const [selectedCategory, setSelectedCategory] = useState('all');
|
|
75
75
|
const [intelligentVariables, setIntelligentVariables] = useState([]);
|
|
76
|
+
const [allDatabaseFields, setAllDatabaseFields] = useState([]);
|
|
76
77
|
const [isLoadingIntelligent, setIsLoadingIntelligent] = useState(false);
|
|
77
|
-
const [
|
|
78
|
+
const [isLoadingDatabase, setIsLoadingDatabase] = useState(false);
|
|
79
|
+
const [useIntelligentVariables, setUseIntelligentVariables] = useState(false);
|
|
80
|
+
const [useAllDatabaseFields, setUseAllDatabaseFields] = useState(true);
|
|
81
|
+
const [expandedTables, setExpandedTables] = useState(new Set());
|
|
78
82
|
|
|
79
83
|
// Load intelligent variables from API
|
|
80
84
|
useEffect(() => {
|
|
@@ -103,29 +107,83 @@ const VariableInserter = ({
|
|
|
103
107
|
loadIntelligentVariables();
|
|
104
108
|
}, [useIntelligentVariables]);
|
|
105
109
|
|
|
106
|
-
//
|
|
110
|
+
// Load all database fields from API
|
|
111
|
+
useEffect(() => {
|
|
112
|
+
const loadAllDatabaseFields = async () => {
|
|
113
|
+
if (!useAllDatabaseFields) return;
|
|
114
|
+
|
|
115
|
+
try {
|
|
116
|
+
setIsLoadingDatabase(true);
|
|
117
|
+
const response = await fetch(apiEndpoints.allDatabaseFields);
|
|
118
|
+
const data = await response.json();
|
|
119
|
+
|
|
120
|
+
if (data.success && data.tables) {
|
|
121
|
+
setAllDatabaseFields(data.tables);
|
|
122
|
+
} else {
|
|
123
|
+
console.warn('No database fields available, falling back to intelligent variables');
|
|
124
|
+
setUseAllDatabaseFields(false);
|
|
125
|
+
}
|
|
126
|
+
} catch (error) {
|
|
127
|
+
console.error('Failed to load database fields:', error);
|
|
128
|
+
setUseAllDatabaseFields(false);
|
|
129
|
+
} finally {
|
|
130
|
+
setIsLoadingDatabase(false);
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
loadAllDatabaseFields();
|
|
135
|
+
}, [useAllDatabaseFields, apiEndpoints.allDatabaseFields]);
|
|
136
|
+
|
|
137
|
+
// Toggle table expansion
|
|
138
|
+
const toggleTable = (tableName) => {
|
|
139
|
+
const newExpanded = new Set(expandedTables);
|
|
140
|
+
if (newExpanded.has(tableName)) {
|
|
141
|
+
newExpanded.delete(tableName);
|
|
142
|
+
} else {
|
|
143
|
+
newExpanded.add(tableName);
|
|
144
|
+
}
|
|
145
|
+
setExpandedTables(newExpanded);
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
// Merge available variables with intelligent variables, database fields, or defaults
|
|
107
149
|
const allVariables = useMemo(() => {
|
|
108
150
|
if (availableVariables.length > 0) {
|
|
109
151
|
return availableVariables;
|
|
110
152
|
}
|
|
111
153
|
|
|
154
|
+
if (useAllDatabaseFields && allDatabaseFields.length > 0) {
|
|
155
|
+
return allDatabaseFields;
|
|
156
|
+
}
|
|
157
|
+
|
|
112
158
|
if (useIntelligentVariables && intelligentVariables.length > 0) {
|
|
113
159
|
return intelligentVariables;
|
|
114
160
|
}
|
|
115
161
|
|
|
116
162
|
return defaultVariables;
|
|
117
|
-
}, [availableVariables, intelligentVariables, useIntelligentVariables]);
|
|
163
|
+
}, [availableVariables, allDatabaseFields, intelligentVariables, useIntelligentVariables, useAllDatabaseFields]);
|
|
118
164
|
|
|
119
|
-
const filteredVariables = allVariables.map(category =>
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
165
|
+
const filteredVariables = allVariables.map(category => {
|
|
166
|
+
// For database fields mode, we need to handle table name searching
|
|
167
|
+
const matchesSearch = searchTerm === '' ||
|
|
168
|
+
category.category.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
|
169
|
+
(category.table_name && category.table_name.toLowerCase().includes(searchTerm.toLowerCase()));
|
|
170
|
+
|
|
171
|
+
// Filter variables within category if there's a search term
|
|
172
|
+
const filteredCategoryVariables = searchTerm === '' ? category.variables :
|
|
173
|
+
category.variables.filter(variable =>
|
|
174
|
+
variable.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
|
175
|
+
variable.description.toLowerCase().includes(searchTerm.toLowerCase())
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
return {
|
|
179
|
+
...category,
|
|
180
|
+
variables: filteredCategoryVariables,
|
|
181
|
+
matchesSearch: matchesSearch || filteredCategoryVariables.length > 0
|
|
182
|
+
};
|
|
183
|
+
}).filter(category =>
|
|
126
184
|
selectedCategory === 'all' ||
|
|
127
185
|
category.category.toLowerCase().includes(selectedCategory.toLowerCase())
|
|
128
|
-
).filter(category => category.
|
|
186
|
+
).filter(category => category.matchesSearch);
|
|
129
187
|
|
|
130
188
|
const categories = ['all', ...allVariables.map(cat => cat.category)];
|
|
131
189
|
|
|
@@ -137,7 +195,12 @@ const VariableInserter = ({
|
|
|
137
195
|
<h2 className="variable-inserter-title">Insert Variable</h2>
|
|
138
196
|
<p className="variable-inserter-subtitle">
|
|
139
197
|
Click on a variable to insert it at the cursor position
|
|
140
|
-
{
|
|
198
|
+
{useAllDatabaseFields && allDatabaseFields.length > 0 && (
|
|
199
|
+
<span className="variable-inserter-intelligent-indicator">
|
|
200
|
+
• Using all database fields ({allDatabaseFields.reduce((sum, table) => sum + table.field_count, 0)} variables from {allDatabaseFields.length} tables)
|
|
201
|
+
</span>
|
|
202
|
+
)}
|
|
203
|
+
{useIntelligentVariables && intelligentVariables.length > 0 && !useAllDatabaseFields && (
|
|
141
204
|
<span className="variable-inserter-intelligent-indicator">
|
|
142
205
|
• Using intelligent model-based variables
|
|
143
206
|
</span>
|
|
@@ -148,16 +211,46 @@ const VariableInserter = ({
|
|
|
148
211
|
Loading intelligent variables...
|
|
149
212
|
</p>
|
|
150
213
|
)}
|
|
214
|
+
{isLoadingDatabase && (
|
|
215
|
+
<p className="variable-inserter-loading">
|
|
216
|
+
Loading database fields...
|
|
217
|
+
</p>
|
|
218
|
+
)}
|
|
151
219
|
</div>
|
|
152
220
|
<div className="variable-inserter-header-actions">
|
|
153
|
-
{intelligentVariables.length > 0 && (
|
|
154
|
-
<
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
221
|
+
{(intelligentVariables.length > 0 || allDatabaseFields.length > 0) && (
|
|
222
|
+
<div className="variable-inserter-mode-buttons">
|
|
223
|
+
<button
|
|
224
|
+
onClick={() => {
|
|
225
|
+
setUseAllDatabaseFields(true);
|
|
226
|
+
setUseIntelligentVariables(false);
|
|
227
|
+
}}
|
|
228
|
+
className={`variable-inserter-toggle-btn ${useAllDatabaseFields ? 'active' : ''}`}
|
|
229
|
+
title="Show all database fields from all tables"
|
|
230
|
+
>
|
|
231
|
+
All Fields
|
|
232
|
+
</button>
|
|
233
|
+
<button
|
|
234
|
+
onClick={() => {
|
|
235
|
+
setUseIntelligentVariables(true);
|
|
236
|
+
setUseAllDatabaseFields(false);
|
|
237
|
+
}}
|
|
238
|
+
className={`variable-inserter-toggle-btn ${useIntelligentVariables && !useAllDatabaseFields ? 'active' : ''}`}
|
|
239
|
+
title="Show intelligent model-based variables"
|
|
240
|
+
>
|
|
241
|
+
Smart
|
|
242
|
+
</button>
|
|
243
|
+
<button
|
|
244
|
+
onClick={() => {
|
|
245
|
+
setUseIntelligentVariables(false);
|
|
246
|
+
setUseAllDatabaseFields(false);
|
|
247
|
+
}}
|
|
248
|
+
className={`variable-inserter-toggle-btn ${!useIntelligentVariables && !useAllDatabaseFields ? 'active' : ''}`}
|
|
249
|
+
title="Show default variables"
|
|
250
|
+
>
|
|
251
|
+
Default
|
|
252
|
+
</button>
|
|
253
|
+
</div>
|
|
161
254
|
)}
|
|
162
255
|
<button
|
|
163
256
|
onClick={onCancel}
|
|
@@ -208,35 +301,62 @@ const VariableInserter = ({
|
|
|
208
301
|
<div className="variable-inserter-categories">
|
|
209
302
|
{filteredVariables.map((category) => {
|
|
210
303
|
const IconComponent = category.icon;
|
|
304
|
+
const isExpanded = expandedTables.has(category.table_name || category.category);
|
|
305
|
+
const isTableMode = useAllDatabaseFields && category.table_name;
|
|
306
|
+
|
|
211
307
|
return (
|
|
212
308
|
<div key={category.category} className="variable-inserter-category">
|
|
213
|
-
<div
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
309
|
+
<div
|
|
310
|
+
className={`variable-inserter-category-header ${isTableMode ? 'clickable' : ''}`}
|
|
311
|
+
onClick={isTableMode ? () => toggleTable(category.table_name) : undefined}
|
|
312
|
+
>
|
|
313
|
+
<div className="variable-inserter-category-header-content">
|
|
314
|
+
<IconComponent size={20} className="variable-inserter-category-icon" />
|
|
315
|
+
<h3 className="variable-inserter-category-title">
|
|
316
|
+
{category.category}
|
|
317
|
+
{isTableMode && (
|
|
318
|
+
<span className="variable-inserter-field-count">
|
|
319
|
+
{category.field_count || category.variables.length} fields
|
|
320
|
+
</span>
|
|
321
|
+
)}
|
|
322
|
+
</h3>
|
|
323
|
+
</div>
|
|
324
|
+
{isTableMode && (
|
|
325
|
+
<div className="variable-inserter-expand-icon">
|
|
326
|
+
{isExpanded ? <ChevronDown size={16} /> : <ChevronRight size={16} />}
|
|
327
|
+
</div>
|
|
328
|
+
)}
|
|
218
329
|
</div>
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
330
|
+
|
|
331
|
+
{/* Show variables only if expanded (for table mode) or always (for other modes) */}
|
|
332
|
+
{(!isTableMode || isExpanded) && (
|
|
333
|
+
<div className="variable-inserter-variables-grid">
|
|
334
|
+
{category.variables.map((variable) => (
|
|
335
|
+
<button
|
|
336
|
+
key={variable.name}
|
|
337
|
+
onClick={() => onInsert(variable.name)}
|
|
338
|
+
className="variable-inserter-variable-button"
|
|
339
|
+
>
|
|
340
|
+
<div className="variable-inserter-variable-content">
|
|
341
|
+
<div className="variable-inserter-variable-info">
|
|
342
|
+
<div className="variable-inserter-variable-name">
|
|
343
|
+
{`{{${variable.name}}}`}
|
|
344
|
+
{variable.is_calculation_version && (
|
|
345
|
+
<span className="variable-inserter-calc-badge">
|
|
346
|
+
CALC
|
|
347
|
+
</span>
|
|
348
|
+
)}
|
|
349
|
+
</div>
|
|
350
|
+
<div className="variable-inserter-variable-description">
|
|
351
|
+
{variable.description}
|
|
352
|
+
</div>
|
|
233
353
|
</div>
|
|
354
|
+
<Tag size={16} className="variable-inserter-variable-icon" />
|
|
234
355
|
</div>
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
</div>
|
|
356
|
+
</button>
|
|
357
|
+
))}
|
|
358
|
+
</div>
|
|
359
|
+
)}
|
|
240
360
|
</div>
|
|
241
361
|
);
|
|
242
362
|
})}
|
package/src/utils/urlBuilder.js
CHANGED
|
@@ -21,11 +21,13 @@ export const defaultProposalApiEndpoints = {
|
|
|
21
21
|
// Variable management
|
|
22
22
|
intelligentVariables: '/ajax/proposalTemplates/variables/intelligent',
|
|
23
23
|
availableVariables: '/ajax/proposalTemplates/variables/available',
|
|
24
|
+
allDatabaseFields: '/ajax/proposalTemplates/variables/all-database-fields',
|
|
24
25
|
|
|
25
26
|
// Template management
|
|
26
27
|
templateData: '/ajax/proposalTemplates/{templateId}',
|
|
27
28
|
templatePreview: '/ajax/proposalTemplates/{templateId}/preview',
|
|
28
29
|
templatePdf: '/ajax/proposalTemplates/{templateId}/pdf',
|
|
30
|
+
templatePdfSpatie: '/ajax/pdf/generate-proposal-spatie',
|
|
29
31
|
templateFullPreview: '/ajax/proposalTemplates/{templateId}/preview',
|
|
30
32
|
|
|
31
33
|
// Section management
|