Kanban Board: neues Tool in Werkzeuge (Spalten konfigurierbar, Drag&Drop + Pfeile, Prioritäten)
This commit is contained in:
171
backend/src/tools/kanban/routes.js
Normal file
171
backend/src/tools/kanban/routes.js
Normal 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;
|
||||
Reference in New Issue
Block a user