Files
oikos/server/routes/notes.js
T
ulsklyc 74b6e5f078 feat: Phase 3 Schritte 16–18 — Pinnwand, Kontakte, Budget-Tracker
Pinnwand (Notes):
- server/routes/notes.js: GET (sortiert: angeheftet zuerst), POST, PUT, PATCH /pin, DELETE
- public/pages/notes.js: Masonry-Grid, Markdown-Light-Renderer (fett/kursiv/Liste),
  Farb-Auswahl (8 Farben), helle/dunkle Textfarbe je nach Hintergrund, Pin-Toggle
- public/styles/notes.css: Masonry-Layout, Sticky-Note-Karten, Hover-Aktionen

Kontakte:
- server/routes/contacts.js: GET (Kategorie- + Volltextfilter), POST, PUT, DELETE, GET /meta
- public/pages/contacts.js: Kategorie-Filter-Chips, Echtzeit-Suche, Gruppenansicht,
  tel:/mailto:/maps-Links, CRUD-Modal
- public/styles/contacts.css: Toolbar mit Suche, Filter-Chips, Kontaktliste, Aktions-Buttons

Budget-Tracker:
- server/routes/budget.js: GET (Monatfilter), GET /summary (Einnahmen/Ausgaben/Saldo +
  Aufschlüsselung), GET /export (CSV mit BOM), POST, PUT, DELETE, GET /meta
- public/pages/budget.js: Monatsnavigation, 3 Zusammenfassungs-Karten, Kategorie-Balken
  (reines CSS, kein Canvas), Transaktionsliste, Einnahme/Ausgabe-Toggle, CSV-Download
- public/styles/budget.css: Summary-Cards, Balkendiagramm, Transaktionsliste, Modal

Tests: 34 neue Tests (10 Notes + 9 Contacts + 15 Budget), gesamt 146/146

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-24 21:24:08 +01:00

151 lines
4.5 KiB
JavaScript

/**
* Modul: Pinnwand / Notizen (Notes)
* Zweck: REST-API-Routen für Notizen (CRUD, Pin-Toggle)
* Abhängigkeiten: express, server/db.js, server/auth.js
*/
'use strict';
const express = require('express');
const router = express.Router();
const db = require('../db');
const COLOR_RE = /^#[0-9A-Fa-f]{6}$/;
/**
* GET /api/v1/notes
* Alle Notizen, angepinnte zuerst, dann nach updated_at DESC.
* Response: { data: Note[] }
*/
router.get('/', (req, res) => {
try {
const notes = db.get().prepare(`
SELECT n.*, u.display_name AS creator_name, u.avatar_color AS creator_color
FROM notes n
LEFT JOIN users u ON u.id = n.created_by
ORDER BY n.pinned DESC, n.updated_at DESC
`).all();
res.json({ data: notes });
} catch (err) {
console.error('[notes/GET /]', err);
res.status(500).json({ error: 'Interner Fehler', code: 500 });
}
});
/**
* POST /api/v1/notes
* Neue Notiz anlegen.
* Body: { content, title?, color?, pinned? }
* Response: { data: Note }
*/
router.post('/', (req, res) => {
try {
const { content, title = null, color = '#FFEB3B', pinned = 0 } = req.body;
if (!content || !content.trim())
return res.status(400).json({ error: 'Inhalt ist erforderlich', code: 400 });
if (!COLOR_RE.test(color))
return res.status(400).json({ error: 'Farbe muss #RRGGBB sein', code: 400 });
const result = db.get().prepare(`
INSERT INTO notes (content, title, color, pinned, created_by)
VALUES (?, ?, ?, ?, ?)
`).run(content.trim(), title?.trim() || null, color, pinned ? 1 : 0, req.session.userId);
const note = db.get().prepare(`
SELECT n.*, u.display_name AS creator_name, u.avatar_color AS creator_color
FROM notes n LEFT JOIN users u ON u.id = n.created_by
WHERE n.id = ?
`).get(result.lastInsertRowid);
res.status(201).json({ data: note });
} catch (err) {
console.error('[notes/POST /]', err);
res.status(500).json({ error: 'Interner Fehler', code: 500 });
}
});
/**
* PUT /api/v1/notes/:id
* Notiz bearbeiten.
* Body: { content?, title?, color?, pinned? }
* Response: { data: Note }
*/
router.put('/:id', (req, res) => {
try {
const id = parseInt(req.params.id, 10);
const note = db.get().prepare('SELECT * FROM notes WHERE id = ?').get(id);
if (!note) return res.status(404).json({ error: 'Notiz nicht gefunden', code: 404 });
const { content, title, color, pinned } = req.body;
if (color !== undefined && !COLOR_RE.test(color))
return res.status(400).json({ error: 'Farbe muss #RRGGBB sein', code: 400 });
db.get().prepare(`
UPDATE notes
SET content = COALESCE(?, content),
title = ?,
color = COALESCE(?, color),
pinned = COALESCE(?, pinned)
WHERE id = ?
`).run(
content?.trim() ?? null,
title !== undefined ? (title?.trim() || null) : note.title,
color ?? null,
pinned !== undefined ? (pinned ? 1 : 0) : null,
id
);
const updated = db.get().prepare(`
SELECT n.*, u.display_name AS creator_name, u.avatar_color AS creator_color
FROM notes n LEFT JOIN users u ON u.id = n.created_by WHERE n.id = ?
`).get(id);
res.json({ data: updated });
} catch (err) {
console.error('[notes/PUT /:id]', err);
res.status(500).json({ error: 'Interner Fehler', code: 500 });
}
});
/**
* PATCH /api/v1/notes/:id/pin
* Pin-Status toggeln.
* Response: { data: { id, pinned } }
*/
router.patch('/:id/pin', (req, res) => {
try {
const id = parseInt(req.params.id, 10);
const note = db.get().prepare('SELECT pinned FROM notes WHERE id = ?').get(id);
if (!note) return res.status(404).json({ error: 'Notiz nicht gefunden', code: 404 });
const newPinned = note.pinned ? 0 : 1;
db.get().prepare('UPDATE notes SET pinned = ? WHERE id = ?').run(newPinned, id);
res.json({ data: { id, pinned: newPinned } });
} catch (err) {
console.error('[notes/PATCH /:id/pin]', err);
res.status(500).json({ error: 'Interner Fehler', code: 500 });
}
});
/**
* DELETE /api/v1/notes/:id
* Notiz löschen.
* Response: 204 No Content
*/
router.delete('/:id', (req, res) => {
try {
const id = parseInt(req.params.id, 10);
const result = db.get().prepare('DELETE FROM notes WHERE id = ?').run(id);
if (result.changes === 0)
return res.status(404).json({ error: 'Notiz nicht gefunden', code: 404 });
res.status(204).end();
} catch (err) {
console.error('[notes/DELETE /:id]', err);
res.status(500).json({ error: 'Interner Fehler', code: 500 });
}
});
module.exports = router;