feat: Implement on-demand PDF generation and remove pre-generation logic
This commit is contained in:
@@ -237,7 +237,8 @@ const CalculationView = ({ apiBaseUrl, project, geometry, labor, materials, exis
|
||||
|
||||
const downloadPdf = async (quoteId) => {
|
||||
try {
|
||||
// Download PDF from database
|
||||
setLoading(true);
|
||||
// Download PDF on-demand - no pre-generation needed
|
||||
const response = await fetch(`${apiBaseUrl}/api/quotes/${quoteId}/pdf`);
|
||||
if (response.ok) {
|
||||
const blob = await response.blob();
|
||||
@@ -250,37 +251,18 @@ const CalculationView = ({ apiBaseUrl, project, geometry, labor, materials, exis
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
} else {
|
||||
throw new Error('PDF ikke tilgængelig');
|
||||
throw new Error('PDF kunne ikke genereres');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error downloading PDF:', error);
|
||||
alert('Fejl ved download af PDF: ' + error.message);
|
||||
}
|
||||
};
|
||||
|
||||
const generatePdf = async (quoteId) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const response = await fetch(`${apiBaseUrl}/api/quotes/${quoteId}/generate-pdf`, {
|
||||
method: 'POST'
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
alert('✅ PDF genereret succesfuldt!');
|
||||
await loadProjectQuotes(); // Refresh quotes list
|
||||
} else {
|
||||
alert(`❌ Fejl: ${data.error}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error generating PDF:', error);
|
||||
alert('❌ Fejl ved generering af PDF');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Remove separate generatePdf function as it's no longer needed
|
||||
|
||||
const formatDate = (dateString) => {
|
||||
return new Date(dateString).toLocaleDateString('da-DK', {
|
||||
year: 'numeric',
|
||||
@@ -505,22 +487,13 @@ const CalculationView = ({ apiBaseUrl, project, geometry, labor, materials, exis
|
||||
🖨️ HTML Version
|
||||
</button>
|
||||
|
||||
{quote.quote_format === 'pdf' && quote.pdf_data ? (
|
||||
<button
|
||||
className="pdf-download-btn"
|
||||
onClick={() => downloadPdf(quote.id)}
|
||||
>
|
||||
📄 Download PDF
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
className="pdf-generate-btn"
|
||||
onClick={() => generatePdf(quote.id)}
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? '⏳' : '📄'} Generér PDF
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
className="pdf-download-btn"
|
||||
onClick={() => downloadPdf(quote.id)}
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? '⏳ Genererer...' : '📄 Download PDF'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
@@ -127,9 +127,10 @@ const CompletedQuotes = ({ apiBaseUrl }) => {
|
||||
return icons[roofType?.toLowerCase()] || icons.default;
|
||||
};
|
||||
|
||||
// Function to download PDF for quote
|
||||
// Function to download PDF for quote (on-demand generation)
|
||||
const downloadPDF = async (quote) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const response = await fetch(`${apiBaseUrl}/api/quotes/${quote.id}/pdf`);
|
||||
if (response.ok) {
|
||||
const blob = await response.blob();
|
||||
@@ -142,11 +143,13 @@ const CompletedQuotes = ({ apiBaseUrl }) => {
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
} else {
|
||||
throw new Error('PDF ikke tilgængelig');
|
||||
throw new Error('PDF kunne ikke genereres');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error downloading PDF:', error);
|
||||
alert('Fejl ved download af PDF: ' + error.message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
+125
-171
@@ -4968,16 +4968,19 @@ app.get('/api/customer-projects/:projectId/quote/html', async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// Get PDF quote from database
|
||||
// Generate and download PDF on-demand (no storage)
|
||||
app.get('/api/quotes/:quoteId/pdf', async (req, res) => {
|
||||
try {
|
||||
const { quoteId } = req.params;
|
||||
|
||||
// Get quote with PDF data
|
||||
const [quote] = await databaseService.query(
|
||||
'SELECT pdf_data, quote_format FROM generated_quotes WHERE id = ?',
|
||||
[quoteId]
|
||||
);
|
||||
// Get quote and related data
|
||||
const [quote] = await databaseService.query(`
|
||||
SELECT gq.*, cp.*, pc.*
|
||||
FROM generated_quotes gq
|
||||
LEFT JOIN customer_projects cp ON gq.project_id = cp.id
|
||||
LEFT JOIN project_calculations pc ON gq.calculation_id = pc.id
|
||||
WHERE gq.id = ?
|
||||
`, [quoteId]);
|
||||
|
||||
if (!quote) {
|
||||
return res.status(404).json({
|
||||
@@ -4986,25 +4989,131 @@ app.get('/api/quotes/:quoteId/pdf', async (req, res) => {
|
||||
});
|
||||
}
|
||||
|
||||
if (!quote.pdf_data) {
|
||||
// Get all required data
|
||||
const projectId = quote.project_id;
|
||||
|
||||
const [project] = await databaseService.query(
|
||||
'SELECT * FROM customer_projects WHERE id = ?',
|
||||
[projectId]
|
||||
);
|
||||
|
||||
const [geometryRows] = await databaseService.query(
|
||||
'SELECT * FROM roof_geometry WHERE project_id = ?',
|
||||
[projectId]
|
||||
);
|
||||
|
||||
const [laborRows] = await databaseService.query(
|
||||
'SELECT * FROM project_labor WHERE project_id = ?',
|
||||
[projectId]
|
||||
);
|
||||
|
||||
const materials = await databaseService.query(
|
||||
'SELECT * FROM project_materials WHERE project_id = ?',
|
||||
[projectId]
|
||||
);
|
||||
|
||||
if (!project) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: 'PDF ikke tilgængelig for dette tilbud'
|
||||
error: 'Projekt ikke fundet'
|
||||
});
|
||||
}
|
||||
|
||||
// Set headers for PDF download
|
||||
res.setHeader('Content-Type', 'application/pdf');
|
||||
res.setHeader('Content-Disposition', `attachment; filename="tilbud-${quoteId}.pdf"`);
|
||||
// Prepare data for HTML template
|
||||
const data = {
|
||||
project: project,
|
||||
geometry: geometryRows || {},
|
||||
labor: laborRows || [],
|
||||
materials: materials || [],
|
||||
totals: {
|
||||
laborTotal: parseFloat(quote.total_labor_cost || 0),
|
||||
materialTotal: parseFloat(quote.total_material_cost || 0),
|
||||
subtotal: parseFloat(quote.subtotal || 0),
|
||||
vat: parseFloat(quote.vat_amount || 0),
|
||||
total: parseFloat(quote.total_incl_vat || 0)
|
||||
}
|
||||
};
|
||||
|
||||
// Generate HTML using template service
|
||||
const htmlContent = await quoteTemplateService.generateHtmlQuote(data);
|
||||
|
||||
// Send PDF data
|
||||
res.send(quote.pdf_data);
|
||||
// Convert HTML to PDF on-demand using wkhtmltopdf
|
||||
const { spawn } = require('child_process');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const tempHtmlPath = `/tmp/quote_${quoteId}_${Date.now()}.html`;
|
||||
const tempPdfPath = `/tmp/quote_${quoteId}_${Date.now()}.pdf`;
|
||||
|
||||
// Write HTML to temp file
|
||||
fs.writeFileSync(tempHtmlPath, htmlContent);
|
||||
|
||||
// Convert to PDF
|
||||
const wkhtmltopdf = spawn('wkhtmltopdf', [
|
||||
'--page-size', 'A4',
|
||||
'--margin-top', '20mm',
|
||||
'--margin-right', '20mm',
|
||||
'--margin-bottom', '20mm',
|
||||
'--margin-left', '20mm',
|
||||
'--encoding', 'UTF-8',
|
||||
'--enable-local-file-access',
|
||||
'--quiet', // Suppress output
|
||||
tempHtmlPath,
|
||||
tempPdfPath
|
||||
]);
|
||||
|
||||
wkhtmltopdf.on('close', (code) => {
|
||||
try {
|
||||
// Clean up temp HTML file
|
||||
if (fs.existsSync(tempHtmlPath)) {
|
||||
fs.unlinkSync(tempHtmlPath);
|
||||
}
|
||||
|
||||
if (code === 0 && fs.existsSync(tempPdfPath)) {
|
||||
// Stream PDF directly to response (no database storage)
|
||||
res.setHeader('Content-Type', 'application/pdf');
|
||||
res.setHeader('Content-Disposition', `attachment; filename="tilbud-${quoteId}.pdf"`);
|
||||
|
||||
const pdfStream = fs.createReadStream(tempPdfPath);
|
||||
pdfStream.pipe(res);
|
||||
|
||||
// Clean up temp PDF file after streaming
|
||||
pdfStream.on('end', () => {
|
||||
if (fs.existsSync(tempPdfPath)) {
|
||||
fs.unlinkSync(tempPdfPath);
|
||||
}
|
||||
});
|
||||
|
||||
console.log(`✅ PDF generated and streamed for quote ${quoteId}`);
|
||||
} else {
|
||||
console.error(`❌ PDF generation failed for quote ${quoteId}, exit code: ${code}`);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'PDF generering fejlede'
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error in PDF streaming:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Fejl ved PDF streaming'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
wkhtmltopdf.on('error', (error) => {
|
||||
console.error('wkhtmltopdf process error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'PDF generator fejl'
|
||||
});
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error retrieving PDF:', error);
|
||||
console.error('Error generating PDF on demand:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Fejl ved hentning af PDF'
|
||||
error: 'Fejl ved PDF generering: ' + error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -5091,162 +5200,7 @@ class PythonPDFService {
|
||||
|
||||
const pythonPDFService = new PythonPDFService();
|
||||
|
||||
// Generate PDF for existing quote using HTML template
|
||||
app.post('/api/quotes/:quoteId/generate-pdf', async (req, res) => {
|
||||
try {
|
||||
const { quoteId } = req.params;
|
||||
|
||||
// Get quote and related data
|
||||
const [quote] = await databaseService.query(`
|
||||
SELECT gq.*, cp.*, pc.*
|
||||
FROM generated_quotes gq
|
||||
LEFT JOIN customer_projects cp ON gq.project_id = cp.id
|
||||
LEFT JOIN project_calculations pc ON gq.calculation_id = pc.id
|
||||
WHERE gq.id = ?
|
||||
`, [quoteId]);
|
||||
|
||||
if (!quote) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: 'Tilbud ikke fundet'
|
||||
});
|
||||
}
|
||||
|
||||
// Use the same data preparation as HTML quote
|
||||
const projectId = quote.project_id;
|
||||
|
||||
// Get project data
|
||||
const [project] = await databaseService.query(
|
||||
'SELECT * FROM customer_projects WHERE id = ?',
|
||||
[projectId]
|
||||
);
|
||||
|
||||
// Get geometry data
|
||||
const [geometryRows] = await databaseService.query(
|
||||
'SELECT * FROM roof_geometry WHERE project_id = ?',
|
||||
[projectId]
|
||||
);
|
||||
|
||||
// Get labor data
|
||||
const [laborRows] = await databaseService.query(
|
||||
'SELECT * FROM project_labor WHERE project_id = ?',
|
||||
[projectId]
|
||||
);
|
||||
|
||||
// Get materials data
|
||||
const materials = await databaseService.query(
|
||||
'SELECT * FROM project_materials WHERE project_id = ?',
|
||||
[projectId]
|
||||
);
|
||||
|
||||
if (!project) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: 'Projekt ikke fundet'
|
||||
});
|
||||
}
|
||||
|
||||
// Prepare data exactly like HTML quote endpoint
|
||||
const data = {
|
||||
project: project,
|
||||
geometry: geometryRows || {},
|
||||
labor: laborRows || [],
|
||||
materials: materials || [],
|
||||
totals: {
|
||||
laborTotal: parseFloat(quote.total_labor_cost || 0),
|
||||
materialTotal: parseFloat(quote.total_material_cost || 0),
|
||||
subtotal: parseFloat(quote.subtotal || 0),
|
||||
vat: parseFloat(quote.vat_amount || 0),
|
||||
total: parseFloat(quote.total_incl_vat || 0)
|
||||
}
|
||||
};
|
||||
|
||||
// Generate HTML using the same template service
|
||||
const htmlContent = await quoteTemplateService.generateHtmlQuote(data);
|
||||
|
||||
// Convert HTML to PDF using wkhtmltopdf (more reliable than Puppeteer)
|
||||
const { spawn } = require('child_process');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const tempHtmlPath = `/tmp/quote_${quoteId}_${Date.now()}.html`;
|
||||
const tempPdfPath = `/tmp/quote_${quoteId}_${Date.now()}.pdf`;
|
||||
|
||||
// Write HTML to temp file
|
||||
fs.writeFileSync(tempHtmlPath, htmlContent);
|
||||
|
||||
// Use wkhtmltopdf to convert HTML to PDF
|
||||
const wkhtmltopdf = spawn('wkhtmltopdf', [
|
||||
'--page-size', 'A4',
|
||||
'--margin-top', '20mm',
|
||||
'--margin-right', '20mm',
|
||||
'--margin-bottom', '20mm',
|
||||
'--margin-left', '20mm',
|
||||
'--encoding', 'UTF-8',
|
||||
'--enable-local-file-access',
|
||||
tempHtmlPath,
|
||||
tempPdfPath
|
||||
]);
|
||||
|
||||
let wkOutput = '';
|
||||
let wkError = '';
|
||||
|
||||
wkhtmltopdf.stdout.on('data', (data) => {
|
||||
wkOutput += data.toString();
|
||||
});
|
||||
|
||||
wkhtmltopdf.stderr.on('data', (data) => {
|
||||
wkError += data.toString();
|
||||
});
|
||||
|
||||
wkhtmltopdf.on('close', async (code) => {
|
||||
try {
|
||||
// Clean up temp HTML file
|
||||
if (fs.existsSync(tempHtmlPath)) {
|
||||
fs.unlinkSync(tempHtmlPath);
|
||||
}
|
||||
|
||||
if (code === 0 && fs.existsSync(tempPdfPath)) {
|
||||
// Read PDF file and store in database
|
||||
const pdfBuffer = fs.readFileSync(tempPdfPath);
|
||||
|
||||
// Update database with PDF
|
||||
await databaseService.query(
|
||||
'UPDATE generated_quotes SET pdf_data = ?, quote_format = ? WHERE id = ?',
|
||||
[pdfBuffer, 'pdf', quoteId]
|
||||
);
|
||||
|
||||
// Clean up temp PDF file
|
||||
fs.unlinkSync(tempPdfPath);
|
||||
|
||||
console.log(`✅ PDF generated successfully for quote ${quoteId}`);
|
||||
console.log(`📊 wkhtmltopdf output: ${wkOutput}`);
|
||||
} else {
|
||||
console.error(`❌ PDF generation failed for quote ${quoteId}, exit code: ${code}`);
|
||||
console.error(`📊 wkhtmltopdf output: ${wkOutput}`);
|
||||
console.error(`📊 wkhtmltopdf error: ${wkError}`);
|
||||
console.error(`📊 Temp PDF exists: ${fs.existsSync(tempPdfPath)}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error in PDF generation cleanup:', error);
|
||||
}
|
||||
});
|
||||
|
||||
// Send immediate success response
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'PDF genereret succesfuldt med HTML template!',
|
||||
quoteId: quoteId
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error generating PDF:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Fejl ved PDF generering: ' + error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
// PDF generation is now on-demand only - no separate generation endpoint needed
|
||||
|
||||
// Check PDF generation status for a quote
|
||||
app.get('/api/quotes/:quoteId/pdf-status', async (req, res) => {
|
||||
|
||||
Reference in New Issue
Block a user