212a8bdb0a
The dashboard meal widget was showing all meal types regardless of the household meal visibility settings configured in Settings > Meal Plan. Root cause: the todayMeals SQL query in dashboard.js did not read visible_meal_types from sync_config. The Meals page applied this filter client-side, but the dashboard API returned unfiltered data. Fix: read visible_meal_types from sync_config before the query and inject the active types as IN (?) placeholders. Falls back to all four types when no preference is stored.
166 lines
5.0 KiB
JavaScript
166 lines
5.0 KiB
JavaScript
/**
|
|
* Modul: Dashboard
|
|
* Zweck: Aggregierter Endpoint - liefert Daten aller Dashboard-Widgets in einem Request
|
|
* Abhängigkeiten: express, server/db.js
|
|
*/
|
|
|
|
import { createLogger } from '../logger.js';
|
|
import express from 'express';
|
|
import * as db from '../db.js';
|
|
|
|
const log = createLogger('Dashboard');
|
|
|
|
const router = express.Router();
|
|
|
|
/**
|
|
* GET /api/v1/dashboard
|
|
* Liefert aggregierte Daten für alle Dashboard-Widgets.
|
|
* Jedes Widget-Objekt hat ein eigenes `error`-Feld falls die Abfrage fehlschlägt -
|
|
* so bricht ein fehlerhaftes Widget nicht das gesamte Dashboard.
|
|
*
|
|
* Response: {
|
|
* upcomingEvents: CalendarEvent[], // Nächste 5 Termine
|
|
* urgentTasks: Task[], // High/Urgent mit Fälligkeit ≤ 48h
|
|
* todayMeals: Meal[], // Mahlzeiten für heute
|
|
* pinnedNotes: Note[], // Angepinnte Notizen (max. 3)
|
|
* users: User[] // Alle User (für Avatar-Farben)
|
|
* }
|
|
*/
|
|
router.get('/', (req, res) => {
|
|
try {
|
|
const d = db.get();
|
|
const result = {};
|
|
|
|
// Heute und +48h als ISO-Strings
|
|
const now = new Date();
|
|
const todayStr = now.toISOString().slice(0, 10);
|
|
const deadline48h = new Date(now.getTime() + 48 * 60 * 60 * 1000).toISOString();
|
|
|
|
// Anstehende Termine (nächste 5, ab jetzt)
|
|
try {
|
|
result.upcomingEvents = d.prepare(`
|
|
SELECT
|
|
ce.*,
|
|
u.display_name AS assigned_name,
|
|
u.avatar_color AS assigned_color
|
|
FROM calendar_events ce
|
|
LEFT JOIN users u ON ce.assigned_to = u.id
|
|
WHERE ce.start_datetime >= ?
|
|
ORDER BY ce.start_datetime ASC
|
|
LIMIT 5
|
|
`).all(now.toISOString());
|
|
} catch (err) {
|
|
log.error('upcomingEvents-Fehler:', err.message);
|
|
result.upcomingEvents = [];
|
|
}
|
|
|
|
// Offene Aufgaben: alle nicht-erledigten, sortiert nach Priorität und Fälligkeit
|
|
try {
|
|
result.urgentTasks = d.prepare(`
|
|
SELECT
|
|
t.*,
|
|
u.display_name AS assigned_name,
|
|
u.avatar_color AS assigned_color
|
|
FROM tasks t
|
|
LEFT JOIN users u ON t.assigned_to = u.id
|
|
WHERE t.status != 'done'
|
|
ORDER BY
|
|
CASE t.priority
|
|
WHEN 'urgent' THEN 0
|
|
WHEN 'high' THEN 1
|
|
WHEN 'medium' THEN 2
|
|
WHEN 'low' THEN 3
|
|
ELSE 4
|
|
END,
|
|
t.due_date ASC NULLS LAST
|
|
LIMIT 5
|
|
`).all();
|
|
} catch (err) {
|
|
log.error('urgentTasks-Fehler:', err.message);
|
|
result.urgentTasks = [];
|
|
}
|
|
|
|
// Heutiges Essen (gefiltert nach haushaltweiten Mahlzeit-Typ-Einstellungen)
|
|
try {
|
|
const ALL_MEAL_TYPES = ['breakfast', 'lunch', 'dinner', 'snack'];
|
|
const prefRow = d.prepare('SELECT value FROM sync_config WHERE key = ?').get('visible_meal_types');
|
|
const visibleTypes = prefRow
|
|
? prefRow.value.split(',').filter((t) => ALL_MEAL_TYPES.includes(t))
|
|
: ALL_MEAL_TYPES;
|
|
const placeholders = visibleTypes.map(() => '?').join(', ');
|
|
result.todayMeals = d.prepare(`
|
|
SELECT * FROM meals
|
|
WHERE date = ?
|
|
AND meal_type IN (${placeholders})
|
|
ORDER BY
|
|
CASE meal_type
|
|
WHEN 'breakfast' THEN 0
|
|
WHEN 'lunch' THEN 1
|
|
WHEN 'dinner' THEN 2
|
|
WHEN 'snack' THEN 3
|
|
END
|
|
`).all(todayStr, ...visibleTypes);
|
|
} catch (err) {
|
|
log.error('todayMeals-Fehler:', err.message);
|
|
result.todayMeals = [];
|
|
}
|
|
|
|
// Neueste Notizen (gepinnte zuerst, dann aktuellste)
|
|
try {
|
|
result.pinnedNotes = d.prepare(`
|
|
SELECT n.*, u.display_name AS author_name, u.avatar_color AS author_color
|
|
FROM notes n
|
|
LEFT JOIN users u ON n.created_by = u.id
|
|
ORDER BY n.pinned DESC, n.updated_at DESC
|
|
LIMIT 3
|
|
`).all();
|
|
} catch (err) {
|
|
log.error('pinnedNotes-Fehler:', err.message);
|
|
result.pinnedNotes = [];
|
|
}
|
|
|
|
// Einkaufslisten mit offenen Artikeln (max. 3 Listen, je bis zu 6 offene Items)
|
|
try {
|
|
const lists = d.prepare(`
|
|
SELECT sl.id, sl.name,
|
|
(SELECT COUNT(*) FROM shopping_items si WHERE si.list_id = sl.id AND si.is_checked = 0) AS open_count,
|
|
(SELECT COUNT(*) FROM shopping_items si WHERE si.list_id = sl.id) AS total_count
|
|
FROM shopping_lists sl
|
|
HAVING open_count > 0
|
|
ORDER BY sl.updated_at DESC
|
|
LIMIT 3
|
|
`).all();
|
|
|
|
for (const list of lists) {
|
|
list.items = d.prepare(`
|
|
SELECT id, name, quantity, is_checked
|
|
FROM shopping_items
|
|
WHERE list_id = ? AND is_checked = 0
|
|
ORDER BY id ASC
|
|
LIMIT 6
|
|
`).all(list.id);
|
|
}
|
|
result.shoppingLists = lists;
|
|
} catch (err) {
|
|
log.error('shoppingLists-Fehler:', err.message);
|
|
result.shoppingLists = [];
|
|
}
|
|
|
|
// Alle User (für Avatar-Farben in Widgets)
|
|
try {
|
|
result.users = d.prepare(
|
|
'SELECT id, display_name, avatar_color FROM users ORDER BY display_name'
|
|
).all();
|
|
} catch (err) {
|
|
result.users = [];
|
|
}
|
|
|
|
res.json(result);
|
|
} catch (err) {
|
|
log.error('Kritischer Fehler:', err.message);
|
|
res.status(500).json({ error: 'Dashboard konnte nicht geladen werden.', code: 500 });
|
|
}
|
|
});
|
|
|
|
export default router;
|