diff --git a/backend/src/db.js b/backend/src/db.js
index f7c774a..c5ad4aa 100644
--- a/backend/src/db.js
+++ b/backend/src/db.js
@@ -288,15 +288,20 @@ for (const [k, v] of loginDefaults) {
if (!db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='board_items'").get()) {
db.exec(`
CREATE TABLE board_items (
- id INTEGER PRIMARY KEY AUTOINCREMENT,
- type TEXT NOT NULL CHECK(type IN ('roadmap','wish')),
- user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
- title TEXT NOT NULL,
- description TEXT NOT NULL DEFAULT '',
- created_at DATETIME DEFAULT NULL
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ type TEXT NOT NULL CHECK(type IN ('roadmap','wish')),
+ user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
+ title TEXT NOT NULL,
+ description TEXT NOT NULL DEFAULT '',
+ promoted_from_wish INTEGER NOT NULL DEFAULT 0,
+ created_at DATETIME DEFAULT NULL
)
`);
}
+// Migration: promoted_from_wish Spalte
+const boardCols = db.prepare("PRAGMA table_info(board_items)").all().map(r => r.name);
+if (boardCols.length && !boardCols.includes('promoted_from_wish'))
+ db.exec("ALTER TABLE board_items ADD COLUMN promoted_from_wish INTEGER NOT NULL DEFAULT 0");
if (!db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='board_reads'").get()) {
db.exec(`
CREATE TABLE board_reads (
diff --git a/backend/src/routes/dashboard.js b/backend/src/routes/dashboard.js
index 3b18312..1ccb327 100644
--- a/backend/src/routes/dashboard.js
+++ b/backend/src/routes/dashboard.js
@@ -189,4 +189,18 @@ router.delete('/board/:id', authenticate, (req, res) => {
res.json({ ok: true });
});
+// Wunsch zur Roadmap befördern (Admin only)
+router.post('/board/:id/promote', authenticate, (req, res) => {
+ if (req.user.role !== 'admin') return res.status(403).json({ error: 'Kein Zugriff' });
+ const item = db.prepare('SELECT * FROM board_items WHERE id=?').get(req.params.id);
+ if (!item) return res.status(404).json({ error: 'Nicht gefunden' });
+ if (item.type !== 'wish') return res.status(400).json({ error: 'Nur Wünsche können befördert werden' });
+ db.prepare('UPDATE board_items SET type=?, promoted_from_wish=1 WHERE id=?').run('roadmap', item.id);
+ const updated = db.prepare(`
+ SELECT b.*, u.username as author FROM board_items b
+ JOIN users u ON u.id=b.user_id WHERE b.id=?
+ `).get(item.id);
+ res.json(updated);
+});
+
module.exports = router;
diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx
index 104ab74..e71f5ea 100644
--- a/frontend/src/App.jsx
+++ b/frontend/src/App.jsx
@@ -822,13 +822,64 @@ function PushoverSettings({ toast }) {
}
// ── Ideen-Board Modal ─────────────────────────────────────────────────────────
+// ── Board ItemRow (außerhalb BoardModal damit Inputs stabil bleiben) ───────────
+function BoardItemRow({ item, isAdmin, userId, editId, editForm, setEditId, setEditForm, onSave, onDelete, onPromote }) {
+ const isEditing = editId === item.id;
+ const canEdit = isAdmin || (item.user_id === userId && item.type === 'wish');
+ return (
+
+ {isEditing ? (
+
+
setEditForm(p=>({...p,title:e.target.value}))}
+ style={{...S.inp,marginBottom:6,fontSize:13}} autoFocus/>
+
setEditForm(p=>({...p,description:e.target.value}))}
+ placeholder="Beschreibung (optional)" style={{...S.inp,marginBottom:8,fontSize:12}}/>
+
+
+
+
+
+ ) : (
+
+
+
+
{item.title}
+ {item.promoted_from_wish && (
+
↑ aus Wünschen
+ )}
+
+ {item.description &&
{item.description}
}
+
+ {item.author} · {item.created_at ? new Date(item.created_at.replace(' ','T')).toLocaleDateString('de-DE') : ''}
+
+
+
+ {isAdmin && item.type==='wish' && (
+
+ )}
+ {canEdit && (
+ <>
+
+
+ >
+ )}
+
+
+ )}
+
+ );
+}
+
function BoardModal({ user, toast, onClose, onRead }) {
const isAdmin = user?.role === 'admin';
- const [items, setItems] = useState([]);
- const [loading, setLoading] = useState(true);
- const [form, setForm] = useState({ type:'wish', title:'', description:'' });
- const [editId, setEditId] = useState(null);
- const [editForm,setEditForm]= useState({});
+ const [items, setItems] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [form, setForm] = useState({ type:'wish', title:'', description:'' });
+ const [editId, setEditId] = useState(null);
+ const [editForm, setEditForm] = useState({ title:'', description:'' });
const isMobile = window.innerWidth < 768;
useEffect(()=>{
@@ -857,46 +908,17 @@ function BoardModal({ user, toast, onClose, onRead }) {
catch(e){ toast(e.message,'error'); }
};
+ const promote = async id => {
+ try {
+ const r = await api(`/dashboard/board/${id}/promote`,{method:'POST'});
+ setItems(p=>p.map(i=>i.id===id?r:i)); toast('Zur Roadmap befördert ✓');
+ } catch(e){ toast(e.message,'error'); }
+ };
+
const roadmap = items.filter(i=>i.type==='roadmap');
const wishes = items.filter(i=>i.type==='wish');
-
- const ItemRow = ({item}) => {
- const canEdit = isAdmin || item.user_id === user?.id;
- const isEditing = editId === item.id;
- return (
-
- {isEditing ? (
-
-
setEditForm(p=>({...p,title:e.target.value}))}
- style={{...S.inp,marginBottom:6,fontSize:13}}/>
-
setEditForm(p=>({...p,description:e.target.value}))}
- placeholder="Beschreibung (optional)" style={{...S.inp,marginBottom:8,fontSize:12}}/>
-
-
-
-
-
- ) : (
-
-
-
{item.title}
- {item.description &&
{item.description}
}
-
- {item.author} · {item.created_at ? new Date(item.created_at.replace(' ','T')).toLocaleDateString('de-DE') : ''}
-
-
- {canEdit && (
-
-
-
-
- )}
-
- )}
-
- );
- };
+ const rowProps = { isAdmin, userId:user?.id, editId, editForm, setEditId, setEditForm,
+ onSave:save, onDelete:del, onPromote:promote };
return (
- {/* Header */}
- {isMobile&&
}
+ display:'flex',justifyContent:'space-between',alignItems:'center',position:'relative'}}>
+ {isMobile&&
}
📋 Ideen & Roadmap
- {/* Roadmap */}
🗺 ROADMAP{isAdmin&& (Admin)}
{loading ?
Lädt…
: roadmap.length===0 ?
Noch keine Einträge.
- : roadmap.map(i=>
)}
+ : roadmap.map(i=>)}
{isAdmin && (
- {/* Wishes */}
-
💡 WÜNSCHE & IDEEN
+
💡 WÜNSCHE & IDEEN
+ {isAdmin &&
↑ befördert Wunsch zur Roadmap
}
{loading ? null : wishes.length===0
?
Noch keine Wünsche.
- : wishes.map(i=>
)}
+ : wishes.map(i=>)}
setForm(f=>({...f,type:'wish'}))}
@@ -968,6 +989,7 @@ function BoardModal({ user, toast, onClose, onRead }) {
);
}
+
function Dashboard({ toast, mobile, setActive, unreadMsgs=0, unreadBoard=0, user }) {
const [now, setNow] = useState(new Date());
const [showBoard, setShowBoard] = useState(false);