537 lines
24 KiB
JavaScript
537 lines
24 KiB
JavaScript
|
|
const express = require('express');
|
||
|
|
const router = express.Router();
|
||
|
|
const bcrypt = require('bcryptjs');
|
||
|
|
// Let op: vanuit routes/spa/ is het juiste pad '../../db' (er is geen routes/db.js)
|
||
|
|
const { getDb, logAudit, getSettings, updateSetting } = require('../../db');
|
||
|
|
const { PROVIDER_PRESETS, getProviders, getProvider, saveProvider, deleteProvider, chatCompletion } = require('../../lib/ai');
|
||
|
|
const { createBackup, listBackups, deleteBackup } = require('../../lib/backup');
|
||
|
|
const emailService = require('../../email');
|
||
|
|
|
||
|
|
// Lazy CREATE TABLE (kopie uit routes/projects.js) zodat de tabellen zeker bestaan
|
||
|
|
try {
|
||
|
|
const db = getDb();
|
||
|
|
db.exec(`
|
||
|
|
CREATE TABLE IF NOT EXISTS projects (
|
||
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
|
|
name TEXT NOT NULL,
|
||
|
|
description TEXT,
|
||
|
|
client_id INTEGER,
|
||
|
|
start_date TEXT,
|
||
|
|
end_date TEXT,
|
||
|
|
status TEXT DEFAULT 'planning',
|
||
|
|
priority TEXT DEFAULT 'medium',
|
||
|
|
budget REAL,
|
||
|
|
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
||
|
|
updated_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
||
|
|
FOREIGN KEY (client_id) REFERENCES clients(id)
|
||
|
|
)
|
||
|
|
`);
|
||
|
|
db.exec(`
|
||
|
|
CREATE TABLE IF NOT EXISTS project_tasks (
|
||
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
|
|
project_id INTEGER NOT NULL,
|
||
|
|
title TEXT NOT NULL,
|
||
|
|
description TEXT,
|
||
|
|
status TEXT DEFAULT 'todo',
|
||
|
|
priority TEXT DEFAULT 'medium',
|
||
|
|
assignee_id INTEGER,
|
||
|
|
start_date TEXT,
|
||
|
|
due_date TEXT,
|
||
|
|
progress INTEGER DEFAULT 0,
|
||
|
|
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
||
|
|
updated_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
||
|
|
FOREIGN KEY (project_id) REFERENCES projects(id),
|
||
|
|
FOREIGN KEY (assignee_id) REFERENCES users(id)
|
||
|
|
)
|
||
|
|
`);
|
||
|
|
db.exec(`
|
||
|
|
CREATE TABLE IF NOT EXISTS project_milestones (
|
||
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
|
|
project_id INTEGER NOT NULL,
|
||
|
|
name TEXT NOT NULL,
|
||
|
|
target_date TEXT,
|
||
|
|
status TEXT DEFAULT 'pending',
|
||
|
|
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
||
|
|
FOREIGN KEY (project_id) REFERENCES projects(id)
|
||
|
|
)
|
||
|
|
`);
|
||
|
|
} catch (e) { /* tabellen bestaan al of db nog niet klaar */ }
|
||
|
|
|
||
|
|
function requireAdmin(req, res, next) {
|
||
|
|
const db = getDb();
|
||
|
|
const user = db.prepare('SELECT * FROM users WHERE id = ?').get(req.session.userId);
|
||
|
|
if (!user || user.role !== 'admin') return res.status(403).json({ error: 'Alleen voor admins' });
|
||
|
|
next();
|
||
|
|
}
|
||
|
|
|
||
|
|
// ============ CALENDAR ============
|
||
|
|
|
||
|
|
router.get('/calendar', (req, res) => {
|
||
|
|
const db = getDb();
|
||
|
|
const events = db.prepare(`
|
||
|
|
SELECT e.*, c.name as client_name
|
||
|
|
FROM calendar_events e
|
||
|
|
LEFT JOIN clients c ON c.id = e.client_id
|
||
|
|
ORDER BY e.start_time DESC
|
||
|
|
`).all();
|
||
|
|
res.json(events);
|
||
|
|
});
|
||
|
|
|
||
|
|
router.post('/calendar', (req, res) => {
|
||
|
|
const db = getDb();
|
||
|
|
const { title, description, client_id, engagement_id, event_type, start_time, end_time, all_day, color } = req.body;
|
||
|
|
if (!title || !start_time || !end_time) return res.status(400).json({ error: 'Titel, start- en eindtijd zijn verplicht' });
|
||
|
|
const result = db.prepare('INSERT INTO calendar_events (user_id, client_id, engagement_id, title, description, event_type, start_time, end_time, all_day, color) VALUES (?,?,?,?,?,?,?,?,?,?)')
|
||
|
|
.run(req.session.userId, client_id || null, engagement_id || null, title, description || '', event_type || 'meeting', start_time, end_time, (all_day === true || all_day === 1 || all_day === '1') ? 1 : 0, color || '#58a6ff');
|
||
|
|
logAudit(req.session.userId, req.session.username, 'event_created', 'calendar_event', result.lastInsertRowid, `Event "${title}" aangemaakt via SPA`);
|
||
|
|
res.status(201).json({ id: result.lastInsertRowid });
|
||
|
|
});
|
||
|
|
|
||
|
|
router.put('/calendar/:id', (req, res) => {
|
||
|
|
const db = getDb();
|
||
|
|
const { title, description, client_id, engagement_id, event_type, start_time, end_time, all_day, color } = req.body;
|
||
|
|
if (!title) return res.status(400).json({ error: 'Titel is verplicht' });
|
||
|
|
db.prepare("UPDATE calendar_events SET title=?, description=?, client_id=?, engagement_id=?, event_type=?, start_time=?, end_time=?, all_day=?, color=?, updated_at=datetime('now') WHERE id=?")
|
||
|
|
.run(title, description, client_id || null, engagement_id || null, event_type, start_time, end_time, (all_day === true || all_day === 1 || all_day === '1') ? 1 : 0, color, req.params.id);
|
||
|
|
res.json({ ok: true });
|
||
|
|
});
|
||
|
|
|
||
|
|
router.delete('/calendar/:id', (req, res) => {
|
||
|
|
const db = getDb();
|
||
|
|
db.prepare('DELETE FROM calendar_events WHERE id = ?').run(req.params.id);
|
||
|
|
res.json({ ok: true });
|
||
|
|
});
|
||
|
|
|
||
|
|
// ============ PROJECTS ============
|
||
|
|
|
||
|
|
router.get('/projects', (req, res) => {
|
||
|
|
const db = getDb();
|
||
|
|
const projects = db.prepare(`
|
||
|
|
SELECT p.*, c.name as client_name,
|
||
|
|
(SELECT COUNT(*) FROM project_tasks t WHERE t.project_id=p.id) as task_count,
|
||
|
|
(SELECT COUNT(*) FROM project_tasks t WHERE t.project_id=p.id AND t.status='done') as done_count
|
||
|
|
FROM projects p
|
||
|
|
LEFT JOIN clients c ON c.id=p.client_id
|
||
|
|
ORDER BY p.updated_at DESC
|
||
|
|
`).all();
|
||
|
|
res.json(projects);
|
||
|
|
});
|
||
|
|
|
||
|
|
router.post('/projects', (req, res) => {
|
||
|
|
const db = getDb();
|
||
|
|
const { name, description, client_id, start_date, end_date, status, priority, budget } = req.body;
|
||
|
|
if (!name || !name.trim()) return res.status(400).json({ error: 'Projectnaam is verplicht' });
|
||
|
|
const result = db.prepare('INSERT INTO projects (name, description, client_id, start_date, end_date, status, priority, budget) VALUES (?,?,?,?,?,?,?,?)')
|
||
|
|
.run(name, description || '', client_id || null, start_date || null, end_date || null, status || 'planning', priority || 'medium', budget || null);
|
||
|
|
logAudit(req.session.userId, req.session.username, 'project_created', 'project', result.lastInsertRowid, `Project ${name} aangemaakt via SPA`);
|
||
|
|
res.status(201).json({ id: result.lastInsertRowid });
|
||
|
|
});
|
||
|
|
|
||
|
|
router.get('/projects/:id', (req, res) => {
|
||
|
|
const db = getDb();
|
||
|
|
const project = db.prepare(`
|
||
|
|
SELECT p.*, c.name as client_name
|
||
|
|
FROM projects p
|
||
|
|
LEFT JOIN clients c ON c.id = p.client_id
|
||
|
|
WHERE p.id = ?
|
||
|
|
`).get(req.params.id);
|
||
|
|
if (!project) return res.status(404).json({ error: 'Project niet gevonden' });
|
||
|
|
res.json({
|
||
|
|
project,
|
||
|
|
tasks: db.prepare(`
|
||
|
|
SELECT pt.*, u.username as assignee_name
|
||
|
|
FROM project_tasks pt
|
||
|
|
LEFT JOIN users u ON u.id = pt.assignee_id
|
||
|
|
WHERE pt.project_id = ?
|
||
|
|
ORDER BY pt.due_date ASC
|
||
|
|
`).all(req.params.id),
|
||
|
|
milestones: db.prepare('SELECT * FROM project_milestones WHERE project_id = ? ORDER BY target_date ASC').all(req.params.id)
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
router.put('/projects/:id', (req, res) => {
|
||
|
|
const db = getDb();
|
||
|
|
const { name, description, client_id, start_date, end_date, status, priority, budget } = req.body;
|
||
|
|
if (!name || !name.trim()) return res.status(400).json({ error: 'Projectnaam is verplicht' });
|
||
|
|
db.prepare('UPDATE projects SET name=?, description=?, client_id=?, start_date=?, end_date=?, status=?, priority=?, budget=?, updated_at=CURRENT_TIMESTAMP WHERE id=?')
|
||
|
|
.run(name, description || '', client_id || null, start_date || null, end_date || null, status, priority, budget || null, req.params.id);
|
||
|
|
res.json({ ok: true });
|
||
|
|
});
|
||
|
|
|
||
|
|
router.delete('/projects/:id', (req, res) => {
|
||
|
|
const db = getDb();
|
||
|
|
db.prepare('DELETE FROM project_tasks WHERE project_id = ?').run(req.params.id);
|
||
|
|
db.prepare('DELETE FROM project_milestones WHERE project_id = ?').run(req.params.id);
|
||
|
|
db.prepare('DELETE FROM projects WHERE id = ?').run(req.params.id);
|
||
|
|
res.json({ ok: true });
|
||
|
|
});
|
||
|
|
|
||
|
|
router.post('/projects/:id/tasks', (req, res) => {
|
||
|
|
const db = getDb();
|
||
|
|
const { title, description, assignee_id, status, priority, progress, due_date } = req.body;
|
||
|
|
if (!title || !title.trim()) return res.status(400).json({ error: 'Titel is verplicht' });
|
||
|
|
const result = db.prepare('INSERT INTO project_tasks (project_id, title, description, assignee_id, status, priority, progress, due_date) VALUES (?,?,?,?,?,?,?,?)')
|
||
|
|
.run(req.params.id, title, description || '', assignee_id || null, status || 'todo', priority || 'medium', progress || 0, due_date || null);
|
||
|
|
res.status(201).json({ id: result.lastInsertRowid });
|
||
|
|
});
|
||
|
|
|
||
|
|
router.post('/projects/tasks/:taskId/status', (req, res) => {
|
||
|
|
const db = getDb();
|
||
|
|
const { status } = req.body;
|
||
|
|
const progress = status === 'done' ? 100 : status === 'in_progress' ? 50 : 0;
|
||
|
|
db.prepare('UPDATE project_tasks SET status = ?, progress = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?')
|
||
|
|
.run(status, progress, req.params.taskId);
|
||
|
|
res.json({ ok: true });
|
||
|
|
});
|
||
|
|
|
||
|
|
router.post('/projects/tasks/:taskId/progress', (req, res) => {
|
||
|
|
const db = getDb();
|
||
|
|
db.prepare('UPDATE project_tasks SET progress = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?')
|
||
|
|
.run(req.body.progress, req.params.taskId);
|
||
|
|
res.json({ ok: true });
|
||
|
|
});
|
||
|
|
|
||
|
|
router.delete('/projects/tasks/:taskId', (req, res) => {
|
||
|
|
const db = getDb();
|
||
|
|
db.prepare('DELETE FROM project_tasks WHERE id = ?').run(req.params.taskId);
|
||
|
|
res.json({ ok: true });
|
||
|
|
});
|
||
|
|
|
||
|
|
// ============ NOTIFICATIONS ============
|
||
|
|
|
||
|
|
router.get('/notifications', (req, res) => {
|
||
|
|
const db = getDb();
|
||
|
|
const notifications = db.prepare('SELECT * FROM notifications WHERE user_id = ? OR user_id IS NULL ORDER BY created_at DESC LIMIT 100').all(req.session.userId);
|
||
|
|
const unread = notifications.filter(n => !n.read).length;
|
||
|
|
res.json({ notifications, unread });
|
||
|
|
});
|
||
|
|
|
||
|
|
router.post('/notifications/:id/read', (req, res) => {
|
||
|
|
const db = getDb();
|
||
|
|
db.prepare('UPDATE notifications SET read=1 WHERE id=? AND (user_id=? OR user_id IS NULL)').run(req.params.id, req.session.userId);
|
||
|
|
res.json({ ok: true });
|
||
|
|
});
|
||
|
|
|
||
|
|
router.post('/notifications/read-all', (req, res) => {
|
||
|
|
const db = getDb();
|
||
|
|
db.prepare('UPDATE notifications SET read=1 WHERE user_id=? AND read=0').run(req.session.userId);
|
||
|
|
res.json({ ok: true });
|
||
|
|
});
|
||
|
|
|
||
|
|
// ============ SEARCH ============
|
||
|
|
|
||
|
|
router.get('/search', (req, res) => {
|
||
|
|
const db = getDb();
|
||
|
|
const q = (req.query.q || '').trim();
|
||
|
|
if (!q) return res.json({ clients: [], engagements: [], tasks: [], diagrams: [], notes: [] });
|
||
|
|
const like = `%${q}%`;
|
||
|
|
|
||
|
|
const clients = db.prepare('SELECT id, name, industry, status FROM clients WHERE name LIKE ? OR industry LIKE ? OR contact_name LIKE ? LIMIT 10').all(like, like, like);
|
||
|
|
|
||
|
|
const engagements = db.prepare('SELECT e.id, e.title, e.type, e.status, c.name as client_name FROM engagements e JOIN clients c ON c.id = e.client_id WHERE e.title LIKE ? OR e.description LIKE ? LIMIT 10').all(like, like);
|
||
|
|
|
||
|
|
const tasks = db.prepare('SELECT t.id, t.title, t.status, t.priority, e.title as engagement_title, c.name as client_name FROM tasks t JOIN engagements e ON e.id = t.engagement_id JOIN clients c ON c.id = e.client_id WHERE t.title LIKE ? LIMIT 10').all(like);
|
||
|
|
|
||
|
|
const diagrams = db.prepare('SELECT d.id, d.name, d.diagram_type, c.name as client_name FROM data_diagrams d JOIN clients c ON c.id = d.client_id WHERE d.name LIKE ? LIMIT 5').all(like);
|
||
|
|
|
||
|
|
const notes = db.prepare('SELECT n.id, n.content, n.created_at, c.name as client_name, c.id as client_id FROM client_notes n JOIN clients c ON c.id = n.client_id WHERE n.content LIKE ? LIMIT 5').all(like);
|
||
|
|
|
||
|
|
res.json({ clients, engagements, tasks, diagrams, notes });
|
||
|
|
});
|
||
|
|
|
||
|
|
// ============ SETTINGS ============
|
||
|
|
|
||
|
|
const ALLOWED_SETTING_KEYS = ['brand_name', 'language', 'theme', 'default_page', 'auto_refresh', 'items_per_page', 'showcase_url', 'smtp_host', 'smtp_port', 'smtp_user', 'smtp_pass', 'smtp_from', 'slack_webhook'];
|
||
|
|
|
||
|
|
router.get('/settings', (req, res) => {
|
||
|
|
const settings = getSettings();
|
||
|
|
const masked = {};
|
||
|
|
for (const [k, v] of Object.entries(settings)) {
|
||
|
|
masked[k] = /pass|secret|key/i.test(k) ? (v ? '••••••••' : '') : v;
|
||
|
|
}
|
||
|
|
res.json(masked);
|
||
|
|
});
|
||
|
|
|
||
|
|
router.post('/settings', (req, res) => {
|
||
|
|
const body = req.body || {};
|
||
|
|
for (const key of ALLOWED_SETTING_KEYS) {
|
||
|
|
if (!(key in body)) continue;
|
||
|
|
const value = body[key];
|
||
|
|
if (value === '••••••••') continue; // gemaskeerde waarde niet opslaan
|
||
|
|
updateSetting(key, value);
|
||
|
|
}
|
||
|
|
res.json({ ok: true });
|
||
|
|
});
|
||
|
|
|
||
|
|
// ============ USERS (admin-only) ============
|
||
|
|
|
||
|
|
router.get('/users', requireAdmin, (req, res) => {
|
||
|
|
const db = getDb();
|
||
|
|
res.json(db.prepare('SELECT id, username, email, role, created_at FROM users ORDER BY username').all());
|
||
|
|
});
|
||
|
|
|
||
|
|
router.post('/users', requireAdmin, (req, res) => {
|
||
|
|
const db = getDb();
|
||
|
|
const { username, password, email, role } = req.body;
|
||
|
|
if (!username || !password) return res.status(400).json({ error: 'Gebruikersnaam en wachtwoord zijn verplicht' });
|
||
|
|
const existing = db.prepare('SELECT id FROM users WHERE username = ?').get(username);
|
||
|
|
if (existing) return res.status(400).json({ error: 'Gebruikersnaam bestaat al' });
|
||
|
|
const hash = bcrypt.hashSync(password, 10);
|
||
|
|
const result = db.prepare('INSERT INTO users (username, password_hash, email, role) VALUES (?, ?, ?, ?)').run(username, hash, email || '', role || 'viewer');
|
||
|
|
db.prepare('INSERT INTO user_settings (user_id) VALUES (?)').run(result.lastInsertRowid);
|
||
|
|
logAudit(req.session.userId, req.session.username, 'user_created', 'user', result.lastInsertRowid, `Gebruiker ${username} aangemaakt via SPA`);
|
||
|
|
res.status(201).json({ id: result.lastInsertRowid });
|
||
|
|
});
|
||
|
|
|
||
|
|
router.put('/users/:id', requireAdmin, (req, res) => {
|
||
|
|
const db = getDb();
|
||
|
|
const { email, role, password } = req.body;
|
||
|
|
const editUser = db.prepare('SELECT id, username, email, role FROM users WHERE id = ?').get(req.params.id);
|
||
|
|
if (!editUser) return res.status(404).json({ error: 'Gebruiker niet gevonden' });
|
||
|
|
if (password && password.trim()) {
|
||
|
|
const hash = bcrypt.hashSync(password, 10);
|
||
|
|
db.prepare('UPDATE users SET password_hash=?, email=?, role=? WHERE id=?').run(hash, email || '', role, req.params.id);
|
||
|
|
} else {
|
||
|
|
db.prepare('UPDATE users SET email=?, role=? WHERE id=?').run(email || '', role, req.params.id);
|
||
|
|
}
|
||
|
|
logAudit(req.session.userId, req.session.username, 'user_updated', 'user', req.params.id, `Gebruiker ${editUser.username} bijgewerkt via SPA`);
|
||
|
|
res.json({ ok: true });
|
||
|
|
});
|
||
|
|
|
||
|
|
router.delete('/users/:id', requireAdmin, (req, res) => {
|
||
|
|
const db = getDb();
|
||
|
|
if (Number(req.params.id) === Number(req.session.userId)) return res.status(400).json({ error: 'Je kunt jezelf niet verwijderen' });
|
||
|
|
db.prepare('DELETE FROM users WHERE id = ?').run(req.params.id);
|
||
|
|
res.json({ ok: true });
|
||
|
|
});
|
||
|
|
|
||
|
|
// ============ AUDIT ============
|
||
|
|
|
||
|
|
router.get('/audit', (req, res) => {
|
||
|
|
const db = getDb();
|
||
|
|
const page = parseInt(req.query.page) || 1;
|
||
|
|
const limit = 50;
|
||
|
|
const offset = (page - 1) * limit;
|
||
|
|
const total = db.prepare('SELECT COUNT(*) as cnt FROM audit_log').get().cnt;
|
||
|
|
const entries = db.prepare('SELECT * FROM audit_log ORDER BY created_at DESC LIMIT ? OFFSET ?').all(limit, offset);
|
||
|
|
res.json({ entries, page, totalPages: Math.ceil(total / limit), total });
|
||
|
|
});
|
||
|
|
|
||
|
|
// ============ BACKUP (admin-only) ============
|
||
|
|
|
||
|
|
router.get('/backup', requireAdmin, (req, res) => {
|
||
|
|
res.json(listBackups());
|
||
|
|
});
|
||
|
|
|
||
|
|
router.post('/backup/create', requireAdmin, (req, res) => {
|
||
|
|
try {
|
||
|
|
const backup = createBackup();
|
||
|
|
logAudit(req.session.userId, req.session.username, 'backup_create', 'system', 0, backup.filename);
|
||
|
|
res.status(201).json({ filename: backup.filename });
|
||
|
|
} catch (e) {
|
||
|
|
res.status(500).json({ error: 'Backup mislukt: ' + e.message });
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
router.post('/backup/delete/:filename', requireAdmin, (req, res) => {
|
||
|
|
if (!deleteBackup(req.params.filename)) return res.status(404).json({ error: 'Backup niet gevonden' });
|
||
|
|
logAudit(req.session.userId, req.session.username, 'backup_delete', 'system', 0, req.params.filename);
|
||
|
|
res.json({ ok: true });
|
||
|
|
});
|
||
|
|
|
||
|
|
// ============ MARKET ============
|
||
|
|
|
||
|
|
router.get('/market/datacenters', (req, res) => {
|
||
|
|
const db = getDb();
|
||
|
|
const { search, vendor, region, type, sort } = req.query;
|
||
|
|
let sql = 'SELECT * FROM datacenters WHERE 1=1';
|
||
|
|
const params = [];
|
||
|
|
if (search) { sql += ' AND (name LIKE ? OR operator LIKE ? OR city LIKE ? OR network_vendors LIKE ? OR server_vendors LIKE ? OR storage_vendors LIKE ?)'; const s = '%'+search+'%'; params.push(s,s,s,s,s,s); }
|
||
|
|
if (vendor) { sql += ' AND (network_vendors LIKE ? OR server_vendors LIKE ? OR storage_vendors LIKE ?)'; const v = '%'+vendor+'%'; params.push(v,v,v); }
|
||
|
|
if (region) { sql += ' AND region LIKE ?'; params.push('%'+region+'%'); }
|
||
|
|
if (type) { sql += ' AND type = ?'; params.push(type); }
|
||
|
|
sql += ' ORDER BY ' + (sort === 'name' ? 'name' : 'power_mw DESC');
|
||
|
|
res.json(db.prepare(sql).all(...params));
|
||
|
|
});
|
||
|
|
|
||
|
|
router.get('/market/companies', (req, res) => {
|
||
|
|
const db = getDb();
|
||
|
|
const { search, industry, data_warehouse, city, sort } = req.query;
|
||
|
|
let sql = 'SELECT * FROM tech_companies WHERE 1=1';
|
||
|
|
const params = [];
|
||
|
|
if (search) { sql += ' AND (name LIKE ? OR industry LIKE ? OR city LIKE ? OR data_warehouse LIKE ? OR bi_tools LIKE ? OR cloud_provider LIKE ? OR ai_ml LIKE ? OR database_vendor LIKE ?)'; const s = '%'+search+'%'; params.push(s,s,s,s,s,s,s,s); }
|
||
|
|
if (industry) { sql += ' AND industry LIKE ?'; params.push('%'+industry+'%'); }
|
||
|
|
if (data_warehouse) { sql += ' AND data_warehouse LIKE ?'; params.push('%'+data_warehouse+'%'); }
|
||
|
|
if (city) { sql += ' AND city LIKE ?'; params.push('%'+city+'%'); }
|
||
|
|
sql += ' ORDER BY ' + (sort === 'name' ? 'name' : sort === 'employees' ? 'employees DESC' : 'lead_score DESC');
|
||
|
|
res.json(db.prepare(sql).all(...params));
|
||
|
|
});
|
||
|
|
|
||
|
|
router.get('/market/filters', (req, res) => {
|
||
|
|
const db = getDb();
|
||
|
|
const allDcs = db.prepare('SELECT * FROM datacenters').all();
|
||
|
|
const allCompanies = db.prepare('SELECT * FROM tech_companies').all();
|
||
|
|
res.json({
|
||
|
|
regions: [...new Set(allDcs.map(d => d.region).filter(Boolean))].sort(),
|
||
|
|
types: [...new Set(allDcs.map(d => d.type).filter(Boolean))].sort(),
|
||
|
|
vendors: [...new Set(allDcs.flatMap(d => ((d.network_vendors||'')+','+(d.server_vendors||'')+','+(d.storage_vendors||'')).split(',').map(s=>s.trim()).filter(Boolean)))].sort(),
|
||
|
|
industries: [...new Set(allCompanies.map(c => c.industry).filter(Boolean))].sort(),
|
||
|
|
cities: [...new Set(allCompanies.map(c => c.city).filter(Boolean))].sort(),
|
||
|
|
data_warehouses: [...new Set(allCompanies.flatMap(c => (c.data_warehouse||'').split(',').map(s=>s.trim()).filter(Boolean)))].sort()
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
// ============ AI ============
|
||
|
|
|
||
|
|
function maskApiKey(key) {
|
||
|
|
if (!key) return '';
|
||
|
|
return '...' + key.slice(-4);
|
||
|
|
}
|
||
|
|
|
||
|
|
router.get('/ai/providers', (req, res) => {
|
||
|
|
const providers = getProviders().map(p => ({
|
||
|
|
...p,
|
||
|
|
api_key: maskApiKey(getProvider(p.id)?.api_key)
|
||
|
|
}));
|
||
|
|
res.json(providers);
|
||
|
|
});
|
||
|
|
|
||
|
|
router.post('/ai/providers', (req, res) => {
|
||
|
|
const { id, name, provider_type, api_key, base_url, default_model, is_active, is_default } = req.body;
|
||
|
|
const preset = PROVIDER_PRESETS[provider_type] || PROVIDER_PRESETS.custom;
|
||
|
|
let key = api_key || '';
|
||
|
|
if (id && (!key || key.includes('...'))) {
|
||
|
|
const existing = getProvider(id);
|
||
|
|
key = existing ? (existing.api_key || '') : '';
|
||
|
|
}
|
||
|
|
const savedId = saveProvider({
|
||
|
|
id: id ? parseInt(id, 10) : null,
|
||
|
|
name: name || preset.label,
|
||
|
|
provider_type: provider_type || 'openrouter',
|
||
|
|
api_key: key,
|
||
|
|
base_url: base_url || preset.base_url,
|
||
|
|
default_model: default_model || preset.models[0] || '',
|
||
|
|
is_active: is_active === true || is_active === 1 || is_active === '1',
|
||
|
|
is_default: is_default === true || is_default === 1 || is_default === '1'
|
||
|
|
});
|
||
|
|
logAudit(req.session.userId, req.session.username, 'ai_provider_saved', 'ai_provider', savedId, name || preset.label);
|
||
|
|
if (id) return res.json({ ok: true, id: savedId });
|
||
|
|
res.status(201).json({ id: savedId });
|
||
|
|
});
|
||
|
|
|
||
|
|
router.delete('/ai/providers/:id', (req, res) => {
|
||
|
|
deleteProvider(req.params.id);
|
||
|
|
logAudit(req.session.userId, req.session.username, 'ai_provider_deleted', 'ai_provider', req.params.id, '');
|
||
|
|
res.json({ ok: true });
|
||
|
|
});
|
||
|
|
|
||
|
|
router.post('/ai/chat', async (req, res) => {
|
||
|
|
try {
|
||
|
|
const { message, client_id, provider_id } = req.body;
|
||
|
|
if (!message || !message.trim()) return res.status(400).json({ error: 'Bericht is verplicht' });
|
||
|
|
const result = await chatCompletion([{ role: 'user', content: message }], {
|
||
|
|
providerId: provider_id ? parseInt(provider_id, 10) : null
|
||
|
|
});
|
||
|
|
const content = result.choices?.[0]?.message?.content || '';
|
||
|
|
if (client_id) {
|
||
|
|
getDb().prepare('INSERT INTO client_notes (client_id, content, created_by) VALUES (?, ?, ?)')
|
||
|
|
.run(client_id, `[AI] ${content.slice(0, 2000)}`, req.session.username || 'ai');
|
||
|
|
}
|
||
|
|
res.json({ content, model: result.model });
|
||
|
|
} catch (e) {
|
||
|
|
res.status(500).json({ error: e.message });
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
// ============ EMAIL ============
|
||
|
|
|
||
|
|
function getActiveAccount(userId) {
|
||
|
|
const db = getDb();
|
||
|
|
return db.prepare('SELECT * FROM email_accounts WHERE user_id = ? AND active = 1 LIMIT 1').get(userId);
|
||
|
|
}
|
||
|
|
|
||
|
|
router.get('/email/messages', (req, res) => {
|
||
|
|
const db = getDb();
|
||
|
|
const messages = db.prepare(`
|
||
|
|
SELECT m.*, a.email as account_email
|
||
|
|
FROM email_messages m
|
||
|
|
JOIN email_accounts a ON a.id = m.account_id
|
||
|
|
WHERE a.user_id = ?
|
||
|
|
ORDER BY m.date DESC LIMIT 50
|
||
|
|
`).all(req.session.userId);
|
||
|
|
res.json(messages);
|
||
|
|
});
|
||
|
|
|
||
|
|
router.get('/email/messages/:id', (req, res) => {
|
||
|
|
const db = getDb();
|
||
|
|
const msg = db.prepare(`
|
||
|
|
SELECT m.*, a.email as account_email
|
||
|
|
FROM email_messages m
|
||
|
|
JOIN email_accounts a ON a.id = m.account_id
|
||
|
|
WHERE m.id = ? AND a.user_id = ?
|
||
|
|
`).get(req.params.id, req.session.userId);
|
||
|
|
if (!msg) return res.status(404).json({ error: 'Bericht niet gevonden' });
|
||
|
|
db.prepare('UPDATE email_messages SET seen = 1 WHERE id = ?').run(req.params.id);
|
||
|
|
msg.seen = 1;
|
||
|
|
res.json(msg);
|
||
|
|
});
|
||
|
|
|
||
|
|
router.post('/email/sync', async (req, res) => {
|
||
|
|
const account = getActiveAccount(req.session.userId);
|
||
|
|
if (!account) return res.status(400).json({ error: 'Geen actief email account geconfigureerd' });
|
||
|
|
try {
|
||
|
|
await emailService.fetchAndStoreMessages(account.id, 'INBOX', 50);
|
||
|
|
logAudit(req.session.userId, req.session.username, 'email_sync', 'email', account.id, 'Email inbox gesynchroniseerd via SPA');
|
||
|
|
res.json({ ok: true });
|
||
|
|
} catch (e) {
|
||
|
|
res.status(500).json({ error: e.message });
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
router.post('/email/send', async (req, res) => {
|
||
|
|
const account = getActiveAccount(req.session.userId);
|
||
|
|
if (!account) return res.status(400).json({ error: 'Geen actief email account geconfigureerd' });
|
||
|
|
const { to, cc, bcc, subject, text, html } = req.body;
|
||
|
|
if (!to || !subject) return res.status(400).json({ error: 'Ontvanger en onderwerp zijn verplicht' });
|
||
|
|
try {
|
||
|
|
await emailService.sendEmail(account.id, to, subject, text, html, cc, bcc);
|
||
|
|
logAudit(req.session.userId, req.session.username, 'email_sent', 'email', account.id, `Email naar ${to}: ${subject}`);
|
||
|
|
res.json({ ok: true });
|
||
|
|
} catch (e) {
|
||
|
|
res.status(500).json({ error: e.message });
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
router.get('/email/accounts', (req, res) => {
|
||
|
|
const db = getDb();
|
||
|
|
const accounts = db.prepare('SELECT id, email, name, imap_host, imap_port, imap_secure, imap_user, smtp_host, smtp_port, smtp_secure, smtp_user, active, created_at FROM email_accounts WHERE user_id = ?').all(req.session.userId);
|
||
|
|
res.json(accounts);
|
||
|
|
});
|
||
|
|
|
||
|
|
router.post('/email/accounts', (req, res) => {
|
||
|
|
const db = getDb();
|
||
|
|
const { id, email, name, imap_host, imap_port, imap_secure, imap_user, imap_pass, smtp_host, smtp_port, smtp_secure, smtp_user, smtp_pass } = req.body;
|
||
|
|
if (!email || !imap_host || !imap_user) return res.status(400).json({ error: 'Email, IMAP host en gebruiker zijn verplicht' });
|
||
|
|
const imapSecure = (imap_secure === true || imap_secure === 1 || imap_secure === '1') ? 1 : 0;
|
||
|
|
const smtpSecure = (smtp_secure === true || smtp_secure === 1 || smtp_secure === '1') ? 1 : 0;
|
||
|
|
if (id) {
|
||
|
|
const existing = db.prepare('SELECT * FROM email_accounts WHERE id = ? AND user_id = ?').get(id, req.session.userId);
|
||
|
|
if (!existing) return res.status(404).json({ error: 'Account niet gevonden' });
|
||
|
|
db.prepare(`UPDATE email_accounts SET email=?, name=?, imap_host=?, imap_port=?, imap_secure=?, imap_user=?, imap_pass=?, smtp_host=?, smtp_port=?, smtp_secure=?, smtp_user=?, smtp_pass=? WHERE id=? AND user_id=?`)
|
||
|
|
.run(email, name || '', imap_host, parseInt(imap_port) || 993, imapSecure, imap_user,
|
||
|
|
imap_pass || existing.imap_pass, smtp_host || imap_host, parseInt(smtp_port) || 587, smtpSecure,
|
||
|
|
smtp_user || imap_user, smtp_pass || existing.smtp_pass, id, req.session.userId);
|
||
|
|
return res.json({ ok: true, id: Number(id) });
|
||
|
|
}
|
||
|
|
if (!imap_pass) return res.status(400).json({ error: 'IMAP wachtwoord is verplicht' });
|
||
|
|
const result = db.prepare(`INSERT INTO email_accounts (user_id, email, name, imap_host, imap_port, imap_secure, imap_user, imap_pass, smtp_host, smtp_port, smtp_secure, smtp_user, smtp_pass)
|
||
|
|
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)`).run(
|
||
|
|
req.session.userId, email, name || '', imap_host, parseInt(imap_port) || 993, imapSecure,
|
||
|
|
imap_user, imap_pass, smtp_host || imap_host, parseInt(smtp_port) || 587, smtpSecure,
|
||
|
|
smtp_user || imap_user, smtp_pass || imap_pass
|
||
|
|
);
|
||
|
|
res.status(201).json({ id: result.lastInsertRowid });
|
||
|
|
});
|
||
|
|
|
||
|
|
module.exports = router;
|