Kanban: Design-Fix (padding/header wie andere Tools), Spaltenfarben, Verschieben-nach im Modal

This commit is contained in:
2026-06-24 01:34:19 +02:00
parent c72ddc6b39
commit 418781f574
2 changed files with 295 additions and 280 deletions

View File

@@ -10,6 +10,7 @@ const router = express.Router();
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
title TEXT NOT NULL,
color TEXT NOT NULL DEFAULT '#4ecdc4',
position INTEGER NOT NULL DEFAULT 0,
created_at TEXT NOT NULL DEFAULT (datetime('now','localtime'))
);
@@ -25,12 +26,16 @@ const router = express.Router();
updated_at TEXT NOT NULL DEFAULT (datetime('now','localtime'))
);
`);
// Nachrüsten falls Tabelle schon existiert aber color fehlt
const cols = db.pragma('table_info(kanban_columns)').map(c => c.name);
if (!cols.includes('color')) {
db.exec("ALTER TABLE kanban_columns ADD COLUMN color TEXT NOT NULL DEFAULT '#4ecdc4'");
}
})();
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(
@@ -39,7 +44,6 @@ router.get('/board', authenticate, (req, res) => {
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) {
@@ -50,29 +54,39 @@ router.get('/board', authenticate, (req, res) => {
// ── Spalte erstellen ──────────────────────────────────────────────────────────
router.post('/columns', authenticate, (req, res) => {
const { title } = req.body;
const { title, color = '#4ecdc4' } = 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);
"INSERT INTO kanban_columns (user_id,title,color,position) VALUES (?,?,?,?)"
).run(me, title.trim(), color, 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' });
// ── Spalte aktualisieren (Titel + Farbe) ─────────────────────────────────────
// Feste Route vor :param (Lesson Learned)
router.post('/columns/reorder', authenticate, (req, res) => {
const { order } = req.body;
if (!Array.isArray(order)) return res.status(400).json({ error: 'order 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);
const upd = db.prepare('UPDATE kanban_columns SET position=? WHERE id=? AND user_id=?');
db.transaction(() => { for (let i = 0; i < order.length; i++) upd.run(i, order[i], me); })();
res.json({ ok: true });
});
router.patch('/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' });
const title = req.body.title !== undefined ? req.body.title.trim() : col.title;
const color = req.body.color !== undefined ? req.body.color : col.color;
if (!title) return res.status(400).json({ error: 'Titel fehlt' });
db.prepare('UPDATE kanban_columns SET title=?, color=? WHERE id=?').run(title, color, 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);
@@ -81,20 +95,7 @@ router.delete('/columns/:id', authenticate, (req, res) => {
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 ───────────────────────────────────────────────────────────
// ── Karten ────────────────────────────────────────────────────────────────────
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' });
@@ -109,7 +110,6 @@ router.post('/cards', authenticate, (req, res) => {
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);
@@ -124,7 +124,6 @@ router.patch('/cards/:id', authenticate, (req, res) => {
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);
@@ -133,38 +132,23 @@ router.delete('/cards/:id', authenticate, (req, res) => {
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.length;
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();
db.transaction(() => { for (let i = 0; i < siblings.length; i++) upd.run(targetColId, i, siblings[i]); })();
res.json({ ok: true });
});