@visns-studio/visns-components 5.13.10 → 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.10",
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": [
@@ -450,7 +450,7 @@ const DataGrid = forwardRef(
450
450
  if (gridRef.current?.api) {
451
451
  gridRef.current.api.forceUpdate();
452
452
  }
453
- }, 10);
453
+ }, 50); // Increased delay to ensure proper rendering consistency
454
454
  });
455
455
 
456
456
  return sqlResult.then((res) => ({
@@ -1208,6 +1208,14 @@ const DataGrid = forwardRef(
1208
1208
 
1209
1209
  const handleReload = () => {
1210
1210
  gridRef.current.paginationProps.reload();
1211
+
1212
+ // Force grid to recalculate row heights after reload
1213
+ // This fixes the row height rendering issue after filter changes
1214
+ setTimeout(() => {
1215
+ if (gridRef.current?.api) {
1216
+ gridRef.current.api.forceUpdate();
1217
+ }
1218
+ }, 50); // Increased delay to ensure proper rendering after reload
1211
1219
  };
1212
1220
 
1213
1221
  const handleGroupAction = async (groupValue, iconConfig) => {
@@ -1312,6 +1320,13 @@ const DataGrid = forwardRef(
1312
1320
  // Reload grid data if needed
1313
1321
  if (gridRef.current && gridRef.current.paginationProps) {
1314
1322
  gridRef.current.paginationProps.reload();
1323
+
1324
+ // Force grid to recalculate row heights after reload
1325
+ setTimeout(() => {
1326
+ if (gridRef.current?.api) {
1327
+ gridRef.current.api.forceUpdate();
1328
+ }
1329
+ }, 50);
1315
1330
  }
1316
1331
  } catch (error) {
1317
1332
  console.error('Group action fetch error:', error);
@@ -3130,6 +3145,13 @@ const DataGrid = forwardRef(
3130
3145
  // Force reload of data
3131
3146
  if (gridRef.current && gridRef.current.paginationProps) {
3132
3147
  gridRef.current.paginationProps.reload();
3148
+
3149
+ // Force grid to recalculate row heights after reload
3150
+ setTimeout(() => {
3151
+ if (gridRef.current?.api) {
3152
+ gridRef.current.api.forceUpdate();
3153
+ }
3154
+ }, 50);
3133
3155
  }
3134
3156
  }, [ajaxSetting?.url]);
3135
3157
 
@@ -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