Changelog: ToDo-Auswahl zum Miterledigen; Admin-Stats: Code-Schnipsel

This commit is contained in:
2026-05-29 15:07:57 +02:00
parent eb95535afa
commit 5afa28e523

View File

@@ -1260,15 +1260,26 @@ function ChangelogModal({ user, toast, onClose, onRead }) {
const [entries, setEntries] = useState([]); const [entries, setEntries] = useState([]);
const [form, setForm] = useState({ version:'', title:'', body:'' }); const [form, setForm] = useState({ version:'', title:'', body:'' });
const [busy, setBusy] = useState(false); const [busy, setBusy] = useState(false);
const [boardItems, setBoardItems] = useState([]);
const [toDelete, setToDelete] = useState(new Set()); // ids to delete on submit
const isMobile = window.innerWidth < 768; const isMobile = window.innerWidth < 768;
useEffect(()=>{ useEffect(()=>{
api('/dashboard/changelog').then(setEntries).catch(()=>{}); api('/dashboard/changelog').then(setEntries).catch(()=>{});
api('/dashboard/changelog/read',{method:'POST'}).then(()=>{ if(onRead) onRead(); }).catch(()=>{}); api('/dashboard/changelog/read',{method:'POST'}).then(()=>{ if(onRead) onRead(); }).catch(()=>{});
if (user?.role==='admin') {
api('/dashboard/board').then(d=>setBoardItems(d.items||[])).catch(()=>{});
}
},[]); },[]);
const toggleDelete = id => setToDelete(p => {
const n = new Set(p);
n.has(id) ? n.delete(id) : n.add(id);
return n;
});
const add = async () => { const add = async () => {
if (!form.version.trim() || !form.title.trim()) { toast('Version und Titel erforderlich','error'); return; } if (!form.version.trim() || !form.title.trim()) { toast('Stichwort und Titel erforderlich','error'); return; }
setBusy(true); setBusy(true);
try { try {
const r = await api('/dashboard/changelog',{body:{ const r = await api('/dashboard/changelog',{body:{
@@ -1276,6 +1287,12 @@ function ChangelogModal({ user, toast, onClose, onRead }) {
build_time: typeof __BUILD_TIME__ !== 'undefined' ? __BUILD_TIME__ : null, build_time: typeof __BUILD_TIME__ !== 'undefined' ? __BUILD_TIME__ : null,
}}); }});
setEntries(p=>[r,...p]); setEntries(p=>[r,...p]);
// Markierte Board-Items löschen
if (toDelete.size > 0) {
await Promise.all([...toDelete].map(id => api(`/dashboard/board/${id}`,{method:'DELETE'}).catch(()=>{})));
setBoardItems(p => p.filter(i => !toDelete.has(i.id)));
setToDelete(new Set());
}
setForm({version:'',title:'',body:''}); setForm({version:'',title:'',body:''});
toast('Eintrag hinzugefügt ✓'); toast('Eintrag hinzugefügt ✓');
} catch(e){ toast(e.message,'error'); } } catch(e){ toast(e.message,'error'); }
@@ -1289,13 +1306,17 @@ function ChangelogModal({ user, toast, onClose, onRead }) {
const fmtDate = s => { if (!s) return ''; const d = new Date(s.replace(' ','T')); return isNaN(d)?'':d.toLocaleString('de-DE',{day:'2-digit',month:'2-digit',year:'numeric',hour:'2-digit',minute:'2-digit'}); }; const fmtDate = s => { if (!s) return ''; const d = new Date(s.replace(' ','T')); return isNaN(d)?'':d.toLocaleString('de-DE',{day:'2-digit',month:'2-digit',year:'numeric',hour:'2-digit',minute:'2-digit'}); };
const roadmap = boardItems.filter(i=>i.type==='roadmap');
const wishes = boardItems.filter(i=>i.type==='wish');
return ( return (
<div style={{position:'fixed',inset:0,background:'rgba(0,0,0,0.85)',zIndex:7000, <div style={{position:'fixed',inset:0,background:'rgba(0,0,0,0.85)',zIndex:7000,
display:'flex',alignItems:isMobile?'flex-end':'center',justifyContent:'center',padding:isMobile?0:24}} display:'flex',alignItems:isMobile?'flex-end':'center',justifyContent:'center',padding:isMobile?0:24}}
onClick={e=>e.target===e.currentTarget&&onClose()}> onClick={e=>e.target===e.currentTarget&&onClose()}>
<div style={{background:'#1a1a1e',borderRadius:isMobile?'16px 16px 0 0':16,width:'100%',maxWidth:540, <div style={{background:'#1a1a1e',borderRadius:isMobile?'16px 16px 0 0':16,
width:'100%',maxWidth: isAdmin ? 860 : 540,
border:'1px solid rgba(255,255,255,0.12)',display:'flex',flexDirection:'column', border:'1px solid rgba(255,255,255,0.12)',display:'flex',flexDirection:'column',
maxHeight:isMobile?'88vh':'82vh',overflow:'hidden'}}> maxHeight:isMobile?'92vh':'84vh',overflow:'hidden'}}>
{/* Header */} {/* Header */}
<div style={{padding:'16px 20px',borderBottom:'1px solid rgba(255,255,255,0.08)',flexShrink:0, <div style={{padding:'16px 20px',borderBottom:'1px solid rgba(255,255,255,0.08)',flexShrink:0,
@@ -1306,20 +1327,30 @@ function ChangelogModal({ user, toast, onClose, onRead }) {
<button onClick={onClose} style={{background:'transparent',border:'none',color:'rgba(255,255,255,0.4)',cursor:'pointer',fontSize:18}}></button> <button onClick={onClose} style={{background:'transparent',border:'none',color:'rgba(255,255,255,0.4)',cursor:'pointer',fontSize:18}}></button>
</div> </div>
<div style={{overflowY:'auto',flex:1,padding:'0 20px'}}> <div style={{display:'flex',flex:1,overflow:'hidden',flexDirection:isMobile?'column':'row'}}>
{/* Linke Spalte: Formular + Einträge */}
<div style={{flex:1,overflowY:'auto',padding:'0 20px',minWidth:0}}>
{/* Admin-Formular */} {/* Admin-Formular */}
{isAdmin && ( {isAdmin && (
<div style={{padding:'14px 0',borderBottom:'1px solid rgba(255,255,255,0.08)'}}> <div style={{padding:'14px 0',borderBottom:'1px solid rgba(255,255,255,0.08)'}}>
<div style={{...S.head,marginBottom:8}}>NEUER EINTRAG</div> <div style={{...S.head,marginBottom:8}}>NEUER EINTRAG</div>
<div style={{display:'grid',gridTemplateColumns:'1fr 2fr',gap:8,marginBottom:8}}> <div style={{display:'grid',gridTemplateColumns:'1fr 2fr',gap:8,marginBottom:8}}>
<input value={form.version} onChange={e=>setForm(p=>({...p,version:e.target.value}))} <input value={form.version} onChange={e=>setForm(p=>({...p,version:e.target.value}))}
placeholder="Stichwort (z.B. Nachrichten)" style={{...S.inp,fontSize:12}}/> placeholder="Stichwort" style={{...S.inp,fontSize:12}}/>
<input value={form.title} onChange={e=>setForm(p=>({...p,title:e.target.value}))} <input value={form.title} onChange={e=>setForm(p=>({...p,title:e.target.value}))}
placeholder="Kurztitel" style={{...S.inp,fontSize:12}}/> placeholder="Kurztitel" style={{...S.inp,fontSize:12}}/>
</div> </div>
<textarea value={form.body} onChange={e=>setForm(p=>({...p,body:e.target.value}))} <textarea value={form.body} onChange={e=>setForm(p=>({...p,body:e.target.value}))}
placeholder="Beschreibung der Änderungen (Markdown-artig, z.B. - Neue Funktion X&#10;- Fix: Bug Y)" placeholder="Beschreibung"
rows={3} style={{...S.inp,resize:'vertical',fontSize:12,marginBottom:8,width:'100%',boxSizing:'border-box'}}/> rows={3} style={{...S.inp,resize:'vertical',fontSize:12,marginBottom:8,width:'100%',boxSizing:'border-box'}}/>
{toDelete.size>0 && (
<div style={{background:'rgba(255,107,157,0.07)',border:'1px solid rgba(255,107,157,0.2)',
borderRadius:6,padding:'6px 10px',marginBottom:8,color:'rgba(255,107,157,0.7)',
fontFamily:'monospace',fontSize:10}}>
🗑 {toDelete.size} ToDo{toDelete.size!==1?'s':''} wird beim Hinzufügen gelöscht
</div>
)}
<button onClick={add} disabled={busy} <button onClick={add} disabled={busy}
style={{...S.btn('#4ecdc4'),width:'100%',textAlign:'center',padding:'9px 0',opacity:busy?0.5:1}}> style={{...S.btn('#4ecdc4'),width:'100%',textAlign:'center',padding:'9px 0',opacity:busy?0.5:1}}>
{busy?'…':'+ Eintrag hinzufügen'} {busy?'…':'+ Eintrag hinzufügen'}
@@ -1365,11 +1396,65 @@ function ChangelogModal({ user, toast, onClose, onRead }) {
))} ))}
</div> </div>
</div> </div>
{/* Rechte Spalte: Board-Items zum Abhaken (nur Admin, nur wenn vorhanden) */}
{isAdmin && boardItems.length>0 && (
<div style={{width: isMobile?'100%':240,flexShrink:0,borderLeft:isMobile?'none':'1px solid rgba(255,255,255,0.07)',
borderTop:isMobile?'1px solid rgba(255,255,255,0.07)':'none',
overflowY:'auto',padding:'14px 16px',background:'rgba(0,0,0,0.15)'}}>
<div style={{...S.head,marginBottom:8}}>TODOS MITERLEDIGEN</div>
<p style={{color:'rgba(255,255,255,0.25)',fontFamily:'monospace',fontSize:9,marginBottom:12,lineHeight:1.6}}>
Markierte Einträge werden beim Hinzufügen des Changelogs gelöscht.
</p>
{roadmap.length>0 && (
<>
<div style={{color:'rgba(255,255,255,0.3)',fontFamily:'monospace',fontSize:9,letterSpacing:1,marginBottom:6}}>🗺 ROADMAP</div>
{roadmap.map(item=>(
<label key={item.id} style={{display:'flex',alignItems:'flex-start',gap:8,
padding:'6px 0',borderBottom:'1px solid rgba(255,255,255,0.04)',cursor:'pointer'}}>
<input type="checkbox" checked={toDelete.has(item.id)} onChange={()=>toggleDelete(item.id)}
style={{marginTop:2,accentColor:'#4ecdc4',flexShrink:0}}/>
<div>
<div style={{color: toDelete.has(item.id)?'rgba(255,255,255,0.35)':'rgba(255,255,255,0.7)',
fontFamily:'monospace',fontSize:11,
textDecoration: toDelete.has(item.id)?'line-through':'none'}}>
{item.title}
</div>
{item.description&&<div style={{color:'rgba(255,255,255,0.25)',fontFamily:'monospace',fontSize:9}}>{item.description}</div>}
</div>
</label>
))}
</>
)}
{wishes.length>0 && (
<div style={{marginTop: roadmap.length>0?12:0}}>
<div style={{color:'rgba(255,255,255,0.3)',fontFamily:'monospace',fontSize:9,letterSpacing:1,marginBottom:6}}>💡 WÜNSCHE</div>
{wishes.map(item=>(
<label key={item.id} style={{display:'flex',alignItems:'flex-start',gap:8,
padding:'6px 0',borderBottom:'1px solid rgba(255,255,255,0.04)',cursor:'pointer'}}>
<input type="checkbox" checked={toDelete.has(item.id)} onChange={()=>toggleDelete(item.id)}
style={{marginTop:2,accentColor:'#ffe66d',flexShrink:0}}/>
<div>
<div style={{color: toDelete.has(item.id)?'rgba(255,255,255,0.35)':'rgba(255,255,255,0.7)',
fontFamily:'monospace',fontSize:11,
textDecoration: toDelete.has(item.id)?'line-through':'none'}}>
{item.title}
</div>
{item.description&&<div style={{color:'rgba(255,255,255,0.25)',fontFamily:'monospace',fontSize:9}}>{item.description}</div>}
</div>
</label>
))}
</div>
)}
</div>
)}
</div>
</div> </div>
</div> </div>
); );
} }
function Dashboard({ toast, mobile, setActive, unreadMsgs=0, unreadBoard=0, unreadPromoted=0, onBoardRead, unreadChangelog=0, onChangelogRead, user }) { function Dashboard({ toast, mobile, setActive, unreadMsgs=0, unreadBoard=0, unreadPromoted=0, onBoardRead, unreadChangelog=0, onChangelogRead, user }) {
const [now, setNow] = useState(new Date()); const [now, setNow] = useState(new Date());
const [showBoard, setShowBoard] = useState(false); const [showBoard, setShowBoard] = useState(false);