@visns-studio/visns-components 5.7.8 → 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 +1 -1
- package/src/components/crm/Download.jsx +3 -1
- package/src/components/crm/generic/ActionButtons.jsx +1 -1
- package/src/components/crm/generic/GenericQuote.jsx +380 -15
- package/src/components/crm/generic/GenericReport.jsx +548 -50
- package/src/components/crm/generic/styles/GenericQuote.module.scss +39 -0
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;
|
|
@@ -1,24 +1,47 @@
|
|
|
1
1
|
import '../../styles/global.css';
|
|
2
2
|
|
|
3
3
|
import React, { useState, useEffect } from 'react';
|
|
4
|
-
import { useParams } from 'react-router-dom';
|
|
4
|
+
import { useParams, useNavigate } from 'react-router-dom';
|
|
5
|
+
import Popup from 'reactjs-popup';
|
|
5
6
|
import moment from 'moment';
|
|
6
7
|
import get from 'lodash/get';
|
|
8
|
+
import { saveAs } from 'file-saver';
|
|
9
|
+
import { toast } from 'react-toastify';
|
|
7
10
|
|
|
8
11
|
import CustomFetch from '../../crm/Fetch';
|
|
9
12
|
import ActionButtons from './ActionButtons';
|
|
13
|
+
import Form from '../../crm/Form';
|
|
10
14
|
import { formatCurrency } from './utils/formatters';
|
|
11
15
|
|
|
12
16
|
import styles from './styles/GenericQuote.module.scss';
|
|
13
17
|
|
|
14
18
|
function GenericQuote({ logo, setting, urlParam }) {
|
|
15
19
|
const routeParams = useParams();
|
|
20
|
+
const navigate = useNavigate();
|
|
16
21
|
|
|
17
|
-
const { actions: actionItems, page, tables } = setting;
|
|
22
|
+
const { actions: actionItems, page, tables, form } = setting;
|
|
18
23
|
|
|
19
24
|
const [data, setData] = useState({});
|
|
20
25
|
const [actions, setActions] = useState(actionItems || []);
|
|
21
26
|
|
|
27
|
+
// Modal state variables
|
|
28
|
+
const [modalShow, setModalShow] = useState(false);
|
|
29
|
+
const [formType, setFormType] = useState('');
|
|
30
|
+
const [formId, setFormId] = useState(0);
|
|
31
|
+
const [formData, setFormData] = useState({});
|
|
32
|
+
|
|
33
|
+
// Modal functions
|
|
34
|
+
const modalOpen = (type, id) => {
|
|
35
|
+
setModalShow(true);
|
|
36
|
+
setFormType(type);
|
|
37
|
+
setFormId(id);
|
|
38
|
+
setFormData(form);
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const modalClose = () => {
|
|
42
|
+
setModalShow(false);
|
|
43
|
+
};
|
|
44
|
+
|
|
22
45
|
useEffect(() => {
|
|
23
46
|
if (actionItems && actionItems.length > 0) {
|
|
24
47
|
setActions(actionItems);
|
|
@@ -44,31 +67,228 @@ function GenericQuote({ logo, setting, urlParam }) {
|
|
|
44
67
|
fetchData();
|
|
45
68
|
}, [routeParams['*']]);
|
|
46
69
|
|
|
47
|
-
const handleActionClick = (
|
|
48
|
-
// Handle different actions based on
|
|
49
|
-
|
|
70
|
+
const handleActionClick = (action) => {
|
|
71
|
+
// Handle different actions based on action properties
|
|
72
|
+
console.log(`Action ${action.id} clicked:`, action);
|
|
73
|
+
|
|
74
|
+
// Check if action has a type property
|
|
75
|
+
if (action.type === 'link' && action.url) {
|
|
76
|
+
// Handle link type actions
|
|
77
|
+
let url = action.url;
|
|
78
|
+
|
|
79
|
+
// If urlParam is specified, append the corresponding data value
|
|
80
|
+
if (action.urlParam && data[action.urlParam]) {
|
|
81
|
+
url = `${url}${data[action.urlParam]}`;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Navigate to the URL using React Router
|
|
85
|
+
navigate(url);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Handle editModal type
|
|
90
|
+
if (action.type === 'editModal') {
|
|
91
|
+
// Open the edit modal with the current quote data
|
|
92
|
+
modalOpen('update', data.id);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Handle windowOpen type - opens URL in a new browser window/tab
|
|
97
|
+
if (action.type === 'windowOpen' && action.url) {
|
|
98
|
+
// Handle window open actions
|
|
99
|
+
let url = action.url;
|
|
100
|
+
|
|
101
|
+
// If urlParam is specified, append the corresponding data value
|
|
102
|
+
if (action.urlParam && data[action.urlParam]) {
|
|
103
|
+
url = `${url}${data[action.urlParam]}`;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Open the URL in a new window/tab
|
|
107
|
+
window.open(url, '_blank');
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
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
|
+
|
|
191
|
+
// Handle other actions based on their id if no specific type is defined
|
|
192
|
+
switch (action.id) {
|
|
50
193
|
case 'addItem':
|
|
51
194
|
// Handle add item action
|
|
52
195
|
console.log('Add item clicked');
|
|
53
196
|
break;
|
|
54
197
|
case 'editQuote':
|
|
55
|
-
//
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
// Handle view ticket action
|
|
60
|
-
console.log('View ticket clicked');
|
|
198
|
+
// If no type is specified but id is editQuote, default to opening the edit modal
|
|
199
|
+
if (!action.type) {
|
|
200
|
+
modalOpen('update', data.id);
|
|
201
|
+
}
|
|
61
202
|
break;
|
|
62
203
|
case 'saveAsPdf':
|
|
63
|
-
//
|
|
64
|
-
|
|
204
|
+
// If no type is specified but id is saveAsPdf, default to PDF generation
|
|
205
|
+
if (!action.type) {
|
|
206
|
+
console.log('Save as PDF clicked');
|
|
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
|
+
});
|
|
282
|
+
}
|
|
65
283
|
break;
|
|
66
284
|
case 'saveToTicket':
|
|
67
285
|
// Handle save to ticket action
|
|
68
286
|
console.log('Save to ticket clicked');
|
|
69
287
|
break;
|
|
70
288
|
default:
|
|
71
|
-
console.log(
|
|
289
|
+
console.log(
|
|
290
|
+
`Action ${action.id} clicked with no specific handler`
|
|
291
|
+
);
|
|
72
292
|
}
|
|
73
293
|
};
|
|
74
294
|
|
|
@@ -153,7 +373,90 @@ function GenericQuote({ logo, setting, urlParam }) {
|
|
|
153
373
|
<div className={styles.quoteParty}>
|
|
154
374
|
<h3 className={styles.partyTitle}>Quote To:</h3>
|
|
155
375
|
<div className={styles.partyDetails}>
|
|
156
|
-
{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
|
+
)}
|
|
157
460
|
</div>
|
|
158
461
|
</div>
|
|
159
462
|
<div className={styles.quoteParty}>
|
|
@@ -345,7 +648,69 @@ function GenericQuote({ logo, setting, urlParam }) {
|
|
|
345
648
|
);
|
|
346
649
|
})}
|
|
347
650
|
</div>
|
|
651
|
+
|
|
652
|
+
{/* Terms and Condition Section */}
|
|
653
|
+
{data.terms_and_condition && (
|
|
654
|
+
<div className={styles.termsSection}>
|
|
655
|
+
<h3 className={styles.termsTitle}>
|
|
656
|
+
Terms and Conditions
|
|
657
|
+
</h3>
|
|
658
|
+
<div className={styles.termsContent}>
|
|
659
|
+
{data.terms_and_condition}
|
|
660
|
+
</div>
|
|
661
|
+
</div>
|
|
662
|
+
)}
|
|
348
663
|
</div>
|
|
664
|
+
|
|
665
|
+
{/* Form Modal */}
|
|
666
|
+
<Popup
|
|
667
|
+
open={modalShow}
|
|
668
|
+
onClose={modalClose}
|
|
669
|
+
closeOnDocumentClick={false}
|
|
670
|
+
contentStyle={{ width: '80vw', maxWidth: '1200px' }}
|
|
671
|
+
>
|
|
672
|
+
<div className="modalwrap top--modal modalWide">
|
|
673
|
+
<div className="modal">
|
|
674
|
+
<Form
|
|
675
|
+
closeModal={modalClose}
|
|
676
|
+
columnId={formId}
|
|
677
|
+
fetchTable={() => {
|
|
678
|
+
// Reload the quote data after form submission
|
|
679
|
+
const fetchData = async () => {
|
|
680
|
+
if (
|
|
681
|
+
routeParams[urlParam] &&
|
|
682
|
+
routeParams[urlParam] > 0
|
|
683
|
+
) {
|
|
684
|
+
try {
|
|
685
|
+
const res = await CustomFetch(
|
|
686
|
+
`${page.fetchUrl}/${routeParams[urlParam]}`,
|
|
687
|
+
'GET',
|
|
688
|
+
{}
|
|
689
|
+
);
|
|
690
|
+
setData(res.data);
|
|
691
|
+
} catch (error) {
|
|
692
|
+
console.error(
|
|
693
|
+
'Error fetching data:',
|
|
694
|
+
error
|
|
695
|
+
);
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
};
|
|
699
|
+
fetchData();
|
|
700
|
+
}}
|
|
701
|
+
formSettings={formData}
|
|
702
|
+
formType={formType}
|
|
703
|
+
style={form.style || {}}
|
|
704
|
+
updateForm={setFormData}
|
|
705
|
+
paramValue={
|
|
706
|
+
routeParams[urlParam]
|
|
707
|
+
? routeParams[urlParam]
|
|
708
|
+
: 0
|
|
709
|
+
}
|
|
710
|
+
/>
|
|
711
|
+
</div>
|
|
712
|
+
</div>
|
|
713
|
+
</Popup>
|
|
349
714
|
</div>
|
|
350
715
|
);
|
|
351
716
|
}
|
|
@@ -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 ||
|
|
@@ -521,6 +521,30 @@
|
|
|
521
521
|
margin-top: 30px;
|
|
522
522
|
}
|
|
523
523
|
|
|
524
|
+
.termsSection {
|
|
525
|
+
margin-top: 30px;
|
|
526
|
+
padding: 20px;
|
|
527
|
+
border-top: 1px solid rgba(var(--primary-rgb), 0.1);
|
|
528
|
+
background-color: rgba(var(--primary-rgb), 0.02);
|
|
529
|
+
border-radius: var(--br, 4px);
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
.termsTitle {
|
|
533
|
+
font-size: 1rem;
|
|
534
|
+
font-weight: 600;
|
|
535
|
+
color: var(--primary-color);
|
|
536
|
+
margin: 0 0 15px 0;
|
|
537
|
+
padding-bottom: 8px;
|
|
538
|
+
border-bottom: 1px dashed rgba(var(--primary-rgb), 0.1);
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
.termsContent {
|
|
542
|
+
font-size: 0.9rem;
|
|
543
|
+
line-height: 1.6;
|
|
544
|
+
color: var(--paragraph-color);
|
|
545
|
+
white-space: pre-wrap;
|
|
546
|
+
}
|
|
547
|
+
|
|
524
548
|
.quoteTableSection {
|
|
525
549
|
margin-bottom: 25px;
|
|
526
550
|
}
|
|
@@ -752,4 +776,19 @@
|
|
|
752
776
|
padding: 8px 10px;
|
|
753
777
|
font-size: 0.85rem;
|
|
754
778
|
}
|
|
779
|
+
|
|
780
|
+
.termsSection {
|
|
781
|
+
padding: 15px;
|
|
782
|
+
margin-top: 20px;
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
.termsTitle {
|
|
786
|
+
font-size: 0.9rem;
|
|
787
|
+
margin-bottom: 10px;
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
.termsContent {
|
|
791
|
+
font-size: 0.85rem;
|
|
792
|
+
line-height: 1.5;
|
|
793
|
+
}
|
|
755
794
|
}
|