From 967eea4121fc660ded05cde88313aba7420da477 Mon Sep 17 00:00:00 2001 From: Alex Date: Sun, 21 Sep 2025 01:08:28 +0200 Subject: [PATCH] feat: Implement on-demand PDF generation and remove pre-generation logic --- frontend/src/components/CalculationView.js | 51 +--- frontend/src/components/CompletedQuotes.js | 7 +- unified-server.js | 296 +++++++++------------ 3 files changed, 142 insertions(+), 212 deletions(-) diff --git a/frontend/src/components/CalculationView.js b/frontend/src/components/CalculationView.js index 3aef016..8581403 100644 --- a/frontend/src/components/CalculationView.js +++ b/frontend/src/components/CalculationView.js @@ -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 - {quote.quote_format === 'pdf' && quote.pdf_data ? ( - - ) : ( - - )} + ))} diff --git a/frontend/src/components/CompletedQuotes.js b/frontend/src/components/CompletedQuotes.js index 51136b0..57ea9b1 100644 --- a/frontend/src/components/CompletedQuotes.js +++ b/frontend/src/components/CompletedQuotes.js @@ -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); } }; diff --git a/unified-server.js b/unified-server.js index 32f4e2c..1d5c050 100644 --- a/unified-server.js +++ b/unified-server.js @@ -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) => {