This commit is contained in:
Andreas Knuth 2026-01-21 20:01:00 -06:00
parent 776ea21ad9
commit bbd1d9e1f2
2 changed files with 66 additions and 15 deletions

View File

@ -195,7 +195,9 @@ async function openQuoteModal(quoteId = null) {
document.getElementById('quote-id').value = quote.id; document.getElementById('quote-id').value = quote.id;
document.getElementById('quote-customer').value = quote.customer_id; document.getElementById('quote-customer').value = quote.customer_id;
document.getElementById('quote-number').value = quote.quote_number; document.getElementById('quote-number').value = quote.quote_number;
document.getElementById('quote-date').value = quote.quote_date; // Convert date from YYYY-MM-DD format (may include time)
const dateOnly = quote.quote_date.split('T')[0];
document.getElementById('quote-date').value = dateOnly;
document.getElementById('quote-tax-exempt').checked = quote.tax_exempt; document.getElementById('quote-tax-exempt').checked = quote.tax_exempt;
document.getElementById('quote-tbd-note').value = quote.tbd_note || ''; document.getElementById('quote-tbd-note').value = quote.tbd_note || '';
@ -280,7 +282,29 @@ function addQuoteItem(item = null) {
itemsDiv.appendChild(itemDiv); itemsDiv.appendChild(itemDiv);
// Add event listeners // Get references to inputs for auto-calculation
const qtyInput = itemDiv.querySelector('[data-field="quantity"]');
const rateInput = itemDiv.querySelector('[data-field="rate"]');
const amountInput = itemDiv.querySelector('[data-field="amount"]');
const tbdCheckbox = itemDiv.querySelector('[data-field="is_tbd"]');
// Auto-calculate amount when qty or rate changes
const calculateAmount = () => {
if (!tbdCheckbox.checked && qtyInput.value && rateInput.value) {
const qty = parseFloat(qtyInput.value) || 0;
// Extract numeric value from rate (handles "125.00/hr" format)
const rateValue = parseFloat(rateInput.value.replace(/[^0-9.]/g, '')) || 0;
const amount = qty * rateValue;
amountInput.value = amount.toFixed(2);
}
updateTotals();
};
// Add event listeners for auto-calculation
qtyInput.addEventListener('input', calculateAmount);
rateInput.addEventListener('input', calculateAmount);
// Add event listeners for totals update
itemDiv.querySelectorAll('.item-input, .item-amount').forEach(input => { itemDiv.querySelectorAll('.item-input, .item-amount').forEach(input => {
input.addEventListener('input', updateTotals); input.addEventListener('input', updateTotals);
}); });
@ -294,6 +318,7 @@ function addQuoteItem(item = null) {
} else { } else {
if (amountInput.value === 'TBD') { if (amountInput.value === 'TBD') {
amountInput.value = ''; amountInput.value = '';
calculateAmount(); // Recalculate when unchecking TBD
} }
amountInput.readOnly = false; amountInput.readOnly = false;
amountInput.classList.remove('bg-gray-100'); amountInput.classList.remove('bg-gray-100');
@ -461,4 +486,4 @@ function setDefaultDate() {
function formatDate(dateString) { function formatDate(dateString) {
const date = new Date(dateString); const date = new Date(dateString);
return `${date.getMonth() + 1}/${date.getDate()}/${date.getFullYear()}`; return `${date.getMonth() + 1}/${date.getDate()}/${date.getFullYear()}`;
} }

View File

@ -371,6 +371,7 @@ app.post('/api/upload-logo', upload.single('logo'), (req, res) => {
// Generate PDF // Generate PDF
app.post('/api/quotes/:id/pdf', async (req, res) => { app.post('/api/quotes/:id/pdf', async (req, res) => {
let browser;
try { try {
const quoteResult = await pool.query( const quoteResult = await pool.query(
`SELECT q.*, c.name as customer_name, c.street, c.city, c.state, c.zip_code, c.account_number `SELECT q.*, c.name as customer_name, c.street, c.city, c.state, c.zip_code, c.account_number
@ -397,38 +398,63 @@ app.post('/api/quotes/:id/pdf', async (req, res) => {
// Generate HTML for PDF // Generate HTML for PDF
const html = generateQuoteHTML(quote); const html = generateQuoteHTML(quote);
console.log('Starting PDF generation for quote', quote.quote_number);
// Generate PDF with Puppeteer // Generate PDF with Puppeteer
const browser = await puppeteer.launch({ browser = await puppeteer.launch({
executablePath: process.env.PUPPETEER_EXECUTABLE_PATH || '/usr/bin/chromium-browser', executablePath: process.env.PUPPETEER_EXECUTABLE_PATH || '/usr/bin/chromium-browser',
headless: true, headless: true,
args: [ args: [
'--no-sandbox', '--no-sandbox',
'--disable-setuid-sandbox', '--disable-setuid-sandbox',
'--disable-dev-shm-usage', '--disable-dev-shm-usage',
'--disable-gpu' '--disable-gpu',
'--disable-software-rasterizer',
'--disable-extensions'
] ]
}); });
console.log('Browser launched, creating page...');
const page = await browser.newPage(); const page = await browser.newPage();
await page.setContent(html, { waitUntil: 'networkidle0' });
await page.setContent(html, {
waitUntil: 'networkidle0',
timeout: 30000
});
console.log('Content set, generating PDF...');
const pdf = await page.pdf({ const pdf = await page.pdf({
format: 'Letter', format: 'Letter',
printBackground: true, printBackground: true,
preferCSSPageSize: false,
margin: { margin: {
top: '0', top: '0.4in',
right: '0', right: '0.4in',
bottom: '0', bottom: '0.4in',
left: '0' left: '0.4in'
} }
}); });
await browser.close(); await browser.close();
browser = null;
res.contentType('application/pdf'); console.log('PDF generated successfully, size:', pdf.length, 'bytes');
res.setHeader('Content-Type', 'application/pdf');
res.setHeader('Content-Length', pdf.length);
res.setHeader('Content-Disposition', `attachment; filename="Quote_${quote.quote_number}.pdf"`);
res.send(pdf); res.send(pdf);
} catch (err) { } catch (err) {
console.error(err); console.error('PDF Generation Error:', err);
res.status(500).json({ error: 'Error generating PDF' }); if (browser) {
try {
await browser.close();
} catch (closeErr) {
console.error('Error closing browser:', closeErr);
}
}
res.status(500).json({ error: 'Error generating PDF: ' + err.message });
} }
}); });
@ -660,7 +686,7 @@ function generateQuoteHTML(quote) {
<div class="container"> <div class="container">
<div class="header"> <div class="header">
<div class="company-info"> <div class="company-info">
<img class="logo-size" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg=="> <div style="width: 40px; height: 40px; background-color: #1e40af; border-radius: 4px; display: flex; align-items: center; justify-content: center; color: white; font-weight: bold; font-size: 18px;">BA</div>
<div class="company-details"> <div class="company-details">
<h1>Bay Area Affiliates, Inc.</h1> <h1>Bay Area Affiliates, Inc.</h1>
<p>1001 Blucher Street<br> <p>1001 Blucher Street<br>
@ -737,4 +763,4 @@ app.listen(PORT, () => {
process.on('SIGTERM', async () => { process.on('SIGTERM', async () => {
await pool.end(); await pool.end();
process.exit(0); process.exit(0);
}); });