customer report

This commit is contained in:
2026-06-10 15:51:43 -05:00
parent e4ffe085ff
commit fb12d66759
4 changed files with 264 additions and 1 deletions

View File

@@ -5,7 +5,12 @@
*/
const express = require('express');
const router = express.Router();
const path = require('path');
const fs = require('fs').promises;
const accountingService = require('../services/accounting-service');
const { pool } = require('../config/database');
const { generatePdfFromHtml, getLogoHtml } = require('../services/pdf-service');
const { formatDate, formatMoney } = require('../utils/helpers');
const multer = require('multer');
// Limit aus ENV, default 5 MB. Akzeptierte Werte z.B. "5", "10", "20"
@@ -124,6 +129,113 @@ router.post('/sales-tax/periods/:id/mark-external', async (req, res) => {
} catch (err) { handleQboError(err, res, 'sales-tax-mark-external'); }
});
// ─── Customer Revenue Report ──────────────────────────────────────
router.get('/reports/customer-revenue', async (req, res) => {
try {
const { startDate, endDate } = req.query;
if (!startDate || !endDate) {
return res.status(400).json({ error: 'startDate and endDate are required' });
}
const result = await pool.query(
`SELECT
c.id,
c.name AS customer_name,
COUNT(i.id)::integer AS invoice_count,
COALESCE(SUM(i.subtotal), 0)::numeric(10,2) AS total_revenue,
(SELECT COALESCE(SUM(i2.subtotal), 0)::numeric(10,2)
FROM invoices i2
WHERE i2.invoice_date >= $1 AND i2.invoice_date <= $2
) AS grand_total
FROM customers c
JOIN invoices i ON i.customer_id = c.id
WHERE i.invoice_date >= $1 AND i.invoice_date <= $2
GROUP BY c.id, c.name
HAVING COUNT(i.id) > 0
ORDER BY total_revenue DESC`,
[startDate, endDate]
);
res.json(result.rows);
} catch (err) {
console.error('customer-revenue error:', err.message);
res.status(500).json({ error: err.message });
}
});
router.get('/reports/customer-revenue/pdf', async (req, res) => {
try {
const { startDate, endDate } = req.query;
if (!startDate || !endDate) {
return res.status(400).json({ error: 'startDate and endDate are required' });
}
const result = await pool.query(
`SELECT
c.name AS customer_name,
COUNT(i.id)::integer AS invoice_count,
COALESCE(SUM(i.subtotal), 0)::numeric(10,2) AS total_revenue,
(SELECT COALESCE(SUM(i2.subtotal), 0)::numeric(10,2)
FROM invoices i2
WHERE i2.invoice_date >= $1 AND i2.invoice_date <= $2
) AS grand_total
FROM customers c
JOIN invoices i ON i.customer_id = c.id
WHERE i.invoice_date >= $1 AND i.invoice_date <= $2
GROUP BY c.id, c.name
HAVING COUNT(i.id) > 0
ORDER BY total_revenue DESC`,
[startDate, endDate]
);
const rows = result.rows;
const grandTotal = rows.length > 0 ? parseFloat(rows[0].grand_total) || 0 : 0;
const totalInvoices = rows.reduce((s, r) => s + parseInt(r.invoice_count), 0);
let rowsHtml = '';
let rank = 0;
for (const r of rows) {
rank++;
const rev = parseFloat(r.total_revenue) || 0;
const pct = grandTotal > 0 ? ((rev / grandTotal) * 100).toFixed(1) : '0.0';
rowsHtml += `<tr>
<td class="name">${rank}. ${r.customer_name}</td>
<td class="number">${r.invoice_count}</td>
<td class="number">$${formatMoney(rev)}</td>
<td class="number">${pct}%</td>
</tr>`;
}
rowsHtml += `<tr class="footer-row">
<td class="total-label">TOTAL (${rows.length} customers)</td>
<td class="total-amount">${totalInvoices}</td>
<td class="total-amount">$${formatMoney(grandTotal)}</td>
<td class="total-amount">100.0%</td>
</tr>`;
const templatePath = path.join(__dirname, '..', '..', 'templates', 'customer-revenue-template.html');
let html = await fs.readFile(templatePath, 'utf-8');
const logoHTML = await getLogoHtml();
const dateRange = `${startDate} to ${endDate}`;
const generated = new Date().toISOString().split('T')[0];
html = html
.replace('{{LOGO_HTML}}', logoHTML)
.replace('{{DATE_RANGE}}', dateRange)
.replace('{{GENERATED_DATE}}', generated)
.replace('{{ROWS}}', rowsHtml);
const pdf = await generatePdfFromHtml(html);
res.set({
'Content-Type': 'application/pdf',
'Content-Length': pdf.length,
'Content-Disposition': `attachment; filename="Customer-Revenue-${startDate}-to-${endDate}.pdf"`
});
res.end(pdf, 'binary');
} catch (err) {
console.error('customer-revenue pdf error:', err.message);
res.status(500).json({ error: err.message });
}
});
// ════════════════════════════════════════════════════════════════════
// Phase 2 Lieferung 1 — Sync + Cache-Reads
// ════════════════════════════════════════════════════════════════════