A lot of change in this commit. Changing the dashboard to get more data and the new features added
This commit is contained in:
+37
-13
@@ -22,6 +22,19 @@ const router = express.Router();
|
||||
const VALID_SOURCES = ['local', 'google', 'apple', 'ics'];
|
||||
const ICS_COLOR_RE = /^#[0-9a-fA-F]{6}$/;
|
||||
|
||||
function getUserId(req) {
|
||||
const candidates = [req.authUserId, req.user?.id, req.session?.userId];
|
||||
for (const value of candidates) {
|
||||
const parsed = Number(value);
|
||||
if (Number.isInteger(parsed) && parsed > 0) return parsed;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function isAdminUser(req) {
|
||||
return req.authRole === 'admin' || req.session?.isAdmin === true || req.session?.role === 'admin';
|
||||
}
|
||||
|
||||
// --------------------------------------------------------
|
||||
// RRULE-Expansion: alle Vorkommen eines wiederkehrenden Events
|
||||
// innerhalb [from, to] generieren (inklusive beider Grenzen).
|
||||
@@ -146,7 +159,7 @@ router.get('/', (req, res) => {
|
||||
)
|
||||
)
|
||||
`;
|
||||
const params = [to, from, to, req.session.userId];
|
||||
const params = [to, from, to, getUserId(req)];
|
||||
|
||||
if (req.query.assigned_to) {
|
||||
sql += ' AND e.assigned_to = ?';
|
||||
@@ -203,7 +216,7 @@ router.get('/upcoming', (req, res) => {
|
||||
)
|
||||
)
|
||||
ORDER BY e.start_datetime ASC
|
||||
`).all(nowDate, future, future, req.session.userId);
|
||||
`).all(nowDate, future, future, getUserId(req));
|
||||
|
||||
const expanded = expandRecurringEvents(rawEvents, nowDate, future)
|
||||
.filter((e) => e.start_datetime >= new Date().toISOString())
|
||||
@@ -396,7 +409,7 @@ router.delete('/apple/disconnect', requireAdmin, (req, res) => {
|
||||
|
||||
router.get('/subscriptions', (req, res) => {
|
||||
try {
|
||||
const subs = icsSubscription.getAll(req.session.userId);
|
||||
const subs = icsSubscription.getAll(getUserId(req));
|
||||
res.json({ data: subs });
|
||||
} catch (err) {
|
||||
log.error('', err);
|
||||
@@ -416,7 +429,7 @@ router.post('/subscriptions', async (req, res) => {
|
||||
if (!colorVal || !ICS_COLOR_RE.test(colorVal))
|
||||
return res.status(400).json({ error: 'color: Pflichtfeld, muss #RRGGBB sein.', code: 400 });
|
||||
|
||||
const { sub, syncError } = await icsSubscription.create(req.session.userId, {
|
||||
const { sub, syncError } = await icsSubscription.create(getUserId(req), {
|
||||
name: name.trim(), url, color: colorVal, shared: shared ? 1 : 0,
|
||||
});
|
||||
res.status(201).json({ data: sub, syncError: syncError || null });
|
||||
@@ -432,7 +445,7 @@ router.patch('/subscriptions/:id', (req, res) => {
|
||||
try {
|
||||
const subId = parseInt(req.params.id, 10);
|
||||
if (!Number.isFinite(subId)) return res.status(400).json({ error: 'Ungültige ID.', code: 400 });
|
||||
const isAdmin = req.session.isAdmin;
|
||||
const isAdmin = isAdminUser(req);
|
||||
const fields = {};
|
||||
if (req.body.name !== undefined) {
|
||||
if (typeof req.body.name !== 'string' || req.body.name.trim().length === 0 || req.body.name.length > 100)
|
||||
@@ -446,7 +459,7 @@ router.patch('/subscriptions/:id', (req, res) => {
|
||||
}
|
||||
if (req.body.shared !== undefined) fields.shared = req.body.shared;
|
||||
|
||||
const updated = icsSubscription.update(req.session.userId, subId, fields, isAdmin);
|
||||
const updated = icsSubscription.update(getUserId(req), subId, fields, isAdmin);
|
||||
if (!updated) return res.status(404).json({ error: 'Abonnement nicht gefunden.', code: 404 });
|
||||
res.json({ data: updated });
|
||||
} catch (err) {
|
||||
@@ -460,8 +473,8 @@ router.delete('/subscriptions/:id', (req, res) => {
|
||||
try {
|
||||
const subId = parseInt(req.params.id, 10);
|
||||
if (!Number.isFinite(subId)) return res.status(400).json({ error: 'Ungültige ID.', code: 400 });
|
||||
const isAdmin = req.session.isAdmin;
|
||||
const ok = icsSubscription.remove(req.session.userId, subId, isAdmin);
|
||||
const isAdmin = isAdminUser(req);
|
||||
const ok = icsSubscription.remove(getUserId(req), subId, isAdmin);
|
||||
if (!ok) return res.status(404).json({ error: 'Abonnement nicht gefunden.', code: 404 });
|
||||
res.status(204).end();
|
||||
} catch (err) {
|
||||
@@ -475,10 +488,10 @@ router.post('/subscriptions/:id/sync', async (req, res) => {
|
||||
try {
|
||||
const subId = parseInt(req.params.id, 10);
|
||||
if (!Number.isFinite(subId)) return res.status(400).json({ error: 'Ungültige ID.', code: 400 });
|
||||
const isAdmin = req.session.isAdmin;
|
||||
const isAdmin = isAdminUser(req);
|
||||
const sub = db.get().prepare('SELECT * FROM ics_subscriptions WHERE id = ?').get(subId);
|
||||
if (!sub) return res.status(404).json({ error: 'Abonnement nicht gefunden.', code: 404 });
|
||||
if (!isAdmin && sub.created_by !== req.session.userId)
|
||||
if (!isAdmin && sub.created_by !== getUserId(req))
|
||||
return res.status(403).json({ error: 'Nicht autorisiert.', code: 403 });
|
||||
await icsSubscription.sync(subId);
|
||||
const updated = db.get().prepare('SELECT * FROM ics_subscriptions WHERE id = ?').get(subId);
|
||||
@@ -526,6 +539,17 @@ router.get('/:id', (req, res) => {
|
||||
// --------------------------------------------------------
|
||||
router.post('/', (req, res) => {
|
||||
try {
|
||||
const userId = getUserId(req);
|
||||
if (!userId) {
|
||||
log.warn('Rejecting calendar create without resolved authenticated user id', {
|
||||
authMethod: req.authMethod || null,
|
||||
authUserId: req.authUserId || null,
|
||||
reqUserId: req.user?.id || null,
|
||||
sessionUserId: req.session?.userId || null,
|
||||
});
|
||||
return res.status(401).json({ error: 'Not authenticated.', code: 401 });
|
||||
}
|
||||
|
||||
const vTitle = str(req.body.title, 'Titel', { max: MAX_TITLE });
|
||||
const vDesc = str(req.body.description, 'Beschreibung', { max: MAX_TEXT, required: false });
|
||||
const vStart = datetime(req.body.start_datetime, 'Startdatum', true);
|
||||
@@ -553,7 +577,7 @@ router.post('/', (req, res) => {
|
||||
vStart.value, vEnd.value,
|
||||
all_day ? 1 : 0, vLoc.value,
|
||||
vColor.value, assigned_to || null,
|
||||
req.session.userId, vRrule.value
|
||||
userId, vRrule.value
|
||||
);
|
||||
|
||||
const event = db.get().prepare(`
|
||||
@@ -669,8 +693,8 @@ router.post('/:id/reset', (req, res) => {
|
||||
if (event.external_source !== 'ics')
|
||||
return res.status(400).json({ error: 'Nur ICS-Events können zurückgesetzt werden.', code: 400 });
|
||||
|
||||
const userId = req.session.userId;
|
||||
const isAdmin = req.session.isAdmin;
|
||||
const userId = getUserId(req);
|
||||
const isAdmin = isAdminUser(req);
|
||||
if (!isAdmin && event.created_by !== userId && event.sub_created_by !== userId)
|
||||
return res.status(403).json({ error: 'Nicht autorisiert.', code: 403 });
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
import { createLogger } from '../logger.js';
|
||||
import express from 'express';
|
||||
import * as db from '../db.js';
|
||||
import { hydrateBirthday } from '../services/birthdays.js';
|
||||
|
||||
const log = createLogger('Dashboard');
|
||||
|
||||
@@ -30,10 +31,12 @@ router.get('/', (req, res) => {
|
||||
try {
|
||||
const d = db.get();
|
||||
const result = {};
|
||||
const userId = req.authUserId || req.session.userId;
|
||||
|
||||
// Heute und +48h als ISO-Strings
|
||||
const now = new Date();
|
||||
const todayStr = now.toISOString().slice(0, 10);
|
||||
const currentMonth = todayStr.slice(0, 7);
|
||||
const deadline48h = new Date(now.getTime() + 48 * 60 * 60 * 1000).toISOString();
|
||||
|
||||
// Anstehende Termine (nächste 5, ab jetzt)
|
||||
@@ -170,6 +173,63 @@ router.get('/', (req, res) => {
|
||||
result.users = [];
|
||||
}
|
||||
|
||||
try {
|
||||
const rows = d.prepare('SELECT * FROM birthdays WHERE created_by = ? ORDER BY name COLLATE NOCASE ASC').all(userId);
|
||||
result.birthdays = rows
|
||||
.map((row) => hydrateBirthday(row))
|
||||
.sort((a, b) => a.days_until - b.days_until || a.name.localeCompare(b.name))
|
||||
.slice(0, 3);
|
||||
result.birthdayCount = rows.length;
|
||||
} catch (err) {
|
||||
log.error('birthdays error:', err.message);
|
||||
result.birthdays = [];
|
||||
result.birthdayCount = 0;
|
||||
}
|
||||
|
||||
try {
|
||||
const from = `${currentMonth}-01`;
|
||||
const to = `${currentMonth}-31`;
|
||||
const totals = d.prepare(`
|
||||
SELECT
|
||||
SUM(CASE WHEN amount > 0 THEN amount ELSE 0 END) AS income,
|
||||
SUM(CASE WHEN amount < 0 THEN amount ELSE 0 END) AS expenses,
|
||||
SUM(amount) AS balance,
|
||||
COUNT(*) AS entry_count
|
||||
FROM budget_entries
|
||||
WHERE date BETWEEN ? AND ?
|
||||
`).get(from, to);
|
||||
|
||||
const topExpense = d.prepare(`
|
||||
SELECT category, SUM(amount) AS amount
|
||||
FROM budget_entries
|
||||
WHERE amount < 0 AND date BETWEEN ? AND ?
|
||||
GROUP BY category
|
||||
ORDER BY ABS(SUM(amount)) DESC
|
||||
LIMIT 1
|
||||
`).get(from, to);
|
||||
|
||||
result.budget = {
|
||||
month: currentMonth,
|
||||
income: totals?.income || 0,
|
||||
expenses: Math.abs(totals?.expenses || 0),
|
||||
balance: totals?.balance || 0,
|
||||
entryCount: totals?.entry_count || 0,
|
||||
topExpenseCategory: topExpense?.category || null,
|
||||
topExpenseAmount: Math.abs(topExpense?.amount || 0),
|
||||
};
|
||||
} catch (err) {
|
||||
log.error('budget error:', err.message);
|
||||
result.budget = {
|
||||
month: currentMonth,
|
||||
income: 0,
|
||||
expenses: 0,
|
||||
balance: 0,
|
||||
entryCount: 0,
|
||||
topExpenseCategory: null,
|
||||
topExpenseAmount: 0,
|
||||
};
|
||||
}
|
||||
|
||||
res.json(result);
|
||||
} catch (err) {
|
||||
log.error('Critical error:', err.message);
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
import { createLogger } from '../logger.js';
|
||||
import express from 'express';
|
||||
import * as db from '../db.js';
|
||||
import { str, MAX_SHORT } from '../middleware/validate.js';
|
||||
|
||||
const log = createLogger('Preferences');
|
||||
|
||||
@@ -17,8 +18,12 @@ const DEFAULT_MEAL_TYPES = VALID_MEAL_TYPES.join(',');
|
||||
|
||||
const VALID_CURRENCIES = ['AED', 'AUD', 'BRL', 'CAD', 'CHF', 'CNY', 'CZK', 'DKK', 'EUR', 'GBP', 'HUF', 'INR', 'JPY', 'NOK', 'PLN', 'RUB', 'SAR', 'SEK', 'TRY', 'UAH', 'USD'];
|
||||
const DEFAULT_CURRENCY = 'EUR';
|
||||
const DEFAULT_APP_NAME = 'Oikos';
|
||||
|
||||
const VALID_WIDGET_IDS = ['weather', 'tasks', 'calendar', 'shopping', 'meals', 'notes'];
|
||||
const VALID_DATE_FORMATS = ['mdy', 'dmy', 'ymd'];
|
||||
const DEFAULT_DATE_FORMAT = 'mdy';
|
||||
|
||||
const VALID_WIDGET_IDS = ['tasks', 'calendar', 'birthdays', 'budget', 'family', 'weather', 'shopping', 'meals', 'notes'];
|
||||
const DEFAULT_WIDGET_CONFIG = JSON.stringify(VALID_WIDGET_IDS.map((id) => ({ id, visible: true })));
|
||||
|
||||
// --------------------------------------------------------
|
||||
@@ -39,6 +44,10 @@ function cfgSet(key, value) {
|
||||
`).run(key, value);
|
||||
}
|
||||
|
||||
function cfgDelete(key) {
|
||||
db.get().prepare('DELETE FROM sync_config WHERE key = ?').run(key);
|
||||
}
|
||||
|
||||
// --------------------------------------------------------
|
||||
// Widget-Hilfsfunktionen
|
||||
// --------------------------------------------------------
|
||||
@@ -78,12 +87,16 @@ router.get('/', (req, res) => {
|
||||
const raw = cfgGet('visible_meal_types') ?? DEFAULT_MEAL_TYPES;
|
||||
const visibleMealTypes = raw.split(',').filter((t) => VALID_MEAL_TYPES.includes(t));
|
||||
const currency = cfgGet('currency') ?? DEFAULT_CURRENCY;
|
||||
const dateFormat = VALID_DATE_FORMATS.includes(cfgGet('date_format')) ? cfgGet('date_format') : DEFAULT_DATE_FORMAT;
|
||||
const appName = cfgGet('app_name') ?? DEFAULT_APP_NAME;
|
||||
const dashboardWidgets = parseWidgetConfig(cfgGet('dashboard_widgets'));
|
||||
|
||||
res.json({
|
||||
data: {
|
||||
visible_meal_types: visibleMealTypes,
|
||||
currency,
|
||||
date_format: dateFormat,
|
||||
app_name: appName,
|
||||
dashboard_widgets: dashboardWidgets,
|
||||
},
|
||||
});
|
||||
@@ -102,7 +115,7 @@ router.get('/', (req, res) => {
|
||||
|
||||
router.put('/', (req, res) => {
|
||||
try {
|
||||
const { visible_meal_types, currency, dashboard_widgets } = req.body;
|
||||
const { visible_meal_types, currency, date_format, app_name, dashboard_widgets } = req.body;
|
||||
|
||||
if (visible_meal_types !== undefined) {
|
||||
if (!Array.isArray(visible_meal_types)) {
|
||||
@@ -122,6 +135,20 @@ router.put('/', (req, res) => {
|
||||
cfgSet('currency', currency);
|
||||
}
|
||||
|
||||
if (date_format !== undefined) {
|
||||
if (!VALID_DATE_FORMATS.includes(date_format)) {
|
||||
return res.status(400).json({ error: `Ungültiges Datumsformat. Erlaubt: ${VALID_DATE_FORMATS.join(', ')}`, code: 400 });
|
||||
}
|
||||
cfgSet('date_format', date_format);
|
||||
}
|
||||
|
||||
if (app_name !== undefined) {
|
||||
const vAppName = str(app_name, 'Application name', { max: MAX_SHORT, required: false });
|
||||
if (vAppName.error) return res.status(400).json({ error: vAppName.error, code: 400 });
|
||||
if (vAppName.value) cfgSet('app_name', vAppName.value);
|
||||
else cfgDelete('app_name');
|
||||
}
|
||||
|
||||
if (dashboard_widgets !== undefined) {
|
||||
if (!Array.isArray(dashboard_widgets)) {
|
||||
return res.status(400).json({ error: 'dashboard_widgets muss ein Array sein', code: 400 });
|
||||
@@ -133,12 +160,16 @@ router.put('/', (req, res) => {
|
||||
const rawMealTypes = cfgGet('visible_meal_types') ?? DEFAULT_MEAL_TYPES;
|
||||
const savedMealTypes = rawMealTypes.split(',').filter((t) => VALID_MEAL_TYPES.includes(t));
|
||||
const savedCurrency = cfgGet('currency') ?? DEFAULT_CURRENCY;
|
||||
const savedDateFormat = VALID_DATE_FORMATS.includes(cfgGet('date_format')) ? cfgGet('date_format') : DEFAULT_DATE_FORMAT;
|
||||
const savedAppName = cfgGet('app_name') ?? DEFAULT_APP_NAME;
|
||||
const savedWidgets = parseWidgetConfig(cfgGet('dashboard_widgets'));
|
||||
|
||||
res.json({
|
||||
data: {
|
||||
visible_meal_types: savedMealTypes,
|
||||
currency: savedCurrency,
|
||||
date_format: savedDateFormat,
|
||||
app_name: savedAppName,
|
||||
dashboard_widgets: savedWidgets,
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user