const express = require('express'); const { Pool } = require('pg'); const path = require('path'); const puppeteer = require('puppeteer'); const fs = require('fs').promises; const multer = require('multer'); const app = express(); const PORT = process.env.PORT || 3000; // Database connection const pool = new Pool({ user: process.env.DB_USER || 'postgres', host: process.env.DB_HOST || 'localhost', database: process.env.DB_NAME || 'quotes_db', password: process.env.DB_PASSWORD || 'postgres', port: process.env.DB_PORT || 5432, }); // Middleware app.use(express.json()); app.use(express.static('public')); // Configure multer for logo upload const storage = multer.diskStorage({ destination: async (req, file, cb) => { const uploadDir = path.join(__dirname, 'public', 'uploads'); try { await fs.mkdir(uploadDir, { recursive: true }); } catch (err) { console.error('Error creating upload directory:', err); } cb(null, uploadDir); }, filename: (req, file, cb) => { cb(null, 'company-logo.png'); } }); const upload = multer({ storage: storage, limits: { fileSize: 5 * 1024 * 1024 }, // 5MB limit fileFilter: (req, file, cb) => { if (file.mimetype.startsWith('image/')) { cb(null, true); } else { cb(new Error('Only image files are allowed')); } } }); // Helper functions function formatDate(date) { const d = new Date(date); const month = String(d.getMonth() + 1).padStart(2, '0'); const day = String(d.getDate()).padStart(2, '0'); const year = d.getFullYear(); return `${month}/${day}/${year}`; } async function getNextQuoteNumber() { const year = new Date().getFullYear(); const result = await pool.query( 'SELECT quote_number FROM quotes WHERE quote_number LIKE $1 ORDER BY quote_number DESC LIMIT 1', [`${year}-%`] ); if (result.rows.length === 0) { return `${year}-001`; } const lastNumber = parseInt(result.rows[0].quote_number.split('-')[1]); const nextNumber = String(lastNumber + 1).padStart(3, '0'); return `${year}-${nextNumber}`; } async function getNextInvoiceNumber() { const year = new Date().getFullYear(); const result = await pool.query( 'SELECT invoice_number FROM invoices WHERE invoice_number LIKE $1 ORDER BY invoice_number DESC LIMIT 1', [`${year}-%`] ); if (result.rows.length === 0) { return `${year}-001`; } const lastNumber = parseInt(result.rows[0].invoice_number.split('-')[1]); const nextNumber = String(lastNumber + 1).padStart(3, '0'); return `${year}-${nextNumber}`; } // Logo endpoints app.get('/api/logo-info', async (req, res) => { try { const logoPath = path.join(__dirname, 'public', 'uploads', 'company-logo.png'); try { await fs.access(logoPath); res.json({ hasLogo: true, logoPath: '/uploads/company-logo.png' }); } catch { res.json({ hasLogo: false }); } } catch (error) { console.error('Error checking logo:', error); res.status(500).json({ error: 'Error checking logo' }); } }); app.post('/api/upload-logo', upload.single('logo'), async (req, res) => { try { if (!req.file) { return res.status(400).json({ error: 'No file uploaded' }); } res.json({ message: 'Logo uploaded successfully', path: '/uploads/company-logo.png' }); } catch (error) { console.error('Upload error:', error); res.status(500).json({ error: 'Error uploading logo' }); } }); // Customer endpoints app.get('/api/customers', async (req, res) => { try { const result = await pool.query('SELECT * FROM customers ORDER BY name'); res.json(result.rows); } catch (error) { console.error('Error fetching customers:', error); res.status(500).json({ error: 'Error fetching customers' }); } }); app.post('/api/customers', async (req, res) => { const { name, street, city, state, zip_code, account_number } = req.body; try { const result = await pool.query( 'INSERT INTO customers (name, street, city, state, zip_code, account_number) VALUES ($1, $2, $3, $4, $5, $6) RETURNING *', [name, street, city, state, zip_code, account_number] ); res.json(result.rows[0]); } catch (error) { console.error('Error creating customer:', error); res.status(500).json({ error: 'Error creating customer' }); } }); app.put('/api/customers/:id', async (req, res) => { const { id } = req.params; const { name, street, city, state, zip_code, account_number } = req.body; try { const result = await pool.query( 'UPDATE customers SET name = $1, street = $2, city = $3, state = $4, zip_code = $5, account_number = $6, updated_at = CURRENT_TIMESTAMP WHERE id = $7 RETURNING *', [name, street, city, state, zip_code, account_number, id] ); res.json(result.rows[0]); } catch (error) { console.error('Error updating customer:', error); res.status(500).json({ error: 'Error updating customer' }); } }); app.delete('/api/customers/:id', async (req, res) => { const { id } = req.params; try { await pool.query('DELETE FROM customers WHERE id = $1', [id]); res.json({ success: true }); } catch (error) { console.error('Error deleting customer:', error); res.status(500).json({ error: 'Error deleting customer' }); } }); // Quote endpoints app.get('/api/quotes', async (req, res) => { try { const result = await pool.query(` SELECT q.*, c.name as customer_name FROM quotes q LEFT JOIN customers c ON q.customer_id = c.id ORDER BY q.created_at DESC `); res.json(result.rows); } catch (error) { console.error('Error fetching quotes:', error); res.status(500).json({ error: 'Error fetching quotes' }); } }); app.get('/api/quotes/:id', async (req, res) => { const { id } = req.params; 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 FROM quotes q LEFT JOIN customers c ON q.customer_id = c.id WHERE q.id = $1 `, [id]); if (quoteResult.rows.length === 0) { return res.status(404).json({ error: 'Quote not found' }); } const itemsResult = await pool.query( 'SELECT * FROM quote_items WHERE quote_id = $1 ORDER BY item_order', [id] ); res.json({ quote: quoteResult.rows[0], items: itemsResult.rows }); } catch (error) { console.error('Error fetching quote:', error); res.status(500).json({ error: 'Error fetching quote' }); } }); app.post('/api/quotes', async (req, res) => { const { customer_id, quote_date, tax_exempt, items } = req.body; const client = await pool.connect(); try { await client.query('BEGIN'); const quote_number = await getNextQuoteNumber(); // Calculate totals let subtotal = 0; let has_tbd = false; for (const item of items) { if (item.rate.toUpperCase() === 'TBD' || item.amount.toUpperCase() === 'TBD') { has_tbd = true; } else { const amount = parseFloat(item.amount.replace(/[$,]/g, '')); if (!isNaN(amount)) { subtotal += amount; } } } const tax_rate = 8.25; const tax_amount = tax_exempt ? 0 : (subtotal * tax_rate / 100); const total = subtotal + tax_amount; // Insert quote const quoteResult = await client.query( `INSERT INTO quotes (quote_number, customer_id, quote_date, tax_exempt, tax_rate, subtotal, tax_amount, total, has_tbd) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) RETURNING *`, [quote_number, customer_id, quote_date, tax_exempt, tax_rate, subtotal, tax_amount, total, has_tbd] ); const quoteId = quoteResult.rows[0].id; // Insert items for (let i = 0; i < items.length; i++) { await client.query( 'INSERT INTO quote_items (quote_id, quantity, description, rate, amount, item_order) VALUES ($1, $2, $3, $4, $5, $6)', [quoteId, items[i].quantity, items[i].description, items[i].rate, items[i].amount, i] ); } await client.query('COMMIT'); res.json(quoteResult.rows[0]); } catch (error) { await client.query('ROLLBACK'); console.error('Error creating quote:', error); res.status(500).json({ error: 'Error creating quote' }); } finally { client.release(); } }); app.put('/api/quotes/:id', async (req, res) => { const { id } = req.params; const { customer_id, quote_date, tax_exempt, items } = req.body; const client = await pool.connect(); try { await client.query('BEGIN'); // Calculate totals let subtotal = 0; let has_tbd = false; for (const item of items) { if (item.rate.toUpperCase() === 'TBD' || item.amount.toUpperCase() === 'TBD') { has_tbd = true; } else { const amount = parseFloat(item.amount.replace(/[$,]/g, '')); if (!isNaN(amount)) { subtotal += amount; } } } const tax_rate = 8.25; const tax_amount = tax_exempt ? 0 : (subtotal * tax_rate / 100); const total = subtotal + tax_amount; // Update quote await client.query( `UPDATE quotes SET customer_id = $1, quote_date = $2, tax_exempt = $3, tax_rate = $4, subtotal = $5, tax_amount = $6, total = $7, has_tbd = $8, updated_at = CURRENT_TIMESTAMP WHERE id = $9`, [customer_id, quote_date, tax_exempt, tax_rate, subtotal, tax_amount, total, has_tbd, id] ); // Delete old items and insert new ones await client.query('DELETE FROM quote_items WHERE quote_id = $1', [id]); for (let i = 0; i < items.length; i++) { await client.query( 'INSERT INTO quote_items (quote_id, quantity, description, rate, amount, item_order) VALUES ($1, $2, $3, $4, $5, $6)', [id, items[i].quantity, items[i].description, items[i].rate, items[i].amount, i] ); } await client.query('COMMIT'); res.json({ success: true }); } catch (error) { await client.query('ROLLBACK'); console.error('Error updating quote:', error); res.status(500).json({ error: 'Error updating quote' }); } finally { client.release(); } }); app.delete('/api/quotes/:id', async (req, res) => { const { id } = req.params; const client = await pool.connect(); try { await client.query('BEGIN'); await client.query('DELETE FROM quote_items WHERE quote_id = $1', [id]); await client.query('DELETE FROM quotes WHERE id = $1', [id]); await client.query('COMMIT'); res.json({ success: true }); } catch (error) { await client.query('ROLLBACK'); console.error('Error deleting quote:', error); res.status(500).json({ error: 'Error deleting quote' }); } finally { client.release(); } }); // Invoice endpoints app.get('/api/invoices', async (req, res) => { try { const result = await pool.query(` SELECT i.*, c.name as customer_name FROM invoices i LEFT JOIN customers c ON i.customer_id = c.id ORDER BY i.created_at DESC `); res.json(result.rows); } catch (error) { console.error('Error fetching invoices:', error); res.status(500).json({ error: 'Error fetching invoices' }); } }); app.get('/api/invoices/:id', async (req, res) => { const { id } = req.params; try { const invoiceResult = await pool.query(` SELECT i.*, c.name as customer_name, c.street, c.city, c.state, c.zip_code, c.account_number FROM invoices i LEFT JOIN customers c ON i.customer_id = c.id WHERE i.id = $1 `, [id]); if (invoiceResult.rows.length === 0) { return res.status(404).json({ error: 'Invoice not found' }); } const itemsResult = await pool.query( 'SELECT * FROM invoice_items WHERE invoice_id = $1 ORDER BY item_order', [id] ); res.json({ invoice: invoiceResult.rows[0], items: itemsResult.rows }); } catch (error) { console.error('Error fetching invoice:', error); res.status(500).json({ error: 'Error fetching invoice' }); } }); app.post('/api/invoices', async (req, res) => { const { customer_id, invoice_date, terms, auth_code, tax_exempt, items, created_from_quote_id } = req.body; const client = await pool.connect(); try { await client.query('BEGIN'); const invoice_number = await getNextInvoiceNumber(); // Calculate totals - invoices should NOT have TBD items let subtotal = 0; for (const item of items) { const amount = parseFloat(item.amount.replace(/[$,]/g, '')); if (!isNaN(amount)) { subtotal += amount; } } const tax_rate = 8.25; const tax_amount = tax_exempt ? 0 : (subtotal * tax_rate / 100); const total = subtotal + tax_amount; // Insert invoice const invoiceResult = await client.query( `INSERT INTO invoices (invoice_number, customer_id, invoice_date, terms, auth_code, tax_exempt, tax_rate, subtotal, tax_amount, total, created_from_quote_id) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11) RETURNING *`, [invoice_number, customer_id, invoice_date, terms, auth_code, tax_exempt, tax_rate, subtotal, tax_amount, total, created_from_quote_id] ); const invoiceId = invoiceResult.rows[0].id; // Insert items for (let i = 0; i < items.length; i++) { await client.query( 'INSERT INTO invoice_items (invoice_id, quantity, description, rate, amount, item_order) VALUES ($1, $2, $3, $4, $5, $6)', [invoiceId, items[i].quantity, items[i].description, items[i].rate, items[i].amount, i] ); } await client.query('COMMIT'); res.json(invoiceResult.rows[0]); } catch (error) { await client.query('ROLLBACK'); console.error('Error creating invoice:', error); res.status(500).json({ error: 'Error creating invoice' }); } finally { client.release(); } }); app.post('/api/quotes/:id/convert-to-invoice', async (req, res) => { const { id } = req.params; const client = await pool.connect(); try { await client.query('BEGIN'); // Get quote details const quoteResult = await pool.query(` SELECT q.*, c.name as customer_name, c.street, c.city, c.state, c.zip_code, c.account_number FROM quotes q LEFT JOIN customers c ON q.customer_id = c.id WHERE q.id = $1 `, [id]); if (quoteResult.rows.length === 0) { await client.query('ROLLBACK'); return res.status(404).json({ error: 'Quote not found' }); } const quote = quoteResult.rows[0]; // Get quote items const itemsResult = await pool.query( 'SELECT * FROM quote_items WHERE quote_id = $1 ORDER BY item_order', [id] ); // Check for TBD items const hasTBD = itemsResult.rows.some(item => item.rate.toUpperCase() === 'TBD' || item.amount.toUpperCase() === 'TBD' ); if (hasTBD) { await client.query('ROLLBACK'); return res.status(400).json({ error: 'Cannot convert quote with TBD items to invoice. Please update all TBD items first.' }); } // Create invoice const invoice_number = await getNextInvoiceNumber(); const invoiceDate = new Date().toISOString().split('T')[0]; const invoiceResult = await client.query( `INSERT INTO invoices (invoice_number, customer_id, invoice_date, terms, auth_code, tax_exempt, tax_rate, subtotal, tax_amount, total, created_from_quote_id) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11) RETURNING *`, [invoice_number, quote.customer_id, invoiceDate, 'Net 30', '', quote.tax_exempt, quote.tax_rate, quote.subtotal, quote.tax_amount, quote.total, id] ); const invoiceId = invoiceResult.rows[0].id; // Copy items to invoice for (let i = 0; i < itemsResult.rows.length; i++) { const item = itemsResult.rows[i]; await client.query( 'INSERT INTO invoice_items (invoice_id, quantity, description, rate, amount, item_order) VALUES ($1, $2, $3, $4, $5, $6)', [invoiceId, item.quantity, item.description, item.rate, item.amount, i] ); } await client.query('COMMIT'); res.json(invoiceResult.rows[0]); } catch (error) { await client.query('ROLLBACK'); console.error('Error converting quote to invoice:', error); res.status(500).json({ error: 'Error converting quote to invoice' }); } finally { client.release(); } }); app.put('/api/invoices/:id', async (req, res) => { const { id } = req.params; const { customer_id, invoice_date, terms, auth_code, tax_exempt, items } = req.body; const client = await pool.connect(); try { await client.query('BEGIN'); // Calculate totals let subtotal = 0; for (const item of items) { const amount = parseFloat(item.amount.replace(/[$,]/g, '')); if (!isNaN(amount)) { subtotal += amount; } } const tax_rate = 8.25; const tax_amount = tax_exempt ? 0 : (subtotal * tax_rate / 100); const total = subtotal + tax_amount; // Update invoice await client.query( `UPDATE invoices SET customer_id = $1, invoice_date = $2, terms = $3, auth_code = $4, tax_exempt = $5, tax_rate = $6, subtotal = $7, tax_amount = $8, total = $9, updated_at = CURRENT_TIMESTAMP WHERE id = $10`, [customer_id, invoice_date, terms, auth_code, tax_exempt, tax_rate, subtotal, tax_amount, total, id] ); // Delete old items and insert new ones await client.query('DELETE FROM invoice_items WHERE invoice_id = $1', [id]); for (let i = 0; i < items.length; i++) { await client.query( 'INSERT INTO invoice_items (invoice_id, quantity, description, rate, amount, item_order) VALUES ($1, $2, $3, $4, $5, $6)', [id, items[i].quantity, items[i].description, items[i].rate, items[i].amount, i] ); } await client.query('COMMIT'); res.json({ success: true }); } catch (error) { await client.query('ROLLBACK'); console.error('Error updating invoice:', error); res.status(500).json({ error: 'Error updating invoice' }); } finally { client.release(); } }); app.delete('/api/invoices/:id', async (req, res) => { const { id } = req.params; const client = await pool.connect(); try { await client.query('BEGIN'); await client.query('DELETE FROM invoice_items WHERE invoice_id = $1', [id]); await client.query('DELETE FROM invoices WHERE id = $1', [id]); await client.query('COMMIT'); res.json({ success: true }); } catch (error) { await client.query('ROLLBACK'); console.error('Error deleting invoice:', error); res.status(500).json({ error: 'Error deleting invoice' }); } finally { client.release(); } }); // PDF Generation for Quotes app.get('/api/quotes/:id/pdf', async (req, res) => { const { id } = req.params; console.log(`[PDF] Starting quote PDF generation for ID: ${id}`); try { console.log('[PDF] Fetching quote from database...'); const quoteResult = await pool.query(` SELECT q.*, c.name as customer_name, c.street, c.city, c.state, c.zip_code, c.account_number FROM quotes q LEFT JOIN customers c ON q.customer_id = c.id WHERE q.id = $1 `, [id]); if (quoteResult.rows.length === 0) { console.log('[PDF] Quote not found'); return res.status(404).json({ error: 'Quote not found' }); } const quote = quoteResult.rows[0]; console.log(`[PDF] Quote loaded: ${quote.quote_number}`); console.log('[PDF] Fetching quote items...'); const itemsResult = await pool.query( 'SELECT * FROM quote_items WHERE quote_id = $1 ORDER BY item_order', [id] ); console.log(`[PDF] Items loaded: ${itemsResult.rows.length} items`); console.log('[PDF] Generating HTML...'); const html = await generateQuotePDFHTML(quote, itemsResult.rows); console.log(`[PDF] HTML generated, length: ${html.length} chars`); console.log('[PDF] Launching Puppeteer...'); const browser = await puppeteer.launch({ headless: 'new', args: [ '--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage', '--disable-gpu', '--disable-software-rasterizer', '--no-zygote', '--single-process' // Wichtig für Docker! ], protocolTimeout: 180000, // 3 Minuten timeout: 180000 }); console.log('[PDF] Browser launched successfully'); console.log('[PDF] Creating new page...'); const page = await browser.newPage(); console.log('[PDF] Page created'); console.log('[PDF] Setting content...'); await page.setContent(html, { waitUntil: 'networkidle0', timeout: 60000 }); console.log('[PDF] Content set'); console.log('[PDF] Generating PDF...'); const pdf = await page.pdf({ format: 'Letter', printBackground: true, margin: { top: '0.5in', right: '0.5in', bottom: '0.5in', left: '0.5in' }, timeout: 60000 }); console.log(`[PDF] PDF generated, size: ${pdf.length} bytes`); console.log('[PDF] Closing browser...'); await browser.close(); console.log('[PDF] Browser closed'); res.set({ 'Content-Type': 'application/pdf', 'Content-Length': pdf.length, 'Content-Disposition': `attachment; filename="Quote-${quote.quote_number}.pdf"` }); res.end(pdf, 'binary'); console.log('[PDF] PDF sent to client successfully'); } catch (error) { console.error('[PDF] ERROR:', error); console.error('[PDF] Stack:', error.stack); res.status(500).json({ error: 'Error generating PDF', details: error.message }); } }); // PDF Generation for Invoices app.get('/api/invoices/:id/pdf', async (req, res) => { const { id } = req.params; console.log(`[INVOICE-PDF] Starting invoice PDF generation for ID: ${id}`); try { console.log('[INVOICE-PDF] Fetching invoice from database...'); const invoiceResult = await pool.query(` SELECT i.*, c.name as customer_name, c.street, c.city, c.state, c.zip_code, c.account_number FROM invoices i LEFT JOIN customers c ON i.customer_id = c.id WHERE i.id = $1 `, [id]); if (invoiceResult.rows.length === 0) { console.log('[INVOICE-PDF] Invoice not found'); return res.status(404).json({ error: 'Invoice not found' }); } const invoice = invoiceResult.rows[0]; console.log(`[INVOICE-PDF] Invoice loaded: ${invoice.invoice_number}`); console.log('[INVOICE-PDF] Fetching invoice items...'); const itemsResult = await pool.query( 'SELECT * FROM invoice_items WHERE invoice_id = $1 ORDER BY item_order', [id] ); console.log(`[INVOICE-PDF] Items loaded: ${itemsResult.rows.length} items`); console.log('[INVOICE-PDF] Generating HTML...'); const html = await generateInvoicePDFHTML(invoice, itemsResult.rows); console.log(`[INVOICE-PDF] HTML generated, length: ${html.length} chars`); console.log('[INVOICE-PDF] Launching Puppeteer...'); const browser = await puppeteer.launch({ headless: 'new', args: [ '--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage', '--disable-gpu', '--disable-software-rasterizer', '--no-zygote', '--single-process' ], protocolTimeout: 180000, timeout: 180000 }); console.log('[INVOICE-PDF] Browser launched successfully'); console.log('[INVOICE-PDF] Creating new page...'); const page = await browser.newPage(); console.log('[INVOICE-PDF] Page created'); console.log('[INVOICE-PDF] Setting content...'); await page.setContent(html, { waitUntil: 'networkidle0', timeout: 60000 }); console.log('[INVOICE-PDF] Content set'); console.log('[INVOICE-PDF] Generating PDF...'); const pdf = await page.pdf({ format: 'Letter', printBackground: true, margin: { top: '0.5in', right: '0.5in', bottom: '0.5in', left: '0.5in' }, timeout: 60000 }); console.log(`[INVOICE-PDF] PDF generated, size: ${pdf.length} bytes`); console.log('[INVOICE-PDF] Closing browser...'); await browser.close(); console.log('[INVOICE-PDF] Browser closed'); res.set({ 'Content-Type': 'application/pdf', 'Content-Length': pdf.length, 'Content-Disposition': `attachment; filename="Invoice-${invoice.invoice_number}.pdf"` }); res.end(pdf, 'binary'); console.log('[INVOICE-PDF] PDF sent to client successfully'); } catch (error) { console.error('[INVOICE-PDF] ERROR:', error); console.error('[INVOICE-PDF] Stack:', error.stack); res.status(500).json({ error: 'Error generating PDF', details: error.message }); } }); async function generateQuotePDFHTML(quote, items) { // Check if logo exists const logoPath = path.join(__dirname, 'public', 'uploads', 'company-logo.png'); let logoHTML = ''; try { const logoData = await fs.readFile(logoPath); const logoBase64 = logoData.toString('base64'); logoHTML = ``; } catch (error) { // No logo, continue without it } // Generate items HTML let itemsHTML = items.map(item => ` ${item.quantity} ${item.description} ${item.rate} ${item.amount} `).join(''); // Add totals itemsHTML += ` Subtotal: $${parseFloat(quote.subtotal).toFixed(2)} `; if (!quote.tax_exempt) { itemsHTML += ` Tax (${quote.tax_rate}%): $${parseFloat(quote.tax_amount).toFixed(2)} `; } itemsHTML += ` TOTAL: $${parseFloat(quote.total).toFixed(2)} Thank you for your business! `; // TBD note if applicable let tbdNote = ''; if (quote.has_tbd) { tbdNote = '

* Note: This quote contains items marked as "TBD" (To Be Determined). The final total may vary once all details are finalized.

'; } return `
${logoHTML}

Bay Area Affiliates, Inc.

1001 Blucher Street
Corpus Christi, Texas 78401

Providing IT Services and Support in South Texas Since 1996
Phone:
(361) 765-8400
(361) 765-8401
(361) 232-6578
Email:
support@bayarea-cc.com
Quote For:
${quote.customer_name}
${quote.street}
${quote.city}, ${quote.state} ${quote.zip_code}
QUOTE # ACCOUNT NO. DATE
${quote.quote_number} ${quote.account_number || ''} ${formatDate(quote.quote_date)}
${itemsHTML}
QTY DESCRIPTION RATE AMOUNT
${tbdNote}
`; } async function generateInvoicePDFHTML(invoice, items) { // Check if logo exists const logoPath = path.join(__dirname, 'public', 'uploads', 'company-logo.png'); let logoHTML = ''; try { const logoData = await fs.readFile(logoPath); const logoBase64 = logoData.toString('base64'); logoHTML = ``; } catch (error) { // No logo, continue without it } // Generate items HTML let itemsHTML = items.map(item => ` ${item.quantity} ${item.description} ${item.rate} ${item.amount} `).join(''); // Add totals itemsHTML += ` Subtotal: $${parseFloat(invoice.subtotal).toFixed(2)} `; if (!invoice.tax_exempt) { itemsHTML += ` Tax (${invoice.tax_rate}%): $${parseFloat(invoice.tax_amount).toFixed(2)} `; } itemsHTML += ` TOTAL: $${parseFloat(invoice.total).toFixed(2)} Thank you for your business! `; return `
${logoHTML}

Bay Area Affiliates, Inc.

1001 Blucher Street
Corpus Christi, Texas 78401

Providing IT Services and Support in South Texas Since 1996
Phone:
(361) 765-8400
(361) 765-8401
(361) 232-6578
Email:
accounting@bayarea-cc.com
Bill To:
${invoice.customer_name}
${invoice.street}
${invoice.city}, ${invoice.state} ${invoice.zip_code}
INVOICE # ACCOUNT NO. DATE TERMS
${invoice.invoice_number} ${invoice.account_number || ''} ${formatDate(invoice.invoice_date)} ${invoice.terms}
${invoice.auth_code ? `

Authorization: ${invoice.auth_code}

` : ''} ${itemsHTML}
QTY DESCRIPTION RATE AMOUNT
`; } // Start server app.listen(PORT, () => { console.log(`Quote System running on port ${PORT}`); }); // Graceful shutdown process.on('SIGTERM', async () => { await pool.end(); process.exit(0); });