106 lines
5.6 KiB
JavaScript
106 lines
5.6 KiB
JavaScript
|
|
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;
|