@visns-studio/visns-components 5.13.11 → 5.13.12

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 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.11",
91
+ "version": "5.13.12",
92
92
  "description": "Various packages to assist in the development of our Custom Applications.",
93
93
  "main": "src/index.js",
94
94
  "files": [
@@ -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
- const response = await fetch(`${buildUrlWithTemplate(apiEndpoints.templatePdf)}?branding_profile_id=${selectedBrandingProfile}`, {
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
- throw new Error('Failed to generate PDF');
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 [useIntelligentVariables, setUseIntelligentVariables] = useState(true);
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
- // Merge available variables with intelligent variables or defaults
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
- ...category,
121
- variables: category.variables.filter(variable =>
122
- variable.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
123
- variable.description.toLowerCase().includes(searchTerm.toLowerCase())
124
- )
125
- })).filter(category =>
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.variables.length > 0);
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
- {useIntelligentVariables && intelligentVariables.length > 0 && (
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
- <button
155
- onClick={() => setUseIntelligentVariables(!useIntelligentVariables)}
156
- className="variable-inserter-toggle-btn"
157
- title={useIntelligentVariables ? "Switch to default variables" : "Switch to intelligent variables"}
158
- >
159
- {useIntelligentVariables ? "Default" : "Smart"}
160
- </button>
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 className="variable-inserter-category-header">
214
- <IconComponent size={20} className="variable-inserter-category-icon" />
215
- <h3 className="variable-inserter-category-title">
216
- {category.category}
217
- </h3>
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
- <div className="variable-inserter-variables-grid">
220
- {category.variables.map((variable) => (
221
- <button
222
- key={variable.name}
223
- onClick={() => onInsert(variable.name)}
224
- className="variable-inserter-variable-button"
225
- >
226
- <div className="variable-inserter-variable-content">
227
- <div className="variable-inserter-variable-info">
228
- <div className="variable-inserter-variable-name">
229
- {`{{${variable.name}}}`}
230
- </div>
231
- <div className="variable-inserter-variable-description">
232
- {variable.description}
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
- <Tag size={16} className="variable-inserter-variable-icon" />
236
- </div>
237
- </button>
238
- ))}
239
- </div>
356
+ </button>
357
+ ))}
358
+ </div>
359
+ )}
240
360
  </div>
241
361
  );
242
362
  })}
@@ -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