293 lines
16 KiB
JavaScript
293 lines
16 KiB
JavaScript
const express = require('express');
|
|
const db = require('../db');
|
|
const { authenticate, requireAdmin } = require('../middleware/auth');
|
|
const router = express.Router();
|
|
const uid = req => req.user.id;
|
|
|
|
// ── Quick Links ───────────────────────────────────────────────────────────────
|
|
router.get('/links', authenticate, (req, res) => {
|
|
const id = uid(req);
|
|
// Rückwärtskomp: bestehende quick_links + neue link_list-Einträge mit in_quickaccess=1 + Ordner mit in_quickaccess=1
|
|
const legacy = db.prepare("SELECT *, 'quicklink' as item_type FROM quick_links WHERE user_id=? ORDER BY sort_order,id").all(id);
|
|
const listLinks = db.prepare("SELECT *, 'link_list' as item_type FROM link_list WHERE user_id=? AND in_quickaccess=1 ORDER BY sort_order,id").all(id);
|
|
const folders = db.prepare("SELECT *, 'folder' as item_type FROM link_list_folders WHERE user_id=? AND in_quickaccess=1 ORDER BY sort_order,id").all(id);
|
|
res.json([...legacy, ...listLinks, ...folders]);
|
|
});
|
|
router.post('/links', authenticate, (req, res) => {
|
|
const { title, url, icon = '🔗' } = req.body;
|
|
if (!title || !url) return res.status(400).json({ error: 'Titel und URL erforderlich' });
|
|
const max = db.prepare('SELECT MAX(sort_order) m FROM quick_links WHERE user_id=?').get(uid(req));
|
|
const r = db.prepare('INSERT INTO quick_links (user_id,title,url,icon,sort_order) VALUES (?,?,?,?,?)')
|
|
.run(uid(req), title, url, icon, (max?.m ?? -1) + 1);
|
|
res.json(db.prepare('SELECT * FROM quick_links WHERE id=?').get(r.lastInsertRowid));
|
|
});
|
|
router.put('/links/:id', authenticate, (req, res) => {
|
|
const ex = db.prepare('SELECT * FROM quick_links WHERE id=? AND user_id=?').get(req.params.id, uid(req));
|
|
if (!ex) return res.status(404).json({ error: 'Nicht gefunden' });
|
|
const { title=ex.title, url=ex.url, icon=ex.icon } = req.body;
|
|
db.prepare('UPDATE quick_links SET title=?,url=?,icon=? WHERE id=? AND user_id=?')
|
|
.run(title, url, icon, req.params.id, uid(req));
|
|
res.json(db.prepare('SELECT * FROM quick_links WHERE id=?').get(req.params.id));
|
|
});
|
|
router.delete('/links/:id', authenticate, (req, res) => {
|
|
const r = db.prepare('DELETE FROM quick_links WHERE id=? AND user_id=?').run(req.params.id, uid(req));
|
|
if (!r.changes) return res.status(404).json({ error: 'Nicht gefunden' });
|
|
res.json({ success: true });
|
|
});
|
|
|
|
router.put('/links-order', authenticate, (req, res) => {
|
|
const { ids } = req.body;
|
|
if (!Array.isArray(ids)) return res.status(400).json({ error: 'ids erforderlich' });
|
|
const update = db.prepare('UPDATE quick_links SET sort_order=? WHERE id=? AND user_id=?');
|
|
db.transaction(() => { ids.forEach((id, i) => update.run(i, id, uid(req))); })();
|
|
res.json({ ok: true });
|
|
});
|
|
|
|
// ── Todos ─────────────────────────────────────────────────────────────────────
|
|
router.get('/todos', authenticate, (req, res) => {
|
|
res.json(db.prepare('SELECT * FROM todos WHERE user_id=? ORDER BY done,sort_order,id').all(uid(req)));
|
|
});
|
|
router.post('/todos', authenticate, (req, res) => {
|
|
const { text } = req.body;
|
|
if (!text?.trim()) return res.status(400).json({ error: 'Text erforderlich' });
|
|
const max = db.prepare('SELECT MAX(sort_order) m FROM todos WHERE user_id=?').get(uid(req));
|
|
const r = db.prepare('INSERT INTO todos (user_id,text,sort_order) VALUES (?,?,?)')
|
|
.run(uid(req), text.trim(), (max?.m ?? -1) + 1);
|
|
res.json(db.prepare('SELECT * FROM todos WHERE id=?').get(r.lastInsertRowid));
|
|
});
|
|
router.put('/todos/:id', authenticate, (req, res) => {
|
|
const ex = db.prepare('SELECT * FROM todos WHERE id=? AND user_id=?').get(req.params.id, uid(req));
|
|
if (!ex) return res.status(404).json({ error: 'Nicht gefunden' });
|
|
const text = req.body.text ?? ex.text;
|
|
const done = req.body.done !== undefined ? (req.body.done ? 1 : 0) : ex.done;
|
|
db.prepare('UPDATE todos SET text=?,done=? WHERE id=? AND user_id=?').run(text, done, req.params.id, uid(req));
|
|
res.json(db.prepare('SELECT * FROM todos WHERE id=?').get(req.params.id));
|
|
});
|
|
router.delete('/todos/:id', authenticate, (req, res) => {
|
|
const r = db.prepare('DELETE FROM todos WHERE id=? AND user_id=?').run(req.params.id, uid(req));
|
|
if (!r.changes) return res.status(404).json({ error: 'Nicht gefunden' });
|
|
res.json({ success: true });
|
|
});
|
|
|
|
// ── Notizen ───────────────────────────────────────────────────────────────────
|
|
router.get('/note', authenticate, (req, res) => {
|
|
res.json(db.prepare('SELECT * FROM notes WHERE user_id=?').get(uid(req)) || { content: '', updated_at: null });
|
|
});
|
|
router.put('/note', authenticate, (req, res) => {
|
|
db.prepare(`INSERT INTO notes (user_id,content,updated_at) VALUES (?,?,CURRENT_TIMESTAMP)
|
|
ON CONFLICT(user_id) DO UPDATE SET content=excluded.content,updated_at=CURRENT_TIMESTAMP`)
|
|
.run(uid(req), req.body.content ?? '');
|
|
res.json({ success: true, updated_at: new Date().toISOString() });
|
|
});
|
|
|
|
|
|
// ── Statistiken ───────────────────────────────────────────────────────────────
|
|
router.get('/stats', authenticate, (req, res) => {
|
|
const uid = req.user.id;
|
|
|
|
const orders = db.prepare(`
|
|
SELECT status, bezahlt,
|
|
SUM(COALESCE(custom_price_sum_view,0)) AS revenue
|
|
FROM (
|
|
SELECT o.status, o.bezahlt,
|
|
SUM(COALESCE(oi.custom_price,0)*COALESCE(oi.stueckzahl,0)) AS custom_price_sum_view
|
|
FROM orders o
|
|
LEFT JOIN order_items oi ON oi.order_id=o.id
|
|
WHERE o.user_id=?
|
|
GROUP BY o.id
|
|
)
|
|
GROUP BY status, bezahlt
|
|
`).all(uid);
|
|
|
|
const totalOrders = db.prepare('SELECT COUNT(*) c FROM orders WHERE user_id=?').get(uid).c;
|
|
const bezahltOrders = db.prepare('SELECT COUNT(*) c FROM orders WHERE user_id=? AND bezahlt=1').get(uid).c;
|
|
// Revenue: Gesamt-Festpreis hat Priorität, sonst Summe der Positions-Festpreise
|
|
const paidOrders = db.prepare('SELECT * FROM orders WHERE user_id=? AND bezahlt=1').all(uid);
|
|
let totalRevenue = 0;
|
|
for (const o of paidOrders) {
|
|
if (o.custom_price != null) {
|
|
totalRevenue += parseFloat(o.custom_price);
|
|
} else {
|
|
const sum = db.prepare('SELECT COALESCE(SUM(custom_price*stueckzahl),0) s FROM order_items WHERE order_id=? AND custom_price IS NOT NULL').get(o.id).s;
|
|
totalRevenue += sum;
|
|
}
|
|
}
|
|
|
|
const openOrders = db.prepare("SELECT * FROM orders WHERE user_id=? AND bezahlt=0 AND status!='warteliste'").all(uid);
|
|
let offeneRevenue = 0;
|
|
for (const o of openOrders) {
|
|
if (o.custom_price != null) {
|
|
offeneRevenue += parseFloat(o.custom_price);
|
|
} else {
|
|
const sum = db.prepare('SELECT COALESCE(SUM(custom_price*stueckzahl),0) s FROM order_items WHERE order_id=? AND custom_price IS NOT NULL').get(o.id).s;
|
|
offeneRevenue += sum;
|
|
}
|
|
}
|
|
const thisMonth = new Date();
|
|
thisMonth.setDate(1); thisMonth.setHours(0,0,0,0);
|
|
const ordersThisMonth = db.prepare('SELECT COUNT(*) c FROM orders WHERE user_id=? AND created_at>=?').get(uid, thisMonth.toISOString()).c;
|
|
const totalCalcs = db.prepare('SELECT COUNT(*) c FROM calculations WHERE user_id=?').get(uid).c;
|
|
|
|
const byStatus = { warteliste:0, in_arbeit:0, fertig:0 };
|
|
for (const r of db.prepare('SELECT status, COUNT(*) c FROM orders WHERE user_id=? AND bezahlt=0 GROUP BY status').all(uid)) {
|
|
byStatus[r.status] = r.c;
|
|
}
|
|
|
|
res.json({ totalOrders, bezahltOrders, totalRevenue, offeneRevenue, ordersThisMonth, totalCalcs, byStatus });
|
|
});
|
|
|
|
// ── Ideen-Board ───────────────────────────────────────────────────────────────
|
|
router.get('/board', authenticate, (req, res) => {
|
|
const u = uid(req);
|
|
const items = db.prepare(`
|
|
SELECT b.*, us.username as author
|
|
FROM board_items b JOIN users us ON us.id=b.user_id
|
|
ORDER BY b.type ASC, b.created_at DESC
|
|
`).all();
|
|
|
|
const lastRead = db.prepare('SELECT last_read FROM board_reads WHERE user_id=?').get(u)?.last_read;
|
|
const unread = lastRead
|
|
? db.prepare("SELECT COUNT(*) c FROM board_items WHERE created_at > ? AND user_id != ?").get(lastRead, u).c
|
|
: db.prepare('SELECT COUNT(*) c FROM board_items WHERE user_id != ?').get(u).c;
|
|
|
|
res.json({ items, unread });
|
|
});
|
|
|
|
router.get('/board/unread', authenticate, (req, res) => {
|
|
const u = uid(req);
|
|
const lastRead = db.prepare('SELECT last_read FROM board_reads WHERE user_id=?').get(u)?.last_read;
|
|
const unread = lastRead
|
|
? db.prepare("SELECT COUNT(*) c FROM board_items WHERE created_at > ? AND user_id != ?").get(lastRead, u).c
|
|
: db.prepare('SELECT COUNT(*) c FROM board_items WHERE user_id != ?').get(u).c;
|
|
const unreadPromoted = lastRead
|
|
? db.prepare("SELECT COUNT(*) c FROM board_items WHERE promoted_at > ?").get(lastRead).c
|
|
: db.prepare("SELECT COUNT(*) c FROM board_items WHERE promoted_at IS NOT NULL").get().c;
|
|
res.json({ unread, unreadPromoted });
|
|
});
|
|
|
|
router.post('/board/read', authenticate, (req, res) => {
|
|
const u = uid(req);
|
|
db.prepare(`INSERT OR REPLACE INTO board_reads (user_id, last_read) VALUES (?, datetime('now','localtime'))`).run(u);
|
|
res.json({ ok: true });
|
|
});
|
|
|
|
router.post('/board', authenticate, (req, res) => {
|
|
const u = uid(req);
|
|
const { type, title, description = '' } = req.body;
|
|
if (!['roadmap','wish'].includes(type)) return res.status(400).json({ error: 'Ungültiger Typ' });
|
|
if (type === 'roadmap' && req.user.role !== 'admin') return res.status(403).json({ error: 'Nur Admins' });
|
|
if (!title?.trim()) return res.status(400).json({ error: 'Titel erforderlich' });
|
|
const r = db.prepare(`INSERT INTO board_items (type, user_id, title, description, created_at) VALUES (?,?,?,?,datetime('now','localtime'))`)
|
|
.run(type, u, title.trim(), description.trim());
|
|
res.json({ ...db.prepare('SELECT * FROM board_items WHERE id=?').get(r.lastInsertRowid),
|
|
author: req.user.username });
|
|
});
|
|
|
|
router.put('/board/:id', authenticate, (req, res) => {
|
|
const item = db.prepare('SELECT * FROM board_items WHERE id=?').get(req.params.id);
|
|
if (!item) return res.status(404).json({ error: 'Nicht gefunden' });
|
|
const isAdmin = req.user.role === 'admin';
|
|
const isOwn = item.user_id === uid(req);
|
|
if (!isAdmin && (!isOwn || item.type === 'roadmap')) return res.status(403).json({ error: 'Kein Zugriff' });
|
|
const { title, description } = req.body;
|
|
db.prepare('UPDATE board_items SET title=?, description=? WHERE id=?')
|
|
.run(title ?? item.title, description ?? item.description, item.id);
|
|
res.json({ ...db.prepare('SELECT * FROM board_items WHERE id=?').get(item.id), author: req.user.username });
|
|
});
|
|
|
|
router.delete('/board/:id', authenticate, (req, res) => {
|
|
const item = db.prepare('SELECT * FROM board_items WHERE id=?').get(req.params.id);
|
|
if (!item) return res.status(404).json({ error: 'Nicht gefunden' });
|
|
const isAdmin = req.user.role === 'admin';
|
|
const isOwn = item.user_id === uid(req);
|
|
if (!isAdmin && !isOwn) return res.status(403).json({ error: 'Kein Zugriff' });
|
|
db.prepare('DELETE FROM board_items WHERE id=?').run(item.id);
|
|
res.json({ ok: true });
|
|
});
|
|
|
|
// Wunsch zur Roadmap befördern (Admin only)
|
|
router.post('/board/:id/promote', authenticate, (req, res) => {
|
|
if (req.user.role !== 'admin') return res.status(403).json({ error: 'Kein Zugriff' });
|
|
const item = db.prepare('SELECT * FROM board_items WHERE id=?').get(req.params.id);
|
|
if (!item) return res.status(404).json({ error: 'Nicht gefunden' });
|
|
if (item.type !== 'wish') return res.status(400).json({ error: 'Nur Wünsche können befördert werden' });
|
|
db.prepare("UPDATE board_items SET type='roadmap', promoted_from_wish=1, promoted_at=datetime('now','localtime') WHERE id=?").run(item.id);
|
|
const updated = db.prepare(`
|
|
SELECT b.*, u.username as author FROM board_items b
|
|
JOIN users u ON u.id=b.user_id WHERE b.id=?
|
|
`).get(item.id);
|
|
res.json(updated);
|
|
});
|
|
|
|
// ── Changelog ─────────────────────────────────────────────────────────────────
|
|
router.get('/changelog', authenticate, (req, res) => {
|
|
res.json(db.prepare('SELECT * FROM changelog ORDER BY created_at DESC').all());
|
|
});
|
|
|
|
router.get('/changelog/unread', authenticate, (req, res) => {
|
|
const u = req.user.id;
|
|
const lastRead = db.prepare('SELECT last_read FROM changelog_reads WHERE user_id=?').get(u)?.last_read;
|
|
const count = lastRead
|
|
? db.prepare('SELECT COUNT(*) c FROM changelog WHERE created_at > ?').get(lastRead).c
|
|
: db.prepare('SELECT COUNT(*) c FROM changelog').get().c;
|
|
res.json({ unread: count });
|
|
});
|
|
|
|
router.post('/changelog/read', authenticate, (req, res) => {
|
|
db.prepare(`INSERT OR REPLACE INTO changelog_reads (user_id, last_read) VALUES (?, datetime('now','localtime'))`)
|
|
.run(req.user.id);
|
|
res.json({ ok: true });
|
|
});
|
|
|
|
router.post('/changelog', authenticate, requireAdmin, (req, res) => {
|
|
const { version, title, body, build_time } = req.body;
|
|
if (!version?.trim() || !title?.trim()) return res.status(400).json({ error: 'Version und Titel erforderlich' });
|
|
const r = db.prepare(`INSERT INTO changelog (version, title, body, build_time, created_at) VALUES (?,?,?,?,datetime('now','localtime'))`)
|
|
.run(version.trim(), title.trim(), body?.trim() || '', build_time || null);
|
|
res.json(db.prepare('SELECT * FROM changelog WHERE id=?').get(r.lastInsertRowid));
|
|
});
|
|
|
|
router.delete('/changelog/:id', authenticate, requireAdmin, (req, res) => {
|
|
db.prepare('DELETE FROM changelog WHERE id=?').run(req.params.id);
|
|
res.json({ ok: true });
|
|
});
|
|
|
|
// ── Push-Zeitplaner ───────────────────────────────────────────────────────────
|
|
router.get('/push-schedules', authenticate, (req, res) => {
|
|
const rows = db.prepare(`
|
|
SELECT * FROM push_schedules
|
|
WHERE user_id=? AND sent=0 AND scheduled_at > datetime('now','localtime')
|
|
ORDER BY scheduled_at ASC
|
|
`).all(uid(req));
|
|
res.json(rows);
|
|
});
|
|
|
|
router.post('/push-schedules', authenticate, (req, res) => {
|
|
const { message, scheduled_at } = req.body;
|
|
if (!message?.trim() || !scheduled_at) return res.status(400).json({ error: 'Nachricht und Zeitpunkt erforderlich' });
|
|
// Normalisieren: T→Leerzeichen, Sekunden ergänzen
|
|
const normalized = scheduled_at.replace('T',' ') + (scheduled_at.length <= 16 ? ':00' : '');
|
|
const r = db.prepare(`
|
|
INSERT INTO push_schedules (user_id, message, scheduled_at, created_at)
|
|
VALUES (?, ?, ?, datetime('now','localtime'))
|
|
`).run(uid(req), message.trim(), normalized);
|
|
res.json(db.prepare('SELECT * FROM push_schedules WHERE id=?').get(r.lastInsertRowid));
|
|
});
|
|
|
|
router.put('/push-schedules/:id', authenticate, (req, res) => {
|
|
const row = db.prepare('SELECT * FROM push_schedules WHERE id=? AND user_id=?').get(req.params.id, uid(req));
|
|
if (!row) return res.status(404).json({ error: 'Nicht gefunden' });
|
|
const { message, scheduled_at } = req.body;
|
|
const normalized = scheduled_at ? scheduled_at.replace('T',' ') + (scheduled_at.length <= 16 ? ':00' : '') : row.scheduled_at;
|
|
db.prepare('UPDATE push_schedules SET message=?, scheduled_at=?, sent=0 WHERE id=?')
|
|
.run(message ?? row.message, normalized, row.id);
|
|
res.json(db.prepare('SELECT * FROM push_schedules WHERE id=?').get(row.id));
|
|
});
|
|
|
|
router.delete('/push-schedules/:id', authenticate, (req, res) => {
|
|
db.prepare('DELETE FROM push_schedules WHERE id=? AND user_id=?').run(req.params.id, uid(req));
|
|
res.json({ ok: true });
|
|
});
|
|
|
|
module.exports = router;
|