/** * Modul: Haushalt-Einstellungen (Preferences) * Zweck: REST-API fuer haushaltweite Praeferenzen (via sync_config-Tabelle) * Abhängigkeiten: express, server/db.js */ 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'); const router = express.Router(); const VALID_MEAL_TYPES = ['breakfast', 'lunch', 'dinner', 'snack']; 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_DATE_FORMATS = ['mdy', 'dmy', 'ymd', 'mdy_dot', 'dmy_dot', 'ymd_dot', 'ymd_slash']; const DEFAULT_DATE_FORMAT = 'mdy'; const VALID_TIME_FORMATS = ['24h', '12h']; const DEFAULT_TIME_FORMAT = '24h'; 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 }))); // -------------------------------------------------------- // Hilfsfunktionen // -------------------------------------------------------- function cfgGet(key) { const row = db.get().prepare('SELECT value FROM sync_config WHERE key = ?').get(key); return row ? row.value : null; } function cfgSet(key, value) { db.get().prepare(` INSERT INTO sync_config (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = strftime('%Y-%m-%dT%H:%M:%SZ', 'now') `).run(key, value); } function cfgDelete(key) { db.get().prepare('DELETE FROM sync_config WHERE key = ?').run(key); } // -------------------------------------------------------- // Widget-Hilfsfunktionen // -------------------------------------------------------- function parseWidgetConfig(raw) { try { const parsed = JSON.parse(raw ?? DEFAULT_WIDGET_CONFIG); return normalizeWidgetConfig(parsed); } catch { return JSON.parse(DEFAULT_WIDGET_CONFIG); } } function normalizeWidgetConfig(input) { const valid = Array.isArray(input) ? input .filter((w) => w && typeof w === 'object' && VALID_WIDGET_IDS.includes(w.id)) .map((w) => ({ id: w.id, visible: Boolean(w.visible) })) : []; // Fehlende Widget-IDs am Ende ergänzen const presentIds = new Set(valid.map((w) => w.id)); for (const id of VALID_WIDGET_IDS) { if (!presentIds.has(id)) valid.push({ id, visible: true }); } return valid; } // -------------------------------------------------------- // GET /api/v1/preferences // Alle Haushalt-Praeferenzen lesen. // Response: { data: { visible_meal_types: string[] } } // -------------------------------------------------------- router.get('/', (req, res) => { try { 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 timeFormat = VALID_TIME_FORMATS.includes(cfgGet('time_format')) ? cfgGet('time_format') : DEFAULT_TIME_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, time_format: timeFormat, app_name: appName, dashboard_widgets: dashboardWidgets, }, }); } catch (err) { log.error('GET /', err); res.status(500).json({ error: 'Interner Fehler', code: 500 }); } }); // -------------------------------------------------------- // PUT /api/v1/preferences // Haushalt-Praeferenzen aktualisieren. // Body: { visible_meal_types: string[] } // Response: { data: { visible_meal_types: string[] } } // -------------------------------------------------------- router.put('/', (req, res) => { try { const { visible_meal_types, currency, date_format, time_format, app_name, dashboard_widgets } = req.body; if (visible_meal_types !== undefined) { if (!Array.isArray(visible_meal_types)) { return res.status(400).json({ error: 'visible_meal_types muss ein Array sein', code: 400 }); } const filtered = visible_meal_types.filter((t) => VALID_MEAL_TYPES.includes(t)); if (filtered.length === 0) { return res.status(400).json({ error: 'Mindestens ein Mahlzeit-Typ muss aktiv sein', code: 400 }); } cfgSet('visible_meal_types', filtered.join(',')); } if (currency !== undefined) { if (!VALID_CURRENCIES.includes(currency)) { return res.status(400).json({ error: `Ungültige Währung. Erlaubt: ${VALID_CURRENCIES.join(', ')}`, code: 400 }); } 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 (time_format !== undefined) { if (!VALID_TIME_FORMATS.includes(time_format)) { return res.status(400).json({ error: `Invalid time format. Allowed: ${VALID_TIME_FORMATS.join(', ')}`, code: 400 }); } cfgSet('time_format', time_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 }); } const normalized = normalizeWidgetConfig(dashboard_widgets); cfgSet('dashboard_widgets', JSON.stringify(normalized)); } 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 savedTimeFormat = VALID_TIME_FORMATS.includes(cfgGet('time_format')) ? cfgGet('time_format') : DEFAULT_TIME_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, time_format: savedTimeFormat, app_name: savedAppName, dashboard_widgets: savedWidgets, }, }); } catch (err) { log.error('PUT /', err); res.status(500).json({ error: 'Interner Fehler', code: 500 }); } }); export default router;