diff --git a/public/app.js b/public/app.js index 364ce5d..212b788 100644 --- a/public/app.js +++ b/public/app.js @@ -195,7 +195,9 @@ async function openQuoteModal(quoteId = null) { document.getElementById('quote-id').value = quote.id; document.getElementById('quote-customer').value = quote.customer_id; 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-tbd-note').value = quote.tbd_note || ''; @@ -280,7 +282,29 @@ function addQuoteItem(item = null) { 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 => { input.addEventListener('input', updateTotals); }); @@ -294,6 +318,7 @@ function addQuoteItem(item = null) { } else { if (amountInput.value === 'TBD') { amountInput.value = ''; + calculateAmount(); // Recalculate when unchecking TBD } amountInput.readOnly = false; amountInput.classList.remove('bg-gray-100'); @@ -461,4 +486,4 @@ function setDefaultDate() { function formatDate(dateString) { const date = new Date(dateString); return `${date.getMonth() + 1}/${date.getDate()}/${date.getFullYear()}`; -} +} \ No newline at end of file diff --git a/server.js b/server.js index 48ef0e8..fb56664 100644 --- a/server.js +++ b/server.js @@ -371,6 +371,7 @@ app.post('/api/upload-logo', upload.single('logo'), (req, res) => { // Generate PDF app.post('/api/quotes/:id/pdf', async (req, res) => { + let browser; try { const quoteResult = await pool.query( `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 const html = generateQuoteHTML(quote); + console.log('Starting PDF generation for quote', quote.quote_number); + // Generate PDF with Puppeteer - const browser = await puppeteer.launch({ + browser = await puppeteer.launch({ executablePath: process.env.PUPPETEER_EXECUTABLE_PATH || '/usr/bin/chromium-browser', headless: true, args: [ '--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage', - '--disable-gpu' + '--disable-gpu', + '--disable-software-rasterizer', + '--disable-extensions' ] }); + + console.log('Browser launched, creating page...'); 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({ format: 'Letter', printBackground: true, + preferCSSPageSize: false, margin: { - top: '0', - right: '0', - bottom: '0', - left: '0' + top: '0.4in', + right: '0.4in', + bottom: '0.4in', + left: '0.4in' } }); 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); } catch (err) { - console.error(err); - res.status(500).json({ error: 'Error generating PDF' }); + console.error('PDF Generation Error:', err); + 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) {
- +
BA

Bay Area Affiliates, Inc.

1001 Blucher Street
@@ -737,4 +763,4 @@ app.listen(PORT, () => { process.on('SIGTERM', async () => { await pool.end(); process.exit(0); -}); +}); \ No newline at end of file