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>
This commit is contained in:
+142
-5
@@ -1,13 +1,150 @@
|
||||
/**
|
||||
* Modul: Pinnwand / Notizen (Notes)
|
||||
* Zweck: REST-API-Routen für Notizen
|
||||
* Zweck: REST-API-Routen für Notizen (CRUD, Pin-Toggle)
|
||||
* Abhängigkeiten: express, server/db.js, server/auth.js
|
||||
*/
|
||||
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
'use strict';
|
||||
|
||||
// Platzhalter — wird in Phase 3 implementiert
|
||||
router.get('/', (req, res) => res.json({ data: [] }));
|
||||
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;
|
||||
|
||||
Reference in New Issue
Block a user