feat: HA-Entitaeten fuer Push-Erinnerungen und Kanban; neues Feature Sidebar-Sichtbarkeit pro Nutzer

This commit is contained in:
2026-07-10 20:47:57 +02:00
parent 37ebad91c6
commit d372bed860
6 changed files with 185 additions and 11 deletions

View File

@@ -155,7 +155,7 @@ function Login({ onLogin }) {
// ── Update Modal ──────────────────────────────────────────────────────────────
// ── Desktop Sidebar ───────────────────────────────────────────────────────────
function Sidebar({ active, setActive, user, onLogout, unreadMsgs=0, hexWarsTurns=0, whiteboardUnread=0, isAdmin=false }) {
function Sidebar({ active, setActive, user, onLogout, unreadMsgs=0, hexWarsTurns=0, whiteboardUnread=0, isAdmin=false, hiddenTools=[] }) {
const [appsOpen, setAppsOpen] = useState(true);
const [collapsed, setCollapsed] = useState(false);
const [collapsedGroups, setCollapsedGroups] = useState({});
@@ -255,7 +255,7 @@ function Sidebar({ active, setActive, user, onLogout, unreadMsgs=0, hexWarsTurns
{!collapsed && <span style={{ flex:1, color:'rgba(255,255,255,0.65)' }}>Apps</span>}
{!collapsed && <ChevronIcon size={13} color="rgba(255,255,255,0.35)" dir={appsOpen?'down':'right'}/>}
</button>
{appsOpen && getGroupedTools(isAdmin).map(([group, tools]) => {
{appsOpen && getGroupedTools(isAdmin, hiddenTools).map(([group, tools]) => {
const isGroupCollapsed = !!collapsedGroups[group];
return (
<div key={group}>
@@ -296,7 +296,7 @@ function Sidebar({ active, setActive, user, onLogout, unreadMsgs=0, hexWarsTurns
}
// ── Mobile Bottom Navigation ──────────────────────────────────────────────────
function BottomNav({ active, setActive, user, onLogout, unreadMsgs=0, hexWarsTurns=0, whiteboardUnread=0, isAdmin=false }) {
function BottomNav({ active, setActive, user, onLogout, unreadMsgs=0, hexWarsTurns=0, whiteboardUnread=0, isAdmin=false, hiddenTools=[] }) {
const [menuOpen, setMenuOpen] = useState(false);
const [appsOpen, setAppsOpen] = useState(false);
const [mobileCollapsedGroups, setMobileCollapsedGroups] = useState({});
@@ -360,7 +360,7 @@ function BottomNav({ active, setActive, user, onLogout, unreadMsgs=0, hexWarsTur
{/* Apps Sheet mit Gruppenüberschriften */}
<Sheet open={appsOpen} onClose={()=>setAppsOpen(false)}>
<div style={{ overflowY:'auto', maxHeight:'60vh', paddingBottom:8 }}>
{getGroupedTools(user?.role==='admin').map(([group, tools]) => {
{getGroupedTools(user?.role==='admin', hiddenTools).map(([group, tools]) => {
const isGrpCollapsed = !!mobileCollapsedGroups[group];
return (
<div key={group}>
@@ -2380,6 +2380,8 @@ function UserManagement({ toast }) {
const [resetId, setResetId] = useState(null);
const [resetPw, setResetPw] = useState('');
const [delId, setDelId] = useState(null);
const [visId, setVisId] = useState(null);
const [visSelection, setVisSelection] = useState([]);
const { confirm, ConfirmDialog } = useConfirm();
const load = () => {
@@ -2411,6 +2413,24 @@ function UserManagement({ toast }) {
} catch(e) { toast(e.message,'error'); }
};
const openVisibility = (u) => {
setVisId(u.id);
setVisSelection(u.hidden_tools || []);
};
const toggleVisTool = (toolId) => {
setVisSelection(prev => prev.includes(toolId) ? prev.filter(t=>t!==toolId) : [...prev, toolId]);
};
const saveVisibility = async () => {
try {
await api(`/admin/users/${visId}/hidden-tools`,{method:'PUT',body:{hidden_tools:visSelection}});
setUsers(p=>p.map(x=>x.id===visId?{...x,hidden_tools:visSelection}:x));
toast('Sichtbarkeit gespeichert');
setVisId(null);
} catch(e) { toast(e.message,'error'); }
};
return (
<div>
<ConfirmDialog/>
@@ -2448,6 +2468,51 @@ function UserManagement({ toast }) {
</div>
)}
{/* Sidebar-Sichtbarkeit Modal */}
{visId && (() => {
const grouped = {};
for (const t of TOOLS) {
const g = t.group || 'Allgemein';
if (!grouped[g]) grouped[g] = [];
grouped[g].push(t);
}
const target = users.find(u=>u.id===visId);
return (
<div style={{position:'fixed',inset:0,background:'rgba(0,0,0,0.8)',zIndex:5000,
display:'flex',alignItems:'center',justifyContent:'center',padding:20}}>
<div style={{...S.card,width:'100%',maxWidth:420,maxHeight:'80vh',overflowY:'auto'}}>
<div style={{color:'#fff',fontFamily:"'Space Mono',monospace",fontSize:14,fontWeight:700,marginBottom:4}}>
Sichtbare Bereiche{target ? `${target.username}` : ''}
</div>
<div style={{color:'rgba(255,255,255,0.4)',fontFamily:'monospace',fontSize:10,marginBottom:14}}>
Angehakt = ausgeblendet für diesen Nutzer
</div>
{Object.entries(grouped).map(([group, tools]) => (
<div key={group} style={{marginBottom:14}}>
<div style={{...S.head,marginBottom:6}}>{group}</div>
{tools.map(t => (
<label key={t.id} style={{
display:'flex',alignItems:'center',gap:8,padding:'6px 2px',cursor:'pointer',
}}>
<input type="checkbox" checked={visSelection.includes(t.id)}
onChange={()=>toggleVisTool(t.id)}
style={{width:15,height:15,accentColor:'#ff6b9d',flexShrink:0}}/>
<span style={{color:'rgba(255,255,255,0.75)',fontFamily:'monospace',fontSize:12}}>
{t.label}{t.adminOnly ? ' (nur Admin)' : ''}
</span>
</label>
))}
</div>
))}
<div style={{display:'flex',gap:8,marginTop:8}}>
<button onClick={saveVisibility} style={{...S.btn('#4ecdc4'),flex:1,textAlign:'center',padding:'10px 0'}}> Speichern</button>
<button onClick={()=>setVisId(null)} style={{...S.btn('#ff6b9d'),flex:1,textAlign:'center',padding:'10px 0'}}>Abbrechen</button>
</div>
</div>
</div>
);
})()}
{/* Benutzerliste */}
<Sec title="BENUTZER">
{loading && <div style={{color:'rgba(255,255,255,0.4)',fontFamily:'monospace',fontSize:12}}>Lädt</div>}
@@ -2508,6 +2573,11 @@ function UserManagement({ toast }) {
)}
<button onClick={()=>{setResetId(u.id);setResetPw('');}}
style={{...S.btn('#ffe66d',true),fontSize:10,padding:'4px 8px'}}>🔑</button>
<button onClick={()=>openVisibility(u)}
title="Sichtbare Bereiche"
style={{...S.btn('#4ecdc4',true),fontSize:10,padding:'4px 8px'}}>
🧩{u.hidden_tools?.length ? ` ${u.hidden_tools.length}` : ''}
</button>
<button
onClick={async ()=>{
if (u.role==='admin') return;
@@ -3702,6 +3772,7 @@ export default function App() {
return window.matchMedia('(display-mode: standalone)').matches && !sessionStorage.getItem('splash_shown');
});
const [user,setUser]=useState(null);
const [hiddenTools,setHiddenTools]=useState([]);
const [active,setActiveRaw]=useState('dashboard');
const [devToolsNav, setDevToolsNav] = useState(null);
const pollUnackedFavs = () => api('/tools/media/favorites/unacked').then(d=>setUnackedFavs({count:d.count||0,isAdmin:d.isAdmin})).catch(()=>{});
@@ -3741,6 +3812,20 @@ export default function App() {
if(!user)return;
},[user]);
// Vom Admin ausgeblendete Sidebar-Bereiche laden (einmal pro Login)
useEffect(()=>{
if(!user)return;
api('/auth/hidden-tools').then(setHiddenTools).catch(()=>{});
},[user]);
// Falls der aktuell offene Bereich inzwischen ausgeblendet wurde (z.B. Admin
// hat während der laufenden Session die Sichtbarkeit geändert): zurück zum Dashboard
useEffect(()=>{
if (active !== 'dashboard' && active !== 'admin' && hiddenTools.includes(active)) {
setActiveRaw('dashboard');
}
},[active, hiddenTools]);
// Schlüssel sofort nach Login initialisieren und Public Key registrieren
useEffect(()=>{
if(!user)return;
@@ -3806,7 +3891,7 @@ export default function App() {
if(!authChecked) return <div style={{background:'#0d0d0f',minHeight:'100vh'}}/>;
if(!user) return <Login onLogin={u=>setUser(u)}/>;
const activeTool = TOOLS.find(t=>t.id===active);
const activeTool = hiddenTools.includes(active) ? null : TOOLS.find(t=>t.id===active);
// Mobile padding bottom für fixed nav
const isNachrichten = active === 'nachrichten';
@@ -3832,9 +3917,9 @@ export default function App() {
<div style={{display:'flex',flexDirection:'row',height:'100dvh',background:'#111114'}}>
{!mobile && (
<Sidebar active={active} setActive={setActive} user={user}
onLogout={()=>{localStorage.removeItem('sk_token');setUser(null);}}
onLogout={()=>{localStorage.removeItem('sk_token');setUser(null);setHiddenTools([]);}}
unreadMsgs={unreadMsgs} hexWarsTurns={hexWarsTurns} whiteboardUnread={whiteboardUnread} isAdmin={user?.role==='admin'}/>
unreadMsgs={unreadMsgs} hexWarsTurns={hexWarsTurns} whiteboardUnread={whiteboardUnread} isAdmin={user?.role==='admin'} hiddenTools={hiddenTools}/>
)}
<main ref={mainRef} style={mainStyle}>
{active==='dashboard' && <Dashboard key={dashboardKey} toast={toast} mobile={mobile} setActive={setActive} setDevToolsNav={setDevToolsNav} unreadMsgs={unreadMsgs} unreadBoard={unreadBoard} unreadPromoted={unreadPromoted} onBoardRead={()=>{ setUnreadBoard(0); setUnreadPromoted(0); }} unreadChangelog={unreadChangelog} onChangelogRead={()=>setUnreadChangelog(0)} unackedFavs={unackedFavs} onClearUnackedFavs={()=>setUnackedFavs({count:0,isAdmin:false})} user={user}/>}
@@ -3844,9 +3929,9 @@ export default function App() {
</div>
{mobile && (
<BottomNav active={active} setActive={setActive} user={user}
onLogout={()=>{localStorage.removeItem('sk_token');setUser(null);}}
onLogout={()=>{localStorage.removeItem('sk_token');setUser(null);setHiddenTools([]);}}
unreadMsgs={unreadMsgs} hexWarsTurns={hexWarsTurns} whiteboardUnread={whiteboardUnread} isAdmin={user?.role==='admin'}/>
unreadMsgs={unreadMsgs} hexWarsTurns={hexWarsTurns} whiteboardUnread={whiteboardUnread} isAdmin={user?.role==='admin'} hiddenTools={hiddenTools}/>
)}
<ScrollToTop scrollRef={mainRef}/>
<Toast msg={toastMsg.msg} type={toastMsg.type}/>