Kanban Board: neues Tool in Werkzeuge (Spalten konfigurierbar, Drag&Drop + Pfeile, Prioritäten)

This commit is contained in:
2026-06-24 01:10:41 +02:00
parent 1d97791237
commit c72ddc6b39
4 changed files with 637 additions and 1 deletions

View File

@@ -0,0 +1,171 @@
const express = require('express');
const db = require('../../db');
const { authenticate } = require('../../middleware/auth');
const router = express.Router();
// ── DB-Migration ──────────────────────────────────────────────────────────────
(function migrate() {
db.exec(`
CREATE TABLE IF NOT EXISTS kanban_columns (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
title TEXT NOT NULL,
position INTEGER NOT NULL DEFAULT 0,
created_at TEXT NOT NULL DEFAULT (datetime('now','localtime'))
);
CREATE TABLE IF NOT EXISTS kanban_cards (
id INTEGER PRIMARY KEY AUTOINCREMENT,
column_id INTEGER NOT NULL REFERENCES kanban_columns(id) ON DELETE CASCADE,
user_id INTEGER NOT NULL,
title TEXT NOT NULL,
description TEXT NOT NULL DEFAULT '',
priority TEXT NOT NULL DEFAULT 'none',
position INTEGER NOT NULL DEFAULT 0,
created_at TEXT NOT NULL DEFAULT (datetime('now','localtime')),
updated_at TEXT NOT NULL DEFAULT (datetime('now','localtime'))
);
`);
})();
const uid = req => req.user.id;
// ── Alle Spalten + Karten laden ───────────────────────────────────────────────
// Feste Routen vor :param-Routen (Lessons Learned)
router.get('/board', authenticate, (req, res) => {
const me = uid(req);
const columns = db.prepare(
'SELECT * FROM kanban_columns WHERE user_id=? ORDER BY position ASC, id ASC'
).all(me);
const cards = db.prepare(
'SELECT * FROM kanban_cards WHERE user_id=? ORDER BY position ASC, id ASC'
).all(me);
// Karten in Spalten einsortieren
const colMap = {};
for (const c of columns) { c.cards = []; colMap[c.id] = c; }
for (const card of cards) {
if (colMap[card.column_id]) colMap[card.column_id].cards.push(card);
}
res.json({ columns });
});
// ── Spalte erstellen ──────────────────────────────────────────────────────────
router.post('/columns', authenticate, (req, res) => {
const { title } = req.body;
if (!title?.trim()) return res.status(400).json({ error: 'Titel fehlt' });
const me = uid(req);
const maxPos = db.prepare('SELECT MAX(position) as m FROM kanban_columns WHERE user_id=?').get(me);
const pos = (maxPos?.m ?? -1) + 1;
const r = db.prepare(
"INSERT INTO kanban_columns (user_id,title,position) VALUES (?,?,?)"
).run(me, title.trim(), pos);
res.json(db.prepare('SELECT * FROM kanban_columns WHERE id=?').get(r.lastInsertRowid));
});
// ── Spalte umbenennen ─────────────────────────────────────────────────────────
router.patch('/columns/:id', authenticate, (req, res) => {
const { title } = req.body;
if (!title?.trim()) return res.status(400).json({ error: 'Titel fehlt' });
const me = uid(req);
const col = db.prepare('SELECT * FROM kanban_columns WHERE id=? AND user_id=?').get(req.params.id, me);
if (!col) return res.status(404).json({ error: 'Spalte nicht gefunden' });
db.prepare('UPDATE kanban_columns SET title=? WHERE id=?').run(title.trim(), col.id);
res.json({ ok: true });
});
// ── Spalte löschen (inkl. aller Karten via CASCADE) ──────────────────────────
router.delete('/columns/:id', authenticate, (req, res) => {
const me = uid(req);
const col = db.prepare('SELECT * FROM kanban_columns WHERE id=? AND user_id=?').get(req.params.id, me);
if (!col) return res.status(404).json({ error: 'Spalte nicht gefunden' });
db.prepare('DELETE FROM kanban_columns WHERE id=?').run(col.id);
res.json({ ok: true });
});
// ── Spalten-Reihenfolge speichern ─────────────────────────────────────────────
router.post('/columns/reorder', authenticate, (req, res) => {
const { order } = req.body; // Array von column-IDs in neuer Reihenfolge
if (!Array.isArray(order)) return res.status(400).json({ error: 'order fehlt' });
const me = uid(req);
const upd = db.prepare('UPDATE kanban_columns SET position=? WHERE id=? AND user_id=?');
const tx = db.transaction(() => {
for (let i = 0; i < order.length; i++) upd.run(i, order[i], me);
});
tx();
res.json({ ok: true });
});
// ── Karte erstellen ───────────────────────────────────────────────────────────
router.post('/cards', authenticate, (req, res) => {
const { column_id, title, description = '', priority = 'none' } = req.body;
if (!column_id || !title?.trim()) return res.status(400).json({ error: 'column_id und title erforderlich' });
const me = uid(req);
const col = db.prepare('SELECT * FROM kanban_columns WHERE id=? AND user_id=?').get(column_id, me);
if (!col) return res.status(404).json({ error: 'Spalte nicht gefunden' });
const maxPos = db.prepare('SELECT MAX(position) as m FROM kanban_cards WHERE column_id=?').get(column_id);
const pos = (maxPos?.m ?? -1) + 1;
const r = db.prepare(
"INSERT INTO kanban_cards (column_id,user_id,title,description,priority,position) VALUES (?,?,?,?,?,?)"
).run(column_id, me, title.trim(), description.trim(), priority, pos);
res.json(db.prepare('SELECT * FROM kanban_cards WHERE id=?').get(r.lastInsertRowid));
});
// ── Karte bearbeiten ──────────────────────────────────────────────────────────
router.patch('/cards/:id', authenticate, (req, res) => {
const me = uid(req);
const card = db.prepare('SELECT * FROM kanban_cards WHERE id=? AND user_id=?').get(req.params.id, me);
if (!card) return res.status(404).json({ error: 'Karte nicht gefunden' });
const title = req.body.title !== undefined ? req.body.title.trim() : card.title;
const description = req.body.description !== undefined ? req.body.description.trim() : card.description;
const priority = req.body.priority !== undefined ? req.body.priority : card.priority;
if (!title) return res.status(400).json({ error: 'Titel darf nicht leer sein' });
db.prepare(
"UPDATE kanban_cards SET title=?,description=?,priority=?,updated_at=datetime('now','localtime') WHERE id=?"
).run(title, description, priority, card.id);
res.json(db.prepare('SELECT * FROM kanban_cards WHERE id=?').get(card.id));
});
// ── Karte löschen ─────────────────────────────────────────────────────────────
router.delete('/cards/:id', authenticate, (req, res) => {
const me = uid(req);
const card = db.prepare('SELECT * FROM kanban_cards WHERE id=? AND user_id=?').get(req.params.id, me);
if (!card) return res.status(404).json({ error: 'Karte nicht gefunden' });
db.prepare('DELETE FROM kanban_cards WHERE id=?').run(card.id);
res.json({ ok: true });
});
// ── Karte verschieben (Spalte + Position) ─────────────────────────────────────
// Wird genutzt für: Drag&Drop zwischen Spalten, Pfeil-Buttons, Position hoch/runter
router.post('/cards/:id/move', authenticate, (req, res) => {
const { column_id, position } = req.body;
const me = uid(req);
const card = db.prepare('SELECT * FROM kanban_cards WHERE id=? AND user_id=?').get(req.params.id, me);
if (!card) return res.status(404).json({ error: 'Karte nicht gefunden' });
const targetColId = column_id !== undefined ? column_id : card.column_id;
// Zielspalte gehört dem User?
const col = db.prepare('SELECT * FROM kanban_columns WHERE id=? AND user_id=?').get(targetColId, me);
if (!col) return res.status(404).json({ error: 'Zielspalte nicht gefunden' });
// Alle Karten der Zielspalte (ohne die verschobene), dann an position einfügen
const siblings = db.prepare(
'SELECT id FROM kanban_cards WHERE column_id=? AND id!=? ORDER BY position ASC, id ASC'
).all(targetColId, card.id).map(r => r.id);
const targetPos = position !== undefined
? Math.max(0, Math.min(position, siblings.length))
: siblings.length; // ans Ende wenn keine position angegeben
siblings.splice(targetPos, 0, card.id);
const upd = db.prepare('UPDATE kanban_cards SET column_id=?, position=? WHERE id=?');
const tx = db.transaction(() => {
for (let i = 0; i < siblings.length; i++) {
upd.run(targetColId, i, siblings[i]);
}
});
tx();
res.json({ ok: true });
});
module.exports = router;