terter
This commit is contained in:
parent
776ea21ad9
commit
bbd1d9e1f2
|
|
@ -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');
|
||||||
|
|
|
||||||
48
server.js
48
server.js
|
|
@ -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>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue