feat: Implement on-demand PDF generation and remove pre-generation logic

This commit is contained in:
2025-09-21 01:08:28 +02:00
parent d089693bd9
commit 967eea4121
3 changed files with 142 additions and 212 deletions
+12 -39
View File
@@ -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>
))}
+5 -2
View File
@@ -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
View File
@@ -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) => {