last state

This commit is contained in:
2026-02-02 16:15:53 -06:00
parent 5e46fa06f1
commit eef316402c
5 changed files with 592 additions and 93 deletions

234
server.js
View File

@@ -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();