@visns-studio/visns-components 5.7.9 → 5.7.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -82,7 +82,7 @@
|
|
|
82
82
|
"react-dom": "^17.0.0 || ^18.0.0"
|
|
83
83
|
},
|
|
84
84
|
"name": "@visns-studio/visns-components",
|
|
85
|
-
"version": "5.7.
|
|
85
|
+
"version": "5.7.10",
|
|
86
86
|
"description": "Various packages to assist in the development of our Custom Applications.",
|
|
87
87
|
"main": "src/index.js",
|
|
88
88
|
"files": [
|
|
@@ -29,9 +29,11 @@ const CustomDownload = (
|
|
|
29
29
|
formData = { ...formData, _method: method };
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
// Let fetchUtil handle the Content-Type and JSON stringification
|
|
32
33
|
if (!(formData instanceof FormData)) {
|
|
33
34
|
headers['Content-Type'] = 'application/json';
|
|
34
|
-
|
|
35
|
+
// Don't stringify here, let fetchUtil handle it
|
|
36
|
+
// formData = JSON.stringify(formData);
|
|
35
37
|
} else {
|
|
36
38
|
headers['Content-Type'] = 'multipart/form-data';
|
|
37
39
|
headers.contentType = false;
|
|
@@ -5,6 +5,8 @@ import { useParams, useNavigate } from 'react-router-dom';
|
|
|
5
5
|
import Popup from 'reactjs-popup';
|
|
6
6
|
import moment from 'moment';
|
|
7
7
|
import get from 'lodash/get';
|
|
8
|
+
import { saveAs } from 'file-saver';
|
|
9
|
+
import { toast } from 'react-toastify';
|
|
8
10
|
|
|
9
11
|
import CustomFetch from '../../crm/Fetch';
|
|
10
12
|
import ActionButtons from './ActionButtons';
|
|
@@ -106,6 +108,86 @@ function GenericQuote({ logo, setting, urlParam }) {
|
|
|
106
108
|
return;
|
|
107
109
|
}
|
|
108
110
|
|
|
111
|
+
// Handle generatePdf type - generates PDF from a view
|
|
112
|
+
if (action.type === 'generatePdf' && action.view) {
|
|
113
|
+
console.log('Generating PDF from view:', action.view);
|
|
114
|
+
|
|
115
|
+
// Show loading toast
|
|
116
|
+
const toastId = toast.loading('Generating PDF...');
|
|
117
|
+
|
|
118
|
+
// Prepare the request body
|
|
119
|
+
const requestBody = {
|
|
120
|
+
view: action.view,
|
|
121
|
+
data: {
|
|
122
|
+
quote: data, // Pass the current quote data
|
|
123
|
+
},
|
|
124
|
+
filename: `quote-${data.id}.pdf`,
|
|
125
|
+
paper: action.paper || 'a4',
|
|
126
|
+
orientation: action.orientation || 'portrait',
|
|
127
|
+
download: action.download !== false, // Default to true if not specified
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
// Filename for the downloaded PDF
|
|
131
|
+
const filename = requestBody.filename;
|
|
132
|
+
|
|
133
|
+
// Get CSRF token
|
|
134
|
+
const csrfToken = document
|
|
135
|
+
.querySelector('meta[name="csrf-token"]')
|
|
136
|
+
?.getAttribute('content');
|
|
137
|
+
|
|
138
|
+
// Call the PDF generation endpoint directly with fetch
|
|
139
|
+
fetch('/ajax/pdf/generate', {
|
|
140
|
+
method: 'POST',
|
|
141
|
+
headers: {
|
|
142
|
+
'Content-Type': 'application/json',
|
|
143
|
+
'X-Requested-With': 'XMLHttpRequest',
|
|
144
|
+
'X-CSRF-TOKEN': csrfToken,
|
|
145
|
+
'X-XSRF-TOKEN': csrfToken,
|
|
146
|
+
},
|
|
147
|
+
body: JSON.stringify(requestBody),
|
|
148
|
+
})
|
|
149
|
+
.then((response) => {
|
|
150
|
+
if (!response.ok) {
|
|
151
|
+
// Try to parse error response as JSON
|
|
152
|
+
return response.json().then((errorData) => {
|
|
153
|
+
throw new Error(
|
|
154
|
+
errorData.message || 'Error generating PDF'
|
|
155
|
+
);
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
return response.blob();
|
|
159
|
+
})
|
|
160
|
+
.then((blob) => {
|
|
161
|
+
// Success - save the blob
|
|
162
|
+
console.log('PDF generated successfully');
|
|
163
|
+
saveAs(blob, filename);
|
|
164
|
+
|
|
165
|
+
// Update toast
|
|
166
|
+
toast.update(toastId, {
|
|
167
|
+
render: 'PDF downloaded successfully!',
|
|
168
|
+
type: 'success',
|
|
169
|
+
isLoading: false,
|
|
170
|
+
autoClose: 3000,
|
|
171
|
+
closeButton: true,
|
|
172
|
+
});
|
|
173
|
+
})
|
|
174
|
+
.catch((error) => {
|
|
175
|
+
// Error handling
|
|
176
|
+
console.error('Error generating PDF:', error);
|
|
177
|
+
|
|
178
|
+
// Update toast
|
|
179
|
+
toast.update(toastId, {
|
|
180
|
+
render: `Error generating PDF: ${error.message}`,
|
|
181
|
+
type: 'error',
|
|
182
|
+
isLoading: false,
|
|
183
|
+
autoClose: 5000,
|
|
184
|
+
closeButton: true,
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
|
|
109
191
|
// Handle other actions based on their id if no specific type is defined
|
|
110
192
|
switch (action.id) {
|
|
111
193
|
case 'addItem':
|
|
@@ -119,10 +201,84 @@ function GenericQuote({ logo, setting, urlParam }) {
|
|
|
119
201
|
}
|
|
120
202
|
break;
|
|
121
203
|
case 'saveAsPdf':
|
|
122
|
-
// If no type is specified but id is saveAsPdf, default to
|
|
204
|
+
// If no type is specified but id is saveAsPdf, default to PDF generation
|
|
123
205
|
if (!action.type) {
|
|
124
206
|
console.log('Save as PDF clicked');
|
|
125
|
-
|
|
207
|
+
|
|
208
|
+
// Show loading toast
|
|
209
|
+
const toastId = toast.loading('Generating PDF...');
|
|
210
|
+
|
|
211
|
+
// Default PDF generation with standard settings
|
|
212
|
+
const requestBody = {
|
|
213
|
+
view: 'quote.pdf', // Default view
|
|
214
|
+
data: {
|
|
215
|
+
quote: data, // Pass the current quote data
|
|
216
|
+
},
|
|
217
|
+
filename: `quote-${data.id}.pdf`,
|
|
218
|
+
paper: 'a4',
|
|
219
|
+
orientation: 'portrait',
|
|
220
|
+
download: true,
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
// Filename for the downloaded PDF
|
|
224
|
+
const filename = requestBody.filename;
|
|
225
|
+
|
|
226
|
+
// Get CSRF token
|
|
227
|
+
const csrfToken = document
|
|
228
|
+
.querySelector('meta[name="csrf-token"]')
|
|
229
|
+
?.getAttribute('content');
|
|
230
|
+
|
|
231
|
+
// Call the PDF generation endpoint directly with fetch
|
|
232
|
+
fetch('/ajax/pdf/generate', {
|
|
233
|
+
method: 'POST',
|
|
234
|
+
headers: {
|
|
235
|
+
'Content-Type': 'application/json',
|
|
236
|
+
'X-Requested-With': 'XMLHttpRequest',
|
|
237
|
+
'X-CSRF-TOKEN': csrfToken,
|
|
238
|
+
'X-XSRF-TOKEN': csrfToken,
|
|
239
|
+
},
|
|
240
|
+
body: JSON.stringify(requestBody),
|
|
241
|
+
})
|
|
242
|
+
.then((response) => {
|
|
243
|
+
if (!response.ok) {
|
|
244
|
+
// Try to parse error response as JSON
|
|
245
|
+
return response.json().then((errorData) => {
|
|
246
|
+
throw new Error(
|
|
247
|
+
errorData.message ||
|
|
248
|
+
errorData.error ||
|
|
249
|
+
'Error generating PDF'
|
|
250
|
+
);
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
return response.blob();
|
|
254
|
+
})
|
|
255
|
+
.then((blob) => {
|
|
256
|
+
// Success - save the blob
|
|
257
|
+
console.log('PDF generated successfully');
|
|
258
|
+
saveAs(blob, filename);
|
|
259
|
+
|
|
260
|
+
// Update toast
|
|
261
|
+
toast.update(toastId, {
|
|
262
|
+
render: 'PDF downloaded successfully!',
|
|
263
|
+
type: 'success',
|
|
264
|
+
isLoading: false,
|
|
265
|
+
autoClose: 3000,
|
|
266
|
+
closeButton: true,
|
|
267
|
+
});
|
|
268
|
+
})
|
|
269
|
+
.catch((error) => {
|
|
270
|
+
// Error handling
|
|
271
|
+
console.error('Error generating PDF:', error);
|
|
272
|
+
|
|
273
|
+
// Update toast
|
|
274
|
+
toast.update(toastId, {
|
|
275
|
+
render: `Error generating PDF: ${error.message}`,
|
|
276
|
+
type: 'error',
|
|
277
|
+
isLoading: false,
|
|
278
|
+
autoClose: 5000,
|
|
279
|
+
closeButton: true,
|
|
280
|
+
});
|
|
281
|
+
});
|
|
126
282
|
}
|
|
127
283
|
break;
|
|
128
284
|
case 'saveToTicket':
|
|
@@ -217,7 +373,90 @@ function GenericQuote({ logo, setting, urlParam }) {
|
|
|
217
373
|
<div className={styles.quoteParty}>
|
|
218
374
|
<h3 className={styles.partyTitle}>Quote To:</h3>
|
|
219
375
|
<div className={styles.partyDetails}>
|
|
220
|
-
{data.customer ?
|
|
376
|
+
{data.customer ? (
|
|
377
|
+
<>
|
|
378
|
+
{data.customer.name}
|
|
379
|
+
<br />
|
|
380
|
+
{(() => {
|
|
381
|
+
// Find primary site
|
|
382
|
+
const primarySite =
|
|
383
|
+
data.customer.sites?.find(
|
|
384
|
+
(site) => site.primary === 1
|
|
385
|
+
);
|
|
386
|
+
if (!primarySite) return null;
|
|
387
|
+
|
|
388
|
+
// Find primary contact for this site
|
|
389
|
+
const primaryContact =
|
|
390
|
+
primarySite.contacts?.find(
|
|
391
|
+
(contact) =>
|
|
392
|
+
contact.primary === 1
|
|
393
|
+
);
|
|
394
|
+
|
|
395
|
+
return (
|
|
396
|
+
<>
|
|
397
|
+
{primaryContact && (
|
|
398
|
+
<>
|
|
399
|
+
{
|
|
400
|
+
primaryContact.firstname
|
|
401
|
+
}{' '}
|
|
402
|
+
{primaryContact.surname}
|
|
403
|
+
<br />
|
|
404
|
+
</>
|
|
405
|
+
)}
|
|
406
|
+
{primarySite.address1 && (
|
|
407
|
+
<>
|
|
408
|
+
{primarySite.address1}
|
|
409
|
+
<br />
|
|
410
|
+
</>
|
|
411
|
+
)}
|
|
412
|
+
{primarySite.address2 && (
|
|
413
|
+
<>
|
|
414
|
+
{primarySite.address2}
|
|
415
|
+
<br />
|
|
416
|
+
</>
|
|
417
|
+
)}
|
|
418
|
+
{primarySite.suburb &&
|
|
419
|
+
primarySite.state &&
|
|
420
|
+
primarySite.postcode && (
|
|
421
|
+
<>
|
|
422
|
+
{primarySite.suburb}
|
|
423
|
+
,{' '}
|
|
424
|
+
{
|
|
425
|
+
primarySite
|
|
426
|
+
.state
|
|
427
|
+
.initial
|
|
428
|
+
}
|
|
429
|
+
,{' '}
|
|
430
|
+
{
|
|
431
|
+
primarySite.postcode
|
|
432
|
+
}
|
|
433
|
+
<br />
|
|
434
|
+
</>
|
|
435
|
+
)}
|
|
436
|
+
{primaryContact &&
|
|
437
|
+
primaryContact.mobile && (
|
|
438
|
+
<>
|
|
439
|
+
{
|
|
440
|
+
primaryContact.mobile
|
|
441
|
+
}
|
|
442
|
+
<br />
|
|
443
|
+
</>
|
|
444
|
+
)}
|
|
445
|
+
{primaryContact &&
|
|
446
|
+
primaryContact.email && (
|
|
447
|
+
<>
|
|
448
|
+
{
|
|
449
|
+
primaryContact.email
|
|
450
|
+
}
|
|
451
|
+
</>
|
|
452
|
+
)}
|
|
453
|
+
</>
|
|
454
|
+
);
|
|
455
|
+
})()}
|
|
456
|
+
</>
|
|
457
|
+
) : (
|
|
458
|
+
''
|
|
459
|
+
)}
|
|
221
460
|
</div>
|
|
222
461
|
</div>
|
|
223
462
|
<div className={styles.quoteParty}>
|
|
@@ -625,6 +625,28 @@ const GenericReport = ({ setting = {} }) => {
|
|
|
625
625
|
const fetchDatabaseTables = async () => {
|
|
626
626
|
setIsLoadingTables(true);
|
|
627
627
|
|
|
628
|
+
// List of tables to hide from the report builder
|
|
629
|
+
const hiddenTables = [
|
|
630
|
+
'audits',
|
|
631
|
+
'crm_settings',
|
|
632
|
+
'two_factor_remember_tokens',
|
|
633
|
+
'system_requests',
|
|
634
|
+
'system_request_types',
|
|
635
|
+
'roles',
|
|
636
|
+
'role_has_permissions',
|
|
637
|
+
'model_has_roles',
|
|
638
|
+
'model_has_permissions',
|
|
639
|
+
'files',
|
|
640
|
+
'migrations',
|
|
641
|
+
'failed_jobs',
|
|
642
|
+
'password_resets',
|
|
643
|
+
'permissions',
|
|
644
|
+
'priorities',
|
|
645
|
+
'sessions',
|
|
646
|
+
'safebook',
|
|
647
|
+
'safebook_category',
|
|
648
|
+
];
|
|
649
|
+
|
|
628
650
|
if (tableUrl) {
|
|
629
651
|
try {
|
|
630
652
|
// Use CustomFetch to make a POST request to the API
|
|
@@ -634,7 +656,11 @@ const GenericReport = ({ setting = {} }) => {
|
|
|
634
656
|
const tablesData = res.data?.data || res.data;
|
|
635
657
|
|
|
636
658
|
if (Array.isArray(tablesData)) {
|
|
637
|
-
|
|
659
|
+
// Filter out the hidden tables
|
|
660
|
+
const filteredTables = tablesData.filter(
|
|
661
|
+
(table) => !hiddenTables.includes(table.name)
|
|
662
|
+
);
|
|
663
|
+
setTables(filteredTables);
|
|
638
664
|
} else {
|
|
639
665
|
console.error('Invalid tables data format:', tablesData);
|
|
640
666
|
setTables([]);
|
|
@@ -1457,8 +1483,388 @@ const GenericReport = ({ setting = {} }) => {
|
|
|
1457
1483
|
}
|
|
1458
1484
|
};
|
|
1459
1485
|
|
|
1486
|
+
// Generate PDF directly using server-side generation
|
|
1487
|
+
const handleDirectPdfGeneration = async () => {
|
|
1488
|
+
if (selectedColumns.length === 0) {
|
|
1489
|
+
toast.error('Please select at least one column for your report');
|
|
1490
|
+
return;
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
if (!reportName) {
|
|
1494
|
+
toast.error('Please enter a name for your report');
|
|
1495
|
+
return;
|
|
1496
|
+
}
|
|
1497
|
+
|
|
1498
|
+
setIsLoading(true);
|
|
1499
|
+
const toastId = toast.loading('Exporting report as PDF...');
|
|
1500
|
+
|
|
1501
|
+
try {
|
|
1502
|
+
// First, execute the query to get the data
|
|
1503
|
+
// Prepare query configuration
|
|
1504
|
+
const queryConfig = {
|
|
1505
|
+
mainTable: selectedTable,
|
|
1506
|
+
columns: selectedColumns.map((col) => ({
|
|
1507
|
+
table: col.table,
|
|
1508
|
+
column: col.column,
|
|
1509
|
+
alias: col.alias || col.displayName,
|
|
1510
|
+
})),
|
|
1511
|
+
joins: joins.map((join) => ({
|
|
1512
|
+
joinType: join.joinType,
|
|
1513
|
+
targetTable: join.targetTable,
|
|
1514
|
+
sourceColumn: join.sourceColumn,
|
|
1515
|
+
targetColumn: join.targetColumn,
|
|
1516
|
+
})),
|
|
1517
|
+
filters: flattenFilterCriteria(filterCriteria),
|
|
1518
|
+
sorting: sortCriteria,
|
|
1519
|
+
};
|
|
1520
|
+
|
|
1521
|
+
// Execute query to get data
|
|
1522
|
+
const queryRes = await CustomFetch(executeUrl, 'POST', {
|
|
1523
|
+
query: queryConfig,
|
|
1524
|
+
limit: 1000, // Get more data for the PDF
|
|
1525
|
+
offset: 0,
|
|
1526
|
+
});
|
|
1527
|
+
|
|
1528
|
+
if (!queryRes.data?.success) {
|
|
1529
|
+
throw new Error(
|
|
1530
|
+
queryRes.data?.message ||
|
|
1531
|
+
'Failed to execute query for PDF generation'
|
|
1532
|
+
);
|
|
1533
|
+
}
|
|
1534
|
+
|
|
1535
|
+
// Get the data from the query result
|
|
1536
|
+
const reportData = queryRes.data.data || [];
|
|
1537
|
+
const totalResults = queryRes.data.total || 0;
|
|
1538
|
+
|
|
1539
|
+
// Generate HTML for the PDF
|
|
1540
|
+
let htmlContent = `
|
|
1541
|
+
<!DOCTYPE html>
|
|
1542
|
+
<html>
|
|
1543
|
+
<head>
|
|
1544
|
+
<meta charset="utf-8">
|
|
1545
|
+
<title>${reportName}</title>
|
|
1546
|
+
<style>
|
|
1547
|
+
body {
|
|
1548
|
+
font-family: Arial, sans-serif;
|
|
1549
|
+
margin: 20px;
|
|
1550
|
+
font-size: 10px; /* Smaller font for more data to fit */
|
|
1551
|
+
}
|
|
1552
|
+
h1 {
|
|
1553
|
+
color: var(--primary-color, #2563eb); /* Use primary color variable with fallback */
|
|
1554
|
+
font-size: 18px;
|
|
1555
|
+
margin-bottom: 10px;
|
|
1556
|
+
border-bottom: 1px solid var(--border-color, #e5e7eb);
|
|
1557
|
+
padding-bottom: 5px;
|
|
1558
|
+
}
|
|
1559
|
+
h2 {
|
|
1560
|
+
color: var(--text-color, #4b5563);
|
|
1561
|
+
font-size: 14px;
|
|
1562
|
+
margin-top: 30px;
|
|
1563
|
+
margin-bottom: 10px;
|
|
1564
|
+
border-bottom: 1px solid var(--border-color, #e5e7eb);
|
|
1565
|
+
padding-bottom: 3px;
|
|
1566
|
+
}
|
|
1567
|
+
.simple-info {
|
|
1568
|
+
margin-bottom: 15px;
|
|
1569
|
+
color: var(--text-secondary-color, #6b7280);
|
|
1570
|
+
font-size: 10px;
|
|
1571
|
+
}
|
|
1572
|
+
.report-details {
|
|
1573
|
+
margin-top: 30px;
|
|
1574
|
+
page-break-before: auto;
|
|
1575
|
+
page-break-inside: avoid;
|
|
1576
|
+
}
|
|
1577
|
+
.report-info {
|
|
1578
|
+
margin-bottom: 15px;
|
|
1579
|
+
color: var(--text-secondary-color, #666);
|
|
1580
|
+
font-size: 9px;
|
|
1581
|
+
display: flex;
|
|
1582
|
+
justify-content: space-between;
|
|
1583
|
+
flex-wrap: wrap;
|
|
1584
|
+
}
|
|
1585
|
+
.report-info-section {
|
|
1586
|
+
margin-right: 20px;
|
|
1587
|
+
margin-bottom: 10px;
|
|
1588
|
+
flex: 1;
|
|
1589
|
+
min-width: 150px;
|
|
1590
|
+
}
|
|
1591
|
+
.report-info-label {
|
|
1592
|
+
font-weight: bold;
|
|
1593
|
+
margin-bottom: 2px;
|
|
1594
|
+
color: var(--text-color, #4b5563);
|
|
1595
|
+
}
|
|
1596
|
+
table {
|
|
1597
|
+
width: 100%;
|
|
1598
|
+
border-collapse: collapse;
|
|
1599
|
+
margin-bottom: 20px;
|
|
1600
|
+
font-size: 9px; /* Even smaller font for table data */
|
|
1601
|
+
}
|
|
1602
|
+
th {
|
|
1603
|
+
background-color: var(--table-header-bg, #f3f4f6);
|
|
1604
|
+
text-align: left;
|
|
1605
|
+
padding: 6px;
|
|
1606
|
+
border: 1px solid var(--border-color-dark, #d1d5db);
|
|
1607
|
+
font-weight: bold;
|
|
1608
|
+
color: var(--text-color-dark, #1f2937);
|
|
1609
|
+
}
|
|
1610
|
+
td {
|
|
1611
|
+
padding: 5px;
|
|
1612
|
+
border: 1px solid var(--border-color, #e5e7eb);
|
|
1613
|
+
vertical-align: top; /* Align to top for multi-line content */
|
|
1614
|
+
word-break: break-word; /* Break long words */
|
|
1615
|
+
}
|
|
1616
|
+
tr:nth-child(even) {
|
|
1617
|
+
background-color: var(--table-row-alt, #f9fafb);
|
|
1618
|
+
}
|
|
1619
|
+
.footer {
|
|
1620
|
+
margin-top: 20px;
|
|
1621
|
+
font-size: 8px;
|
|
1622
|
+
color: var(--text-secondary-color, #6b7280);
|
|
1623
|
+
text-align: center;
|
|
1624
|
+
border-top: 1px solid var(--border-color, #e5e7eb);
|
|
1625
|
+
padding-top: 10px;
|
|
1626
|
+
}
|
|
1627
|
+
.page-number:before {
|
|
1628
|
+
content: "Page " counter(page);
|
|
1629
|
+
}
|
|
1630
|
+
/* Pre-formatted text for JSON */
|
|
1631
|
+
.json-data {
|
|
1632
|
+
font-family: monospace;
|
|
1633
|
+
white-space: pre-wrap;
|
|
1634
|
+
font-size: 8px;
|
|
1635
|
+
color: var(--code-color, #374151);
|
|
1636
|
+
background-color: var(--code-bg, #f3f4f6);
|
|
1637
|
+
}
|
|
1638
|
+
/* Define CSS variables that match the project's root variables */
|
|
1639
|
+
:root {
|
|
1640
|
+
--primary-color: #2563eb;
|
|
1641
|
+
--secondary-color: #4f46e5;
|
|
1642
|
+
--text-color: #4b5563;
|
|
1643
|
+
--text-color-dark: #1f2937;
|
|
1644
|
+
--text-secondary-color: #6b7280;
|
|
1645
|
+
--border-color: #e5e7eb;
|
|
1646
|
+
--border-color-dark: #d1d5db;
|
|
1647
|
+
--table-header-bg: #f3f4f6;
|
|
1648
|
+
--table-row-alt: #f9fafb;
|
|
1649
|
+
--code-color: #374151;
|
|
1650
|
+
--code-bg: #f3f4f6;
|
|
1651
|
+
}
|
|
1652
|
+
</style>
|
|
1653
|
+
</head>
|
|
1654
|
+
<body>
|
|
1655
|
+
<h1>${reportName}</h1>
|
|
1656
|
+
<div class="simple-info">
|
|
1657
|
+
<p>Generated on: ${moment().format(
|
|
1658
|
+
'MMMM D, YYYY'
|
|
1659
|
+
)} | Total records: ${totalResults}</p>
|
|
1660
|
+
</div>
|
|
1661
|
+
`;
|
|
1662
|
+
|
|
1663
|
+
// Add table with data
|
|
1664
|
+
if (reportData.length > 0) {
|
|
1665
|
+
htmlContent += '<table>';
|
|
1666
|
+
|
|
1667
|
+
// Table headers
|
|
1668
|
+
htmlContent += '<thead><tr>';
|
|
1669
|
+
const columns = Object.keys(reportData[0]);
|
|
1670
|
+
columns.forEach((column) => {
|
|
1671
|
+
htmlContent += `<th>${column}</th>`;
|
|
1672
|
+
});
|
|
1673
|
+
htmlContent += '</tr></thead>';
|
|
1674
|
+
|
|
1675
|
+
// Table body
|
|
1676
|
+
htmlContent += '<tbody>';
|
|
1677
|
+
reportData.forEach((row) => {
|
|
1678
|
+
htmlContent += '<tr>';
|
|
1679
|
+
columns.forEach((column) => {
|
|
1680
|
+
let cellValue = row[column];
|
|
1681
|
+
|
|
1682
|
+
// Handle different data types
|
|
1683
|
+
if (cellValue === null || cellValue === undefined) {
|
|
1684
|
+
cellValue = '';
|
|
1685
|
+
} else if (isJsonValue(cellValue)) {
|
|
1686
|
+
// Format JSON data
|
|
1687
|
+
try {
|
|
1688
|
+
// If it's a string that looks like JSON, parse it first
|
|
1689
|
+
if (typeof cellValue === 'string') {
|
|
1690
|
+
try {
|
|
1691
|
+
cellValue = JSON.parse(cellValue);
|
|
1692
|
+
} catch (e) {
|
|
1693
|
+
// If parsing fails, keep as string
|
|
1694
|
+
}
|
|
1695
|
+
}
|
|
1696
|
+
|
|
1697
|
+
// Transform JSON to have human-readable keys
|
|
1698
|
+
if (
|
|
1699
|
+
typeof cellValue === 'object' &&
|
|
1700
|
+
cellValue !== null
|
|
1701
|
+
) {
|
|
1702
|
+
cellValue =
|
|
1703
|
+
transformJsonForDisplay(cellValue);
|
|
1704
|
+
}
|
|
1705
|
+
|
|
1706
|
+
// Convert to formatted string
|
|
1707
|
+
cellValue = JSON.stringify(cellValue, null, 2);
|
|
1708
|
+
|
|
1709
|
+
// Limit length for PDF readability
|
|
1710
|
+
if (cellValue.length > 200) {
|
|
1711
|
+
cellValue =
|
|
1712
|
+
cellValue.substring(0, 200) + '...';
|
|
1713
|
+
}
|
|
1714
|
+
|
|
1715
|
+
// Wrap JSON in a styled span
|
|
1716
|
+
cellValue = `<span class="json-data">${cellValue}</span>`;
|
|
1717
|
+
} catch (e) {
|
|
1718
|
+
cellValue = String(cellValue);
|
|
1719
|
+
}
|
|
1720
|
+
} else if (typeof cellValue === 'string') {
|
|
1721
|
+
// Truncate long text fields for better PDF layout
|
|
1722
|
+
if (cellValue.length > 100) {
|
|
1723
|
+
cellValue = cellValue.substring(0, 100) + '...';
|
|
1724
|
+
}
|
|
1725
|
+
}
|
|
1726
|
+
|
|
1727
|
+
// Check if the value already contains HTML (for JSON data)
|
|
1728
|
+
if (
|
|
1729
|
+
typeof cellValue === 'string' &&
|
|
1730
|
+
cellValue.startsWith('<span class="json-data">')
|
|
1731
|
+
) {
|
|
1732
|
+
// It's already HTML-formatted JSON, use it directly
|
|
1733
|
+
htmlContent += `<td>${cellValue}</td>`;
|
|
1734
|
+
} else {
|
|
1735
|
+
// Escape HTML to prevent injection
|
|
1736
|
+
const escapeHtml = (unsafe) => {
|
|
1737
|
+
return String(unsafe)
|
|
1738
|
+
.replace(/&/g, '&')
|
|
1739
|
+
.replace(/</g, '<')
|
|
1740
|
+
.replace(/>/g, '>')
|
|
1741
|
+
.replace(/"/g, '"')
|
|
1742
|
+
.replace(/'/g, ''');
|
|
1743
|
+
};
|
|
1744
|
+
|
|
1745
|
+
htmlContent += `<td>${escapeHtml(cellValue)}</td>`;
|
|
1746
|
+
}
|
|
1747
|
+
});
|
|
1748
|
+
htmlContent += '</tr>';
|
|
1749
|
+
});
|
|
1750
|
+
htmlContent += '</tbody></table>';
|
|
1751
|
+
} else {
|
|
1752
|
+
htmlContent += '<p>No data found for this report.</p>';
|
|
1753
|
+
}
|
|
1754
|
+
|
|
1755
|
+
// Add detailed report information at the end
|
|
1756
|
+
htmlContent += `
|
|
1757
|
+
<div class="report-details">
|
|
1758
|
+
<h2>Report Details</h2>
|
|
1759
|
+
<div class="report-info">
|
|
1760
|
+
<div class="report-info-section">
|
|
1761
|
+
<div class="report-info-label">Report Information</div>
|
|
1762
|
+
<p>Name: ${reportName}</p>
|
|
1763
|
+
<p>Generated on: ${moment().format(
|
|
1764
|
+
'MMMM D, YYYY [at] h:mm A'
|
|
1765
|
+
)}</p>
|
|
1766
|
+
<p>Total records: ${totalResults}</p>
|
|
1767
|
+
</div>
|
|
1768
|
+
<div class="report-info-section">
|
|
1769
|
+
<div class="report-info-label">Data Source</div>
|
|
1770
|
+
<p>Main table: ${formatName(selectedTable)}</p>
|
|
1771
|
+
<p>Columns: ${selectedColumns.length}</p>
|
|
1772
|
+
<p>Joins: ${joins.length}</p>
|
|
1773
|
+
</div>
|
|
1774
|
+
${
|
|
1775
|
+
filterCriteria &&
|
|
1776
|
+
filterCriteria.groups &&
|
|
1777
|
+
filterCriteria.groups.some(
|
|
1778
|
+
(group) =>
|
|
1779
|
+
group.filters && group.filters.length > 0
|
|
1780
|
+
)
|
|
1781
|
+
? `
|
|
1782
|
+
<div class="report-info-section">
|
|
1783
|
+
<div class="report-info-label">Filters Applied</div>
|
|
1784
|
+
<p>Filter groups: ${
|
|
1785
|
+
filterCriteria.groups.filter(
|
|
1786
|
+
(group) =>
|
|
1787
|
+
group.filters &&
|
|
1788
|
+
group.filters.length > 0
|
|
1789
|
+
).length
|
|
1790
|
+
}</p>
|
|
1791
|
+
<p>Total filters: ${filterCriteria.groups.reduce(
|
|
1792
|
+
(total, group) =>
|
|
1793
|
+
total +
|
|
1794
|
+
(group.filters ? group.filters.length : 0),
|
|
1795
|
+
0
|
|
1796
|
+
)}</p>
|
|
1797
|
+
</div>
|
|
1798
|
+
`
|
|
1799
|
+
: ''
|
|
1800
|
+
}
|
|
1801
|
+
</div>
|
|
1802
|
+
</div>
|
|
1803
|
+
`;
|
|
1804
|
+
|
|
1805
|
+
// Add footer
|
|
1806
|
+
htmlContent += `
|
|
1807
|
+
<div class="footer">
|
|
1808
|
+
<p>Report generated from ${formatName(
|
|
1809
|
+
selectedTable
|
|
1810
|
+
)} table on ${moment().format('YYYY-MM-DD')} |
|
|
1811
|
+
${selectedColumns.length} columns selected |
|
|
1812
|
+
${joins.length > 0 ? `${joins.length} table joins | ` : ''}
|
|
1813
|
+
<span class="page-number"></span></p>
|
|
1814
|
+
</div>
|
|
1815
|
+
</body>
|
|
1816
|
+
</html>`;
|
|
1817
|
+
|
|
1818
|
+
// Use Download to call the PDF generation endpoint with HTML content
|
|
1819
|
+
const res = await Download('/ajax/pdf/generate-from-html', 'POST', {
|
|
1820
|
+
html: htmlContent,
|
|
1821
|
+
filename: `${moment().format('YYYYMMDD')}_${reportName}.pdf`,
|
|
1822
|
+
paper: 'a4',
|
|
1823
|
+
orientation: 'landscape', // Use landscape for reports with many columns
|
|
1824
|
+
download: true,
|
|
1825
|
+
});
|
|
1826
|
+
|
|
1827
|
+
if (res.data instanceof Blob) {
|
|
1828
|
+
// Use FileSaver.js to save the file
|
|
1829
|
+
saveAs(
|
|
1830
|
+
res.data,
|
|
1831
|
+
`${moment().format('YYYYMMDD')}_${reportName}.pdf`
|
|
1832
|
+
);
|
|
1833
|
+
|
|
1834
|
+
toast.update(toastId, {
|
|
1835
|
+
render: 'PDF exported successfully. Check your downloads folder.',
|
|
1836
|
+
type: 'success',
|
|
1837
|
+
isLoading: false,
|
|
1838
|
+
autoClose: 5000,
|
|
1839
|
+
closeButton: true,
|
|
1840
|
+
});
|
|
1841
|
+
} else {
|
|
1842
|
+
throw new Error('Invalid response format from PDF generation');
|
|
1843
|
+
}
|
|
1844
|
+
} catch (error) {
|
|
1845
|
+
console.error('Error in PDF generation:', error);
|
|
1846
|
+
toast.update(toastId, {
|
|
1847
|
+
render: `Failed to export PDF: ${
|
|
1848
|
+
error.message || 'Unknown error'
|
|
1849
|
+
}`,
|
|
1850
|
+
type: 'error',
|
|
1851
|
+
isLoading: false,
|
|
1852
|
+
autoClose: 5000,
|
|
1853
|
+
closeButton: true,
|
|
1854
|
+
});
|
|
1855
|
+
} finally {
|
|
1856
|
+
setIsLoading(false);
|
|
1857
|
+
}
|
|
1858
|
+
};
|
|
1859
|
+
|
|
1460
1860
|
// Export report as CSV, Excel or PDF
|
|
1461
1861
|
const handleExportReport = async (format = 'csv') => {
|
|
1862
|
+
// If it's a PDF and we want to try the alternative method first
|
|
1863
|
+
const useAlternativePdfMethod = false; // Set to true to enable alternative method by default
|
|
1864
|
+
if (format === 'pdf' && useAlternativePdfMethod) {
|
|
1865
|
+
return handleDirectPdfGeneration();
|
|
1866
|
+
}
|
|
1867
|
+
|
|
1462
1868
|
if (selectedColumns.length === 0) {
|
|
1463
1869
|
toast.error('Please select at least one column for your report');
|
|
1464
1870
|
return;
|
|
@@ -1514,10 +1920,30 @@ const GenericReport = ({ setting = {} }) => {
|
|
|
1514
1920
|
}
|
|
1515
1921
|
|
|
1516
1922
|
// Use Download utility for file downloads
|
|
1923
|
+
// For PDF exports, we'll add a custom timeout and error handler
|
|
1517
1924
|
const res = await Download(
|
|
1518
1925
|
'/ajax/reportBuilder/export',
|
|
1519
1926
|
'POST',
|
|
1520
|
-
exportData
|
|
1927
|
+
exportData,
|
|
1928
|
+
// Success callback
|
|
1929
|
+
format === 'pdf'
|
|
1930
|
+
? (data) => {
|
|
1931
|
+
console.log(
|
|
1932
|
+
'PDF export successful, received data of type:',
|
|
1933
|
+
typeof data
|
|
1934
|
+
);
|
|
1935
|
+
console.log('PDF data size:', data.size);
|
|
1936
|
+
}
|
|
1937
|
+
: null,
|
|
1938
|
+
// Error callback
|
|
1939
|
+
format === 'pdf'
|
|
1940
|
+
? (errorMsg) => {
|
|
1941
|
+
console.error(
|
|
1942
|
+
'PDF export error from Download component:',
|
|
1943
|
+
errorMsg
|
|
1944
|
+
);
|
|
1945
|
+
}
|
|
1946
|
+
: null
|
|
1521
1947
|
);
|
|
1522
1948
|
|
|
1523
1949
|
if (res.data instanceof Blob) {
|
|
@@ -1527,52 +1953,106 @@ const GenericReport = ({ setting = {} }) => {
|
|
|
1527
1953
|
|
|
1528
1954
|
// For PDF (HTML) format, open in a new window for printing
|
|
1529
1955
|
if (format === 'pdf') {
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
printButton.style.padding = '8px 16px';
|
|
1549
|
-
printButton.style.backgroundColor = '#4CAF50';
|
|
1550
|
-
printButton.style.color = 'white';
|
|
1551
|
-
printButton.style.border = 'none';
|
|
1552
|
-
printButton.style.borderRadius = '4px';
|
|
1553
|
-
printButton.style.cursor = 'pointer';
|
|
1554
|
-
|
|
1555
|
-
printButton.onclick = function () {
|
|
1556
|
-
// Hide the button before printing
|
|
1557
|
-
this.style.display = 'none';
|
|
1558
|
-
printWindow.print();
|
|
1559
|
-
this.style.display = 'block';
|
|
1560
|
-
};
|
|
1956
|
+
try {
|
|
1957
|
+
// Check if the blob is valid and has content
|
|
1958
|
+
if (!res.data || res.data.size === 0) {
|
|
1959
|
+
throw new Error(
|
|
1960
|
+
'Received empty PDF data from server'
|
|
1961
|
+
);
|
|
1962
|
+
}
|
|
1963
|
+
|
|
1964
|
+
// Log the blob type for debugging
|
|
1965
|
+
console.log('PDF Blob type:', res.data.type);
|
|
1966
|
+
|
|
1967
|
+
// Convert blob to URL - use application/pdf if it's a PDF, otherwise text/html
|
|
1968
|
+
const blobType =
|
|
1969
|
+
res.data.type === 'application/pdf'
|
|
1970
|
+
? 'application/pdf'
|
|
1971
|
+
: 'text/html';
|
|
1972
|
+
const blob = new Blob([res.data], { type: blobType });
|
|
1973
|
+
const url = URL.createObjectURL(blob);
|
|
1561
1974
|
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1975
|
+
// Open in a new window
|
|
1976
|
+
const printWindow = window.open(url, '_blank');
|
|
1977
|
+
|
|
1978
|
+
// Check if window was successfully opened
|
|
1979
|
+
if (!printWindow) {
|
|
1980
|
+
throw new Error(
|
|
1981
|
+
'Failed to open PDF in new window. Please check your popup blocker settings.'
|
|
1565
1982
|
);
|
|
1566
|
-
}
|
|
1567
|
-
}
|
|
1983
|
+
}
|
|
1568
1984
|
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1985
|
+
// Add print instructions
|
|
1986
|
+
if (printWindow) {
|
|
1987
|
+
printWindow.onload = function () {
|
|
1988
|
+
// Add print button at the top
|
|
1989
|
+
const printButton =
|
|
1990
|
+
printWindow.document.createElement(
|
|
1991
|
+
'button'
|
|
1992
|
+
);
|
|
1993
|
+
printButton.innerHTML = 'Print PDF';
|
|
1994
|
+
printButton.style.position = 'fixed';
|
|
1995
|
+
printButton.style.top = '10px';
|
|
1996
|
+
printButton.style.right = '10px';
|
|
1997
|
+
printButton.style.zIndex = '9999';
|
|
1998
|
+
printButton.style.padding = '8px 16px';
|
|
1999
|
+
printButton.style.backgroundColor = '#4CAF50';
|
|
2000
|
+
printButton.style.color = 'white';
|
|
2001
|
+
printButton.style.border = 'none';
|
|
2002
|
+
printButton.style.borderRadius = '4px';
|
|
2003
|
+
printButton.style.cursor = 'pointer';
|
|
2004
|
+
|
|
2005
|
+
printButton.onclick = function () {
|
|
2006
|
+
// Hide the button before printing
|
|
2007
|
+
this.style.display = 'none';
|
|
2008
|
+
printWindow.print();
|
|
2009
|
+
this.style.display = 'block';
|
|
2010
|
+
};
|
|
2011
|
+
|
|
2012
|
+
printWindow.document.body.insertBefore(
|
|
2013
|
+
printButton,
|
|
2014
|
+
printWindow.document.body.firstChild
|
|
2015
|
+
);
|
|
2016
|
+
};
|
|
2017
|
+
}
|
|
2018
|
+
|
|
2019
|
+
toast.update(toastId, {
|
|
2020
|
+
render: `Report opened in a new window. Use browser's print function to save as PDF.`,
|
|
2021
|
+
type: 'success',
|
|
2022
|
+
isLoading: false,
|
|
2023
|
+
autoClose: 5000,
|
|
2024
|
+
closeButton: true,
|
|
2025
|
+
});
|
|
2026
|
+
} catch (pdfError) {
|
|
2027
|
+
console.error('PDF rendering error:', pdfError);
|
|
2028
|
+
toast.update(toastId, {
|
|
2029
|
+
render: `Failed to render PDF: ${pdfError.message}. Trying alternative method...`,
|
|
2030
|
+
type: 'warning',
|
|
2031
|
+
isLoading: true,
|
|
2032
|
+
autoClose: false,
|
|
2033
|
+
});
|
|
2034
|
+
|
|
2035
|
+
// Fallback method: Try direct download instead of opening in a new window
|
|
2036
|
+
try {
|
|
2037
|
+
saveAs(res.data, fileName);
|
|
2038
|
+
toast.update(toastId, {
|
|
2039
|
+
render: `PDF exported successfully. Opening with your default PDF viewer.`,
|
|
2040
|
+
type: 'success',
|
|
2041
|
+
isLoading: false,
|
|
2042
|
+
autoClose: 5000,
|
|
2043
|
+
closeButton: true,
|
|
2044
|
+
});
|
|
2045
|
+
} catch (fallbackError) {
|
|
2046
|
+
console.error('PDF fallback error:', fallbackError);
|
|
2047
|
+
toast.update(toastId, {
|
|
2048
|
+
render: `Failed to export PDF: ${fallbackError.message}`,
|
|
2049
|
+
type: 'error',
|
|
2050
|
+
isLoading: false,
|
|
2051
|
+
autoClose: 5000,
|
|
2052
|
+
closeButton: true,
|
|
2053
|
+
});
|
|
2054
|
+
}
|
|
2055
|
+
}
|
|
1576
2056
|
} else {
|
|
1577
2057
|
// Use FileSaver.js to save the file for CSV and Excel
|
|
1578
2058
|
saveAs(res.data, fileName);
|
|
@@ -1590,13 +2070,31 @@ const GenericReport = ({ setting = {} }) => {
|
|
|
1590
2070
|
}
|
|
1591
2071
|
} catch (error) {
|
|
1592
2072
|
console.error('Error exporting report:', error);
|
|
2073
|
+
|
|
2074
|
+
// Provide more detailed error information
|
|
2075
|
+
let errorMessage = error.message || 'Unknown error';
|
|
2076
|
+
|
|
2077
|
+
// Check for network errors specifically
|
|
2078
|
+
if (
|
|
2079
|
+
error.name === 'TypeError' &&
|
|
2080
|
+
errorMessage.includes('NetworkError')
|
|
2081
|
+
) {
|
|
2082
|
+
errorMessage = `Network error when attempting to fetch resource. This could be due to:
|
|
2083
|
+
1. Server timeout - PDF generation may take longer than expected
|
|
2084
|
+
2. CORS issues - Check server configuration
|
|
2085
|
+
3. Server is unreachable - Check your connection`;
|
|
2086
|
+
|
|
2087
|
+
// Log additional debugging information
|
|
2088
|
+
console.log('Export format:', format);
|
|
2089
|
+
console.log('Request URL:', '/ajax/reportBuilder/export');
|
|
2090
|
+
console.log('Query size:', JSON.stringify(exportData).length);
|
|
2091
|
+
}
|
|
2092
|
+
|
|
1593
2093
|
toast.update(toastId, {
|
|
1594
|
-
render: `Failed to export report: ${
|
|
1595
|
-
error.message || 'Unknown error'
|
|
1596
|
-
}`,
|
|
2094
|
+
render: `Failed to export report: ${errorMessage}`,
|
|
1597
2095
|
type: 'error',
|
|
1598
2096
|
isLoading: false,
|
|
1599
|
-
autoClose:
|
|
2097
|
+
autoClose: 8000,
|
|
1600
2098
|
closeButton: true,
|
|
1601
2099
|
});
|
|
1602
2100
|
} finally {
|
|
@@ -4437,7 +4935,7 @@ const GenericReport = ({ setting = {} }) => {
|
|
|
4437
4935
|
</button>
|
|
4438
4936
|
<button
|
|
4439
4937
|
className={`${styles.btn} ${styles.btnSecondary}`}
|
|
4440
|
-
onClick={
|
|
4938
|
+
onClick={handleDirectPdfGeneration}
|
|
4441
4939
|
disabled={
|
|
4442
4940
|
isLoading ||
|
|
4443
4941
|
selectedColumns.length === 0 ||
|