From 418781f5749dc455c3a00e90e7b46a98cf951d3d Mon Sep 17 00:00:00 2001 From: Dicken Date: Wed, 24 Jun 2026 01:34:19 +0200 Subject: [PATCH] Kanban: Design-Fix (padding/header wie andere Tools), Spaltenfarben, Verschieben-nach im Modal --- backend/src/tools/kanban/routes.js | 76 ++--- frontend/src/tools/kanban.jsx | 499 +++++++++++++++-------------- 2 files changed, 295 insertions(+), 280 deletions(-) diff --git a/backend/src/tools/kanban/routes.js b/backend/src/tools/kanban/routes.js index 75b9ae9..5603ebd 100644 --- a/backend/src/tools/kanban/routes.js +++ b/backend/src/tools/kanban/routes.js @@ -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 }); }); diff --git a/frontend/src/tools/kanban.jsx b/frontend/src/tools/kanban.jsx index bbc8769..e19fc38 100644 --- a/frontend/src/tools/kanban.jsx +++ b/frontend/src/tools/kanban.jsx @@ -1,84 +1,126 @@ import { useState, useEffect, useRef, useCallback } from 'react'; import { api, S } from '../lib.js'; -// ── Prioritäten ─────────────────────────────────────────────────────────────── +// ── Konstanten ──────────────────────────────────────────────────────────────── const PRIORITIES = [ - { id: 'none', label: 'Keine', color: '#888888' }, + { id: 'none', label: 'Keine', color: '#666666' }, { id: 'low', label: 'Niedrig', color: '#4ecdc4' }, { id: 'medium', label: 'Mittel', color: '#ffe66d' }, { id: 'high', label: 'Hoch', color: '#ff6b9d' }, ]; - +const COL_COLORS = ['#4ecdc4','#60a5fa','#a78bfa','#ff6b9d','#ffe66d','#fb923c','#4ade80','#f472b6','#94a3b8']; const prioOf = id => PRIORITIES.find(p => p.id === id) || PRIORITIES[0]; -// ── Karten-Modal (Erstellen + Bearbeiten) ───────────────────────────────────── -// Absichtlich AUSSERHALB der Hauptkomponente definiert (Lesson Learned: kein Remount) -function CardModal({ card, columnId, onSave, onClose }) { - const [title, setTitle] = useState(card?.title || ''); - const [desc, setDesc] = useState(card?.description || ''); - const [prio, setPrio] = useState(card?.priority || 'none'); - const [saving, setSaving] = useState(false); - const titleRef = useRef(null); +// ── Karten-Modal ────────────────────────────────────────────────────────────── +function CardModal({ card, columnId, columns, onSave, onClose, toast }) { + const [title, setTitle] = useState(card?.title || ''); + const [desc, setDesc] = useState(card?.description || ''); + const [prio, setPrio] = useState(card?.priority || 'none'); + const [moveToCol, setMoveToCol] = useState(card?.column_id || columnId); + const [newColName, setNewColName] = useState(''); + const [showNewCol, setShowNewCol] = useState(false); + const [saving, setSaving] = useState(false); + const titleRef = useRef(null); + const newColRef = useRef(null); + const isMob = window.innerWidth < 768; useEffect(() => { titleRef.current?.focus(); }, []); + useEffect(() => { if (showNewCol) newColRef.current?.focus(); }, [showNewCol]); const save = async () => { if (!title.trim()) return; setSaving(true); try { + // Neue Spalte anlegen wenn gewünscht + let targetCol = moveToCol; + if (showNewCol && newColName.trim()) { + const nc = await api('/tools/kanban/columns', { body: { title: newColName.trim() } }); + targetCol = nc.id; + } if (card) { - await api(`/tools/kanban/cards/${card.id}`, { method: 'PATCH', body: { title, description: desc, priority: prio } }); + await api(`/tools/kanban/cards/${card.id}`, { + method: 'PATCH', + body: { title, description: desc, priority: prio }, + }); + // Spalte gewechselt? + if (targetCol && targetCol !== card.column_id) { + await api(`/tools/kanban/cards/${card.id}/move`, { body: { column_id: targetCol } }); + } } else { - await api('/tools/kanban/cards', { body: { column_id: columnId, title, description: desc, priority: prio } }); + await api('/tools/kanban/cards', { body: { column_id: targetCol || columnId, title, description: desc, priority: prio } }); } onSave(); - } catch (e) { alert(e.message); } + } catch (e) { toast(e.message, 'error'); } finally { setSaving(false); } }; - const onKey = e => { if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) save(); if (e.key === 'Escape') onClose(); }; + const onKey = e => { + if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) save(); + if (e.key === 'Escape') onClose(); + }; return ( -
-
e.stopPropagation()} style={{ background:'#1a1a1e',border:'1px solid rgba(255,255,255,0.1)',borderRadius:14,padding:24,width:'100%',maxWidth:440 }}> -
- {card ? 'KARTE BEARBEITEN' : 'NEUE KARTE'} +
e.target === e.currentTarget && onClose()} + style={{ position:'fixed',inset:0,background:'rgba(0,0,0,0.7)',zIndex:1000, + display:'flex',alignItems:isMob?'flex-end':'center',justifyContent:'center',padding:isMob?0:20 }}> +
+ {isMob &&
} + +
+
{card ? 'KARTE BEARBEITEN' : 'NEUE KARTE'}
+
- setTitle(e.target.value)} - onKeyDown={onKey} - placeholder="Titel" - style={{ ...S.inp, marginBottom: 10 }} - /> + setTitle(e.target.value)} onKeyDown={onKey} + placeholder="Titel" style={{ ...S.inp, marginBottom:10 }} /> -