feat: Phase 2 Schritt 8 — Dashboard mit allen Widgets
- Aggregierter GET /api/v1/dashboard Endpoint (1 Request für alle Widgets) - Widget: Begrüßung mit tageszeit-abhängigem Text + aktuellem Datum - Widget: Dringende Aufgaben (priority high/urgent, fällig ≤ 48h, nicht done) - Widget: Anstehende Termine (nächste 5, mit Avatar-Farbe) - Widget: Heutiges Essen (nach Mahlzeit-Typ sortiert) - Widget: Angepinnte Notizen (max. 3, mit Notizfarbe) - Skeleton-Loading-States während API-Call (keine Spinner) - FAB Speed-Dial: + Aufgabe, + Termin, + Einkauf, + Notiz - Responsives 1/2/3-Spalten-Grid (Mobil / Tablet / Desktop) - Dashboard-Tests: 8/8 bestanden (node:sqlite) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,200 @@
|
||||
/**
|
||||
* Modul: Dashboard-API-Test
|
||||
* Zweck: Validiert die Dashboard-Aggregationsabfragen mit node:sqlite
|
||||
* Ausführen: node --experimental-sqlite test-dashboard.js
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const { DatabaseSync } = require('node:sqlite');
|
||||
const { MIGRATIONS_SQL } = require('./server/db-schema-test');
|
||||
|
||||
let passed = 0;
|
||||
let failed = 0;
|
||||
|
||||
function test(name, fn) {
|
||||
try {
|
||||
fn();
|
||||
console.log(` ✓ ${name}`);
|
||||
passed++;
|
||||
} catch (err) {
|
||||
console.error(` ✗ ${name}: ${err.message}`);
|
||||
failed++;
|
||||
}
|
||||
}
|
||||
|
||||
function assert(condition, msg) {
|
||||
if (!condition) throw new Error(msg || 'Assertion fehlgeschlagen');
|
||||
}
|
||||
|
||||
// --------------------------------------------------------
|
||||
// DB aufbauen
|
||||
// --------------------------------------------------------
|
||||
const db = new DatabaseSync(':memory:');
|
||||
db.exec('PRAGMA foreign_keys = ON;');
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS schema_migrations (
|
||||
version INTEGER PRIMARY KEY, description TEXT NOT NULL,
|
||||
applied_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now'))
|
||||
);
|
||||
`);
|
||||
db.exec(MIGRATIONS_SQL[1]);
|
||||
|
||||
// Testdaten einfügen
|
||||
const u1 = db.prepare(`INSERT INTO users (username, display_name, password_hash, avatar_color, role)
|
||||
VALUES ('admin', 'Anna Admin', 'x', '#007AFF', 'admin')`).run();
|
||||
const u2 = db.prepare(`INSERT INTO users (username, display_name, password_hash, avatar_color)
|
||||
VALUES ('max', 'Max Muster', 'x', '#34C759')`).run();
|
||||
|
||||
const uid1 = u1.lastInsertRowid;
|
||||
const uid2 = u2.lastInsertRowid;
|
||||
|
||||
const today = new Date().toISOString().slice(0, 10);
|
||||
const tomorrow = new Date(Date.now() + 86400000).toISOString().slice(0, 10);
|
||||
const inOneHour = new Date(Date.now() + 3600000).toISOString();
|
||||
const in30h = new Date(Date.now() + 30 * 3600000).toISOString().slice(0, 10);
|
||||
const in72h = new Date(Date.now() + 72 * 3600000).toISOString().slice(0, 10);
|
||||
|
||||
// Aufgaben
|
||||
db.prepare(`INSERT INTO tasks (title, priority, status, due_date, created_by, assigned_to)
|
||||
VALUES ('Urgent Task', 'urgent', 'open', ?, ?, ?)`).run(today, uid1, uid2);
|
||||
db.prepare(`INSERT INTO tasks (title, priority, status, due_date, created_by)
|
||||
VALUES ('High Task morgen', 'high', 'open', ?, ?)`).run(tomorrow, uid1);
|
||||
db.prepare(`INSERT INTO tasks (title, priority, status, due_date, created_by)
|
||||
VALUES ('High Task in 3 Tagen', 'high', 'open', ?, ?)`).run(in72h, uid1);
|
||||
db.prepare(`INSERT INTO tasks (title, priority, status, due_date, created_by)
|
||||
VALUES ('Done Task', 'urgent', 'done', ?, ?)`).run(today, uid1);
|
||||
|
||||
// Kalender-Events
|
||||
db.prepare(`INSERT INTO calendar_events (title, start_datetime, created_by, assigned_to, color)
|
||||
VALUES ('Morgen-Meeting', ?, ?, ?, '#007AFF')`).run(inOneHour, uid1, uid2);
|
||||
db.prepare(`INSERT INTO calendar_events (title, start_datetime, created_by)
|
||||
VALUES ('Event in 3 Tagen', ?, ?)`).run(in72h + 'T10:00:00Z', uid1);
|
||||
|
||||
// Mahlzeiten
|
||||
db.prepare(`INSERT INTO meals (date, meal_type, title, created_by)
|
||||
VALUES (?, 'breakfast', 'Haferbrei', ?)`).run(today, uid1);
|
||||
db.prepare(`INSERT INTO meals (date, meal_type, title, created_by)
|
||||
VALUES (?, 'dinner', 'Pasta', ?)`).run(today, uid1);
|
||||
db.prepare(`INSERT INTO meals (date, meal_type, title, created_by)
|
||||
VALUES (?, 'lunch', 'Salat morgen', ?)`).run(tomorrow, uid1);
|
||||
|
||||
// Notizen
|
||||
db.prepare(`INSERT INTO notes (content, title, pinned, color, created_by)
|
||||
VALUES ('Wichtige Info', 'Pinnwand-Notiz', 1, '#FFEB3B', ?)`).run(uid1);
|
||||
db.prepare(`INSERT INTO notes (content, pinned, color, created_by)
|
||||
VALUES ('Nicht angepinnt', 0, '#E3F2FF', ?)`).run(uid1);
|
||||
|
||||
console.log('\n[Dashboard-Test] API-Abfragen\n');
|
||||
|
||||
// --------------------------------------------------------
|
||||
// Tests: Dringende Aufgaben
|
||||
// --------------------------------------------------------
|
||||
const deadline48h = new Date(Date.now() + 48 * 3600000).toISOString().slice(0, 10);
|
||||
|
||||
test('Dringende Aufgaben: nur high/urgent mit Fälligkeit ≤ 48h und nicht done', () => {
|
||||
const tasks = db.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.priority IN ('high', 'urgent')
|
||||
AND t.status != 'done'
|
||||
AND (t.due_date IS NULL OR t.due_date <= ?)
|
||||
ORDER BY CASE t.priority WHEN 'urgent' THEN 0 ELSE 1 END, t.due_date ASC
|
||||
LIMIT 10
|
||||
`).all(deadline48h);
|
||||
|
||||
assert(tasks.length === 2, `Erwartet 2 Aufgaben, erhalten ${tasks.length}`);
|
||||
assert(tasks[0].priority === 'urgent', 'Urgent zuerst');
|
||||
assert(tasks[0].assigned_name === 'Max Muster', 'assigned_name korrekt');
|
||||
assert(tasks[0].assigned_color === '#34C759', 'assigned_color korrekt');
|
||||
});
|
||||
|
||||
test('Dringende Aufgaben: erledigte Aufgaben werden nicht angezeigt', () => {
|
||||
const tasks = db.prepare(`
|
||||
SELECT * FROM tasks
|
||||
WHERE priority IN ('high', 'urgent') AND status != 'done' AND due_date <= ?
|
||||
`).all(deadline48h);
|
||||
const doneTask = tasks.find((t) => t.title === 'Done Task');
|
||||
assert(!doneTask, 'Erledigte Aufgaben sollten gefiltert sein');
|
||||
});
|
||||
|
||||
test('Dringende Aufgaben: Task mit Fälligkeit in 3 Tagen wird ausgeschlossen', () => {
|
||||
const tasks = db.prepare(`
|
||||
SELECT * FROM tasks
|
||||
WHERE priority IN ('high', 'urgent') AND status != 'done' AND due_date <= ?
|
||||
`).all(deadline48h);
|
||||
const farTask = tasks.find((t) => t.title === 'High Task in 3 Tagen');
|
||||
assert(!farTask, 'Aufgabe in 72h sollte nicht erscheinen');
|
||||
});
|
||||
|
||||
// --------------------------------------------------------
|
||||
// Tests: Anstehende Termine
|
||||
// --------------------------------------------------------
|
||||
test('Anstehende Termine: zukünftige Events, sortiert, max 5', () => {
|
||||
const now = new Date().toISOString();
|
||||
const events = db.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);
|
||||
|
||||
assert(events.length === 2, `Erwartet 2 Events, erhalten ${events.length}`);
|
||||
assert(events[0].title === 'Morgen-Meeting', 'Erstes Event ist das nächste');
|
||||
assert(events[0].assigned_color === '#34C759', 'assigned_color vom Join');
|
||||
});
|
||||
|
||||
// --------------------------------------------------------
|
||||
// Tests: Heutige Mahlzeiten
|
||||
// --------------------------------------------------------
|
||||
test('Heutige Mahlzeiten: nur heute, in korrekter Reihenfolge', () => {
|
||||
const meals = db.prepare(`
|
||||
SELECT * FROM meals WHERE date = ?
|
||||
ORDER BY CASE meal_type
|
||||
WHEN 'breakfast' THEN 0 WHEN 'lunch' THEN 1
|
||||
WHEN 'dinner' THEN 2 WHEN 'snack' THEN 3 END
|
||||
`).all(today);
|
||||
|
||||
assert(meals.length === 2, `Erwartet 2 Mahlzeiten, erhalten ${meals.length}`);
|
||||
assert(meals[0].meal_type === 'breakfast', 'Frühstück zuerst');
|
||||
assert(meals[1].meal_type === 'dinner', 'Abendessen danach');
|
||||
});
|
||||
|
||||
test('Heutige Mahlzeiten: morgige Mahlzeit nicht enthalten', () => {
|
||||
const meals = db.prepare(`SELECT * FROM meals WHERE date = ?`).all(today);
|
||||
const wrongMeal = meals.find((m) => m.title === 'Salat morgen');
|
||||
assert(!wrongMeal, 'Morgige Mahlzeit sollte nicht erscheinen');
|
||||
});
|
||||
|
||||
// --------------------------------------------------------
|
||||
// Tests: Angepinnte Notizen
|
||||
// --------------------------------------------------------
|
||||
test('Angepinnte Notizen: nur pinned=1, max 3', () => {
|
||||
const notes = db.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
|
||||
WHERE n.pinned = 1
|
||||
ORDER BY n.updated_at DESC
|
||||
LIMIT 3
|
||||
`).all();
|
||||
|
||||
assert(notes.length === 1, `Erwartet 1 Notiz, erhalten ${notes.length}`);
|
||||
assert(notes[0].title === 'Pinnwand-Notiz', 'Korrekte Notiz');
|
||||
assert(notes[0].author_name === 'Anna Admin', 'author_name vom Join');
|
||||
});
|
||||
|
||||
test('Angepinnte Notizen: nicht angepinnte werden ausgeschlossen', () => {
|
||||
const notes = db.prepare(`SELECT * FROM notes WHERE pinned = 1`).all();
|
||||
const unpinned = notes.find((n) => n.content === 'Nicht angepinnt');
|
||||
assert(!unpinned, 'Nicht angepinnte Notiz sollte gefiltert sein');
|
||||
});
|
||||
|
||||
// --------------------------------------------------------
|
||||
// Ergebnis
|
||||
// --------------------------------------------------------
|
||||
console.log(`\n[Dashboard-Test] Ergebnis: ${passed} bestanden, ${failed} fehlgeschlagen\n`);
|
||||
if (failed > 0) process.exit(1);
|
||||
Reference in New Issue
Block a user