Files
mek-tech-consulting/routes/invoices.js
T

106 lines
5.6 KiB
JavaScript
Raw Normal View History

const express = require('express');
const router = express.Router();
const { getDb, logAudit } = require('../db');
const emailService = require('../email');
router.get('/', (req, res) => {
const db = getDb();
const invoices = db.prepare(`
SELECT i.*, c.name as client_name
FROM invoices i JOIN clients c ON c.id = i.client_id
ORDER BY i.created_at DESC
`).all();
const totalOutstanding = invoices.filter(i => i.status === 'sent' || i.status === 'overdue').reduce((s, i) => s + i.total, 0);
const totalPaid = invoices.filter(i => i.status === 'paid').reduce((s, i) => s + i.total, 0);
res.render('invoice-list', { invoices, totalOutstanding, totalPaid });
});
router.get('/new', (req, res) => {
const db = getDb();
const clients = db.prepare('SELECT id, name FROM clients ORDER BY name').all();
res.render('invoice-form', { invoice: null, clients, selectedClient: null, timeEntries: [], error: null });
});
router.get('/new/:clientId', (req, res) => {
const db = getDb();
const clients = db.prepare('SELECT id, name FROM clients ORDER BY name').all();
const client = db.prepare('SELECT * FROM clients WHERE id = ?').get(req.params.clientId);
if (!client) return res.redirect('/invoices/new');
const timeEntries = db.prepare(`
SELECT t.*, e.title as engagement_title
FROM time_entries t
LEFT JOIN engagements e ON e.id = t.engagement_id
WHERE t.client_id = ? AND t.billable = 1
ORDER BY t.date DESC
`).all(req.params.clientId);
res.render('invoice-form', { invoice: null, clients, selectedClient: parseInt(req.params.clientId), timeEntries, error: null });
});
router.post('/new', (req, res) => {
const db = getDb();
const { client_id, date, due_date, notes, items } = req.body;
if (!client_id) return res.redirect('/invoices');
const count = db.prepare('SELECT COUNT(*) as cnt FROM invoices').get().cnt + 1;
const number = `FACT-${new Date().getFullYear()}-${String(count).padStart(4, '0')}`;
const subtotal = parseFloat(req.body.subtotal) || 0;
const tax = parseFloat(req.body.tax) || 0;
const total = subtotal + tax;
const result = db.prepare('INSERT INTO invoices (client_id, number, status, date, due_date, subtotal, tax, total, notes) VALUES (?,?,?,?,?,?,?,?,?)')
.run(client_id, number, 'draft', date || new Date().toISOString().split('T')[0], due_date || '', subtotal, tax, total, notes || '');
const invoiceId = result.lastInsertRowid;
if (items && Array.isArray(items)) {
const insert = db.prepare('INSERT INTO invoice_items (invoice_id, description, quantity, unit_price, total) VALUES (?,?,?,?,?)');
for (const item of items) {
if (item.description) {
const itemTotal = (parseFloat(item.quantity) || 1) * (parseFloat(item.unit_price) || 0);
insert.run(invoiceId, item.description, parseFloat(item.quantity) || 1, parseFloat(item.unit_price) || 0, itemTotal);
}
}
}
logAudit(req.session.userId, req.session.username, 'invoice_created', 'invoice', invoiceId, `Factuur ${number} aangemaakt`);
res.redirect('/invoices/' + invoiceId);
});
router.get('/:id', (req, res) => {
const db = getDb();
const invoice = db.prepare('SELECT i.*, c.name as client_name, c.contact_name, c.contact_email, c.contact_phone, c.website FROM invoices i JOIN clients c ON c.id = i.client_id WHERE i.id = ?').get(req.params.id);
if (!invoice) return res.redirect('/invoices');
const items = db.prepare('SELECT * FROM invoice_items WHERE invoice_id = ?').all(req.params.id);
res.render('invoice-detail', { invoice, items, sent: req.query.sent === '1', error: req.query.error || null });
});
router.post('/:id/status', (req, res) => {
const db = getDb();
db.prepare("UPDATE invoices SET status=?, updated_at=datetime('now') WHERE id=?").run(req.body.status, req.params.id);
res.redirect('/invoices/' + req.params.id);
});
router.post('/:id/send-email', async (req, res) => {
const db = getDb();
const invoice = db.prepare('SELECT i.*, c.name as client_name, c.contact_email FROM invoices i JOIN clients c ON c.id = i.client_id WHERE i.id = ?').get(req.params.id);
if (!invoice) return res.redirect('/invoices');
const account = db.prepare('SELECT * FROM email_accounts WHERE user_id = ? AND active = 1 LIMIT 1').get(req.session.userId);
if (!account) return res.redirect('/invoices/' + req.params.id + '?error=noemail');
const items = db.prepare('SELECT * FROM invoice_items WHERE invoice_id = ?').all(req.params.id);
let text = `Factuur ${invoice.number}\n\nCliënt: ${invoice.client_name}\nDatum: ${invoice.date}\nVervaldatum: ${invoice.due_date || '-'}\n\n`;
items.forEach(item => { text += `${item.description} x${item.quantity}${(item.unit_price||0).toFixed(2)}${(item.total||0).toFixed(2)}\n`; });
text += `\nSubtotaal: €${(invoice.subtotal||0).toFixed(2)}\nBTW: €${(invoice.tax||0).toFixed(2)}\nTotaal: €${(invoice.total||0).toFixed(2)}\n\nMek-Tech Consulting`;
const to = invoice.contact_email || (await emailService.getAccount(account.id)).email;
try {
await emailService.sendEmail(account.id, to, `Factuur ${invoice.number}`, text, text.replace(/\n/g, '<br>'));
logAudit(req.session.userId, req.session.username, 'invoice_emailed', 'invoice', invoice.id, `Factuur ${invoice.number} gemaild naar ${to}`);
db.prepare("UPDATE invoices SET status='sent', updated_at=datetime('now') WHERE id=?").run(req.params.id);
res.redirect('/invoices/' + req.params.id + '?sent=1');
} catch (e) {
res.redirect('/invoices/' + req.params.id + '?error=' + encodeURIComponent(e.message));
}
});
router.post('/:id/delete', (req, res) => {
const db = getDb();
db.prepare('DELETE FROM invoices WHERE id = ?').run(req.params.id);
res.redirect('/invoices');
});
module.exports = router;