Files

196 lines
10 KiB
JavaScript

const express = require('express');
const router = express.Router();
const { getDb, logAudit } = require('../../db');
const emailService = require('../../email');
// ============ TIME ENTRIES ============
router.get('/time', (req, res) => {
const db = getDb();
const { client_id, date_from, date_to } = req.query;
let sql = `
SELECT t.*, c.name as client_name, e.title as engagement_title, u.username
FROM time_entries t
LEFT JOIN clients c ON c.id = t.client_id
LEFT JOIN engagements e ON e.id = t.engagement_id
LEFT JOIN users u ON u.id = t.user_id
WHERE 1=1`;
const params = [];
if (client_id) { sql += ' AND t.client_id = ?'; params.push(client_id); }
if (date_from) { sql += ' AND t.date >= ?'; params.push(date_from); }
if (date_to) { sql += ' AND t.date <= ?'; params.push(date_to); }
sql += ' ORDER BY t.date DESC, t.created_at DESC';
const entries = db.prepare(sql).all(...params);
res.json({
entries,
totalHours: entries.reduce((s, e) => s + e.hours, 0),
billableHours: entries.filter(e => e.billable).reduce((s, e) => s + e.hours, 0)
});
});
router.post('/time', (req, res) => {
const db = getDb();
const { client_id, engagement_id, date, hours, description, billable, hourly_rate } = req.body;
if (!date || !hours) return res.status(400).json({ error: 'Datum en uren zijn verplicht' });
const result = db.prepare('INSERT INTO time_entries (user_id, client_id, engagement_id, date, hours, description, billable, hourly_rate) VALUES (?,?,?,?,?,?,?,?)')
.run(req.session.userId, client_id || null, engagement_id || null, date, parseFloat(hours), description || '', billable ? 1 : 0, parseFloat(hourly_rate) || 150.0);
logAudit(req.session.userId, req.session.username, 'time_entry_created', 'time_entry', result.lastInsertRowid, `${hours}u geregistreerd op ${date} via SPA`);
res.status(201).json({ id: result.lastInsertRowid });
});
router.put('/time/:id', (req, res) => {
const db = getDb();
const { client_id, engagement_id, date, hours, description, billable, hourly_rate } = req.body;
db.prepare("UPDATE time_entries SET client_id=?, engagement_id=?, date=?, hours=?, description=?, billable=?, hourly_rate=?, updated_at=datetime('now') WHERE id=?")
.run(client_id || null, engagement_id || null, date, parseFloat(hours), description || '', billable ? 1 : 0, parseFloat(hourly_rate) || 150.0, req.params.id);
res.json({ ok: true });
});
router.delete('/time/:id', (req, res) => {
const db = getDb();
db.prepare('DELETE FROM time_entries WHERE id = ?').run(req.params.id);
res.json({ ok: true });
});
// Factuur genereren uit billable uren
router.post('/time/create-invoice', (req, res) => {
const db = getDb();
const { client_id, date_from, date_to } = req.body;
if (!client_id) return res.status(400).json({ error: 'Client is verplicht' });
const entries = 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
AND (? IS NULL OR t.date >= ?) AND (? IS NULL OR t.date <= ?)
`).all(client_id, date_from || null, date_from || null, date_to || null, date_to || null);
if (entries.length === 0) return res.status(400).json({ error: 'Geen facturabele uren gevonden' });
const totalHours = entries.reduce((s, e) => s + e.hours, 0);
const rate = entries[0].hourly_rate || 150;
const subtotal = totalHours * rate;
const tax = subtotal * 0.21;
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 date = new Date().toISOString().split('T')[0];
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, '', subtotal, tax, subtotal + tax, `Factuur gegenereerd uit ${entries.length} tijdregistraties`);
const invoiceId = result.lastInsertRowid;
const insert = db.prepare('INSERT INTO invoice_items (invoice_id, description, quantity, unit_price, total) VALUES (?,?,?,?,?)');
entries.forEach(e => {
const lineTotal = e.hours * (e.hourly_rate || rate);
insert.run(invoiceId, `${e.date} - ${e.engagement_title || 'Werkzaamheden'}: ${e.description || ''}`, e.hours, e.hourly_rate || rate, lineTotal);
});
logAudit(req.session.userId, req.session.username, 'invoice_from_time', 'invoice', invoiceId, `Factuur ${number} gemaakt uit ${entries.length} uren via SPA`);
res.status(201).json({ id: invoiceId, number });
});
// ============ INVOICES ============
router.get('/invoices', (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();
res.json({
invoices,
totalOutstanding: invoices.filter(i => i.status === 'sent' || i.status === 'overdue').reduce((s, i) => s + i.total, 0),
totalPaid: invoices.filter(i => i.status === 'paid').reduce((s, i) => s + i.total, 0)
});
});
router.post('/invoices', (req, res) => {
const db = getDb();
const { client_id, date, due_date, notes, items, subtotal, tax } = req.body;
if (!client_id) return res.status(400).json({ error: 'Client is verplicht' });
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 sub = parseFloat(subtotal) || 0;
const taxAmt = parseFloat(tax) || 0;
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 || '', sub, taxAmt, sub + taxAmt, 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 via SPA`);
res.status(201).json({ id: invoiceId, number });
});
router.get('/invoices/: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.status(404).json({ error: 'Factuur niet gevonden' });
const items = db.prepare('SELECT * FROM invoice_items WHERE invoice_id = ?').all(req.params.id);
res.json({ invoice, items });
});
router.post('/invoices/: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.json({ ok: true });
});
router.delete('/invoices/:id', (req, res) => {
const db = getDb();
db.prepare('DELETE FROM invoices WHERE id = ?').run(req.params.id);
res.json({ ok: true });
});
router.post('/invoices/: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.status(404).json({ error: 'Factuur niet gevonden' });
const account = db.prepare('SELECT * FROM email_accounts WHERE user_id = ? AND active = 1 LIMIT 1').get(req.session.userId);
if (!account) return res.status(400).json({ error: 'Geen e-mailaccount geconfigureerd' });
const items = db.prepare('SELECT * FROM invoice_items WHERE invoice_id = ?').all(req.params.id);
let text = `Factuur ${invoice.number}\n\nClient: ${invoice.client_name}\nDatum: ${invoice.date}\nVervaldatum: ${invoice.due_date || '-'}\n\n`;
items.forEach(item => { text += `${item.description} x${item.quantity} EUR ${(item.unit_price || 0).toFixed(2)} EUR ${(item.total || 0).toFixed(2)}\n`; });
text += `\nSubtotaal: EUR ${(invoice.subtotal || 0).toFixed(2)}\nBTW: EUR ${(invoice.tax || 0).toFixed(2)}\nTotaal: EUR ${(invoice.total || 0).toFixed(2)}\n\nMek-Tech Consulting`;
const to = invoice.contact_email || account.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.json({ ok: true });
} catch (e) {
res.status(500).json({ error: e.message });
}
});
// ============ FINANCE OVERZICHT ============
router.get('/finance', (req, res) => {
const db = getDb();
const year = parseInt(req.query.year) || new Date().getFullYear();
const timeStats = db.prepare(`
SELECT strftime('%m', date) as month, SUM(hours) as hours,
SUM(CASE WHEN billable = 1 THEN hours ELSE 0 END) as billable_hours
FROM time_entries WHERE strftime('%Y', date) = ?
GROUP BY month ORDER BY month
`).all(String(year));
const invoiceStats = db.prepare(`
SELECT strftime('%m', date) as month,
SUM(CASE WHEN status = 'paid' THEN total ELSE 0 END) as paid,
SUM(CASE WHEN status IN ('sent','overdue') THEN total ELSE 0 END) as outstanding
FROM invoices WHERE strftime('%Y', date) = ?
GROUP BY month ORDER BY month
`).all(String(year));
const totals = {
hoursThisYear: db.prepare("SELECT COALESCE(SUM(hours),0) as s FROM time_entries WHERE strftime('%Y', date) = ?").get(String(year)).s,
paidThisYear: db.prepare("SELECT COALESCE(SUM(total),0) as s FROM invoices WHERE status='paid' AND strftime('%Y', date) = ?").get(String(year)).s,
outstanding: db.prepare("SELECT COALESCE(SUM(total),0) as s FROM invoices WHERE status IN ('sent','overdue')").get().s,
draftTotal: db.prepare("SELECT COALESCE(SUM(total),0) as s FROM invoices WHERE status='draft'").get().s
};
res.json({ year, timeStats, invoiceStats, totals });
});
module.exports = router;