@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.9",
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
- formData = JSON.stringify(formData);
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 opening PDF in new window
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
- // You could implement a default PDF generation here if needed
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 ? data.customer.name : ''}
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
- setTables(tablesData);
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, '&amp;')
1739
+ .replace(/</g, '&lt;')
1740
+ .replace(/>/g, '&gt;')
1741
+ .replace(/"/g, '&quot;')
1742
+ .replace(/'/g, '&#039;');
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
- // Convert blob to URL
1531
- const blob = new Blob([res.data], { type: 'text/html' });
1532
- const url = URL.createObjectURL(blob);
1533
-
1534
- // Open in a new window
1535
- const printWindow = window.open(url, '_blank');
1536
-
1537
- // Add print instructions
1538
- if (printWindow) {
1539
- printWindow.onload = function () {
1540
- // Add print button at the top
1541
- const printButton =
1542
- printWindow.document.createElement('button');
1543
- printButton.innerHTML = 'Print PDF';
1544
- printButton.style.position = 'fixed';
1545
- printButton.style.top = '10px';
1546
- printButton.style.right = '10px';
1547
- printButton.style.zIndex = '9999';
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
- printWindow.document.body.insertBefore(
1563
- printButton,
1564
- printWindow.document.body.firstChild
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
- toast.update(toastId, {
1570
- render: `Report opened in a new window. Use browser's print function to save as PDF.`,
1571
- type: 'success',
1572
- isLoading: false,
1573
- autoClose: 5000,
1574
- closeButton: true,
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: 5000,
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={() => handleExportReport('pdf')}
4938
+ onClick={handleDirectPdfGeneration}
4441
4939
  disabled={
4442
4940
  isLoading ||
4443
4941
  selectedColumns.length === 0 ||