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) => {