last state
This commit is contained in:
234
server.js
234
server.js
@@ -638,14 +638,23 @@ app.get('/api/quotes/:id/pdf', async (req, res) => {
|
||||
}
|
||||
|
||||
// Generate items HTML
|
||||
let itemsHTML = itemsResult.rows.map(item => `
|
||||
let itemsHTML = itemsResult.rows.map(item => {
|
||||
let rateFormatted = item.rate;
|
||||
if (item.rate.toUpperCase() !== 'TBD' && !item.rate.includes('/')) {
|
||||
const rateNum = parseFloat(item.rate.replace(/[^0-9.]/g, ''));
|
||||
if (!isNaN(rateNum)) {
|
||||
rateFormatted = rateNum.toFixed(2);
|
||||
}
|
||||
}
|
||||
return `
|
||||
<tr>
|
||||
<td class="qty">${item.quantity}</td>
|
||||
<td class="description">${item.description}</td>
|
||||
<td class="rate">${item.rate}</td>
|
||||
<td class="rate">${rateFormatted}</td>
|
||||
<td class="amount">${item.amount}</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
// Add totals
|
||||
itemsHTML += `
|
||||
@@ -758,14 +767,23 @@ app.get('/api/invoices/:id/pdf', async (req, res) => {
|
||||
}
|
||||
|
||||
// Generate items HTML
|
||||
let itemsHTML = itemsResult.rows.map(item => `
|
||||
let itemsHTML = itemsResult.rows.map(item => {
|
||||
let rateFormatted = item.rate;
|
||||
if (item.rate.toUpperCase() !== 'TBD' && !item.rate.includes('/')) {
|
||||
const rateNum = parseFloat(item.rate.replace(/[^0-9.]/g, ''));
|
||||
if (!isNaN(rateNum)) {
|
||||
rateFormatted = rateNum.toFixed(2);
|
||||
}
|
||||
}
|
||||
return `
|
||||
<tr>
|
||||
<td class="qty">${item.quantity}</td>
|
||||
<td class="description">${item.description}</td>
|
||||
<td class="rate">${item.rate}</td>
|
||||
<td class="rate">${rateFormatted}</td>
|
||||
<td class="amount">${item.amount}</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
// Add totals
|
||||
itemsHTML += `
|
||||
@@ -839,6 +857,210 @@ app.get('/api/invoices/:id/pdf', async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// Nach den PDF-Endpoints, vor "Start server", einfügen:
|
||||
|
||||
// HTML Debug Endpoints
|
||||
app.get('/api/quotes/:id/html', 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 quote = quoteResult.rows[0];
|
||||
const itemsResult = await pool.query(
|
||||
'SELECT * FROM quote_items WHERE quote_id = $1 ORDER BY item_order',
|
||||
[id]
|
||||
);
|
||||
|
||||
const templatePath = path.join(__dirname, 'templates', 'quote-template.html');
|
||||
let html = await fs.readFile(templatePath, 'utf-8');
|
||||
|
||||
let logoHTML = '';
|
||||
try {
|
||||
const logoPath = path.join(__dirname, 'public', 'uploads', 'company-logo.png');
|
||||
const logoData = await fs.readFile(logoPath);
|
||||
const logoBase64 = logoData.toString('base64');
|
||||
logoHTML = `<img src="data:image/png;base64,${logoBase64}" alt="Company Logo" class="logo logo-size">`;
|
||||
} catch (err) {}
|
||||
|
||||
let itemsHTML = itemsResult.rows.map(item => {
|
||||
let rateFormatted = item.rate;
|
||||
if (item.rate.toUpperCase() !== 'TBD' && !item.rate.includes('/')) {
|
||||
const rateNum = parseFloat(item.rate.replace(/[^0-9.]/g, ''));
|
||||
if (!isNaN(rateNum)) {
|
||||
rateFormatted = rateNum.toFixed(2);
|
||||
}
|
||||
}
|
||||
return `
|
||||
<tr>
|
||||
<td class="qty">${item.quantity}</td>
|
||||
<td class="description">${item.description}</td>
|
||||
<td class="rate">${rateFormatted}</td>
|
||||
<td class="amount">${item.amount}</td>
|
||||
</tr>
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
itemsHTML += `
|
||||
<tr class="footer-row">
|
||||
<td colspan="3" class="total-label">Subtotal:</td>
|
||||
<td class="total-amount">$${parseFloat(quote.subtotal).toFixed(2)}</td>
|
||||
</tr>`;
|
||||
|
||||
if (!quote.tax_exempt) {
|
||||
itemsHTML += `
|
||||
<tr class="footer-row">
|
||||
<td colspan="3" class="total-label">Tax (${quote.tax_rate}%):</td>
|
||||
<td class="total-amount">$${parseFloat(quote.tax_amount).toFixed(2)}</td>
|
||||
</tr>`;
|
||||
}
|
||||
|
||||
itemsHTML += `
|
||||
<tr class="footer-row">
|
||||
<td colspan="3" class="total-label">TOTAL:</td>
|
||||
<td class="total-amount">$${parseFloat(quote.total).toFixed(2)}</td>
|
||||
</tr>
|
||||
<tr class="footer-row">
|
||||
<td colspan="4" class="thank-you">Thank you for your business!</td>
|
||||
</tr>`;
|
||||
|
||||
let tbdNote = '';
|
||||
if (quote.has_tbd) {
|
||||
tbdNote = '<p style="font-size: 12px; margin-top: 20px;"><em>* Note: This quote contains items marked as "TBD" (To Be Determined). The final total may vary once all details are finalized.</em></p>';
|
||||
}
|
||||
|
||||
html = html
|
||||
.replace('{{LOGO_HTML}}', logoHTML)
|
||||
.replace('{{CUSTOMER_NAME}}', quote.customer_name || '')
|
||||
.replace('{{CUSTOMER_STREET}}', quote.street || '')
|
||||
.replace('{{CUSTOMER_CITY}}', quote.city || '')
|
||||
.replace('{{CUSTOMER_STATE}}', quote.state || '')
|
||||
.replace('{{CUSTOMER_ZIP}}', quote.zip_code || '')
|
||||
.replace('{{QUOTE_NUMBER}}', quote.quote_number)
|
||||
.replace('{{ACCOUNT_NUMBER}}', quote.account_number || '')
|
||||
.replace('{{QUOTE_DATE}}', formatDate(quote.quote_date))
|
||||
.replace('{{ITEMS}}', itemsHTML)
|
||||
.replace('{{TBD_NOTE}}', tbdNote);
|
||||
|
||||
res.setHeader('Content-Type', 'text/html');
|
||||
res.send(html);
|
||||
|
||||
} catch (error) {
|
||||
console.error('[HTML] ERROR:', error);
|
||||
res.status(500).json({ error: 'Error generating HTML' });
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/api/invoices/:id/html', 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 invoice = invoiceResult.rows[0];
|
||||
const itemsResult = await pool.query(
|
||||
'SELECT * FROM invoice_items WHERE invoice_id = $1 ORDER BY item_order',
|
||||
[id]
|
||||
);
|
||||
|
||||
const templatePath = path.join(__dirname, 'templates', 'invoice-template.html');
|
||||
let html = await fs.readFile(templatePath, 'utf-8');
|
||||
|
||||
let logoHTML = '';
|
||||
try {
|
||||
const logoPath = path.join(__dirname, 'public', 'uploads', 'company-logo.png');
|
||||
const logoData = await fs.readFile(logoPath);
|
||||
const logoBase64 = logoData.toString('base64');
|
||||
logoHTML = `<img src="data:image/png;base64,${logoBase64}" alt="Company Logo" class="logo logo-size">`;
|
||||
} catch (err) {}
|
||||
|
||||
let itemsHTML = itemsResult.rows.map(item => {
|
||||
let rateFormatted = item.rate;
|
||||
if (item.rate.toUpperCase() !== 'TBD' && !item.rate.includes('/')) {
|
||||
const rateNum = parseFloat(item.rate.replace(/[^0-9.]/g, ''));
|
||||
if (!isNaN(rateNum)) {
|
||||
rateFormatted = rateNum.toFixed(2);
|
||||
}
|
||||
}
|
||||
return `
|
||||
<tr>
|
||||
<td class="qty">${item.quantity}</td>
|
||||
<td class="description">${item.description}</td>
|
||||
<td class="rate">${rateFormatted}</td>
|
||||
<td class="amount">${item.amount}</td>
|
||||
</tr>
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
itemsHTML += `
|
||||
<tr class="footer-row">
|
||||
<td colspan="3" class="total-label">Subtotal:</td>
|
||||
<td class="total-amount">$${parseFloat(invoice.subtotal).toFixed(2)}</td>
|
||||
</tr>`;
|
||||
|
||||
if (!invoice.tax_exempt) {
|
||||
itemsHTML += `
|
||||
<tr class="footer-row">
|
||||
<td colspan="3" class="total-label">Tax (${invoice.tax_rate}%):</td>
|
||||
<td class="total-amount">$${parseFloat(invoice.tax_amount).toFixed(2)}</td>
|
||||
</tr>`;
|
||||
}
|
||||
|
||||
itemsHTML += `
|
||||
<tr class="footer-row">
|
||||
<td colspan="3" class="total-label">TOTAL:</td>
|
||||
<td class="total-amount">$${parseFloat(invoice.total).toFixed(2)}</td>
|
||||
</tr>
|
||||
<tr class="footer-row">
|
||||
<td colspan="4" class="thank-you">Thank you for your business!</td>
|
||||
</tr>`;
|
||||
|
||||
const authHTML = invoice.auth_code ?
|
||||
`<p style="margin-bottom: 20px; font-size: 13px;"><strong>Authorization:</strong> ${invoice.auth_code}</p>` : '';
|
||||
|
||||
html = html
|
||||
.replace('{{LOGO_HTML}}', logoHTML)
|
||||
.replace('{{CUSTOMER_NAME}}', invoice.customer_name || '')
|
||||
.replace('{{CUSTOMER_STREET}}', invoice.street || '')
|
||||
.replace('{{CUSTOMER_CITY}}', invoice.city || '')
|
||||
.replace('{{CUSTOMER_STATE}}', invoice.state || '')
|
||||
.replace('{{CUSTOMER_ZIP}}', invoice.zip_code || '')
|
||||
.replace('{{INVOICE_NUMBER}}', invoice.invoice_number)
|
||||
.replace('{{ACCOUNT_NUMBER}}', invoice.account_number || '')
|
||||
.replace('{{INVOICE_DATE}}', formatDate(invoice.invoice_date))
|
||||
.replace('{{TERMS}}', invoice.terms)
|
||||
.replace('{{AUTHORIZATION}}', authHTML)
|
||||
.replace('{{ITEMS}}', itemsHTML);
|
||||
|
||||
res.setHeader('Content-Type', 'text/html');
|
||||
res.send(html);
|
||||
|
||||
} catch (error) {
|
||||
console.error('[HTML] ERROR:', error);
|
||||
res.status(500).json({ error: 'Error generating HTML' });
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// Start server and browser
|
||||
async function startServer() {
|
||||
await initBrowser();
|
||||
|
||||
Reference in New Issue
Block a user