feat: HA-Entitaeten fuer Push-Erinnerungen und Kanban; neues Feature Sidebar-Sichtbarkeit pro Nutzer
This commit is contained in:
@@ -258,6 +258,8 @@ if (!userCols2.includes('hidden'))
|
|||||||
db.exec("ALTER TABLE users ADD COLUMN hidden INTEGER NOT NULL DEFAULT 0");
|
db.exec("ALTER TABLE users ADD COLUMN hidden INTEGER NOT NULL DEFAULT 0");
|
||||||
if (!userCols2.includes('chat_active_at'))
|
if (!userCols2.includes('chat_active_at'))
|
||||||
db.exec("ALTER TABLE users ADD COLUMN chat_active_at DATETIME DEFAULT NULL");
|
db.exec("ALTER TABLE users ADD COLUMN chat_active_at DATETIME DEFAULT NULL");
|
||||||
|
if (!userCols2.includes('hidden_tools'))
|
||||||
|
db.exec("ALTER TABLE users ADD COLUMN hidden_tools TEXT NOT NULL DEFAULT '[]'");
|
||||||
|
|
||||||
// ── Login-Sicherheit ──────────────────────────────────────────────────────────
|
// ── Login-Sicherheit ──────────────────────────────────────────────────────────
|
||||||
// Spalten für Account-Lockout in users-Tabelle
|
// Spalten für Account-Lockout in users-Tabelle
|
||||||
|
|||||||
@@ -302,6 +302,8 @@ async function runScheduler() {
|
|||||||
mqttClient.publishPresence();
|
mqttClient.publishPresence();
|
||||||
mqttClient.publishMediaAnfragen();
|
mqttClient.publishMediaAnfragen();
|
||||||
mqttClient.publishDruckStatistik();
|
mqttClient.publishDruckStatistik();
|
||||||
|
mqttClient.publishPushErinnerungen();
|
||||||
|
mqttClient.publishKanban();
|
||||||
try {
|
try {
|
||||||
const now = db.prepare("SELECT datetime('now','localtime') as t").get().t;
|
const now = db.prepare("SELECT datetime('now','localtime') as t").get().t;
|
||||||
const due = db.prepare(`
|
const due = db.prepare(`
|
||||||
|
|||||||
@@ -288,6 +288,28 @@ function publishDiscovery() {
|
|||||||
availability_topic: AVAILABILITY_TOPIC, device: DEVICE,
|
availability_topic: AVAILABILITY_TOPIC, device: DEVICE,
|
||||||
}],
|
}],
|
||||||
|
|
||||||
|
// ── Dashboard ─────────────────────────────────────────────────────────
|
||||||
|
['sensor', 'dickendock_dashboard_push_erinnerungen', {
|
||||||
|
name: 'Dashboard Push-Erinnerungen offen',
|
||||||
|
unique_id: 'dickendock_dashboard_push_erinnerungen',
|
||||||
|
state_topic: `${BASE_TOPIC}/dashboard/push_erinnerungen`,
|
||||||
|
json_attributes_topic: `${BASE_TOPIC}/dashboard/push_erinnerungen_attributes`,
|
||||||
|
icon: 'mdi:bell-ring-outline',
|
||||||
|
unit_of_measurement: 'Erinnerungen', state_class: 'measurement',
|
||||||
|
availability_topic: AVAILABILITY_TOPIC, device: DEVICE,
|
||||||
|
}],
|
||||||
|
|
||||||
|
// ── Kanban ────────────────────────────────────────────────────────────
|
||||||
|
['sensor', 'dickendock_kanban_karten', {
|
||||||
|
name: 'Kanban Karten offen',
|
||||||
|
unique_id: 'dickendock_kanban_karten',
|
||||||
|
state_topic: `${BASE_TOPIC}/kanban/karten`,
|
||||||
|
json_attributes_topic: `${BASE_TOPIC}/kanban/karten_attributes`,
|
||||||
|
icon: 'mdi:view-column-outline',
|
||||||
|
unit_of_measurement: 'Karten', state_class: 'measurement',
|
||||||
|
availability_topic: AVAILABILITY_TOPIC, device: DEVICE,
|
||||||
|
}],
|
||||||
|
|
||||||
// ── System ────────────────────────────────────────────────────────────
|
// ── System ────────────────────────────────────────────────────────────
|
||||||
['sensor', 'dickendock_system_online', {
|
['sensor', 'dickendock_system_online', {
|
||||||
name: 'System Nutzer online',
|
name: 'System Nutzer online',
|
||||||
@@ -542,6 +564,43 @@ function publishDruckStatistik() {
|
|||||||
pub('druck/bestellungen_gesamt', allOrders.length);
|
pub('druck/bestellungen_gesamt', allOrders.length);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Wird jede Minute vom Scheduler aufgerufen — offene (noch nicht versendete,
|
||||||
|
// in der Zukunft liegende) Push-Erinnerungen aus dem Dashboard, systemweit
|
||||||
|
function publishPushErinnerungen() {
|
||||||
|
if (!client?.connected) return;
|
||||||
|
const offen = db.prepare(`
|
||||||
|
SELECT s.message, s.scheduled_at, u.username
|
||||||
|
FROM push_schedules s JOIN users u ON u.id = s.user_id
|
||||||
|
WHERE s.sent = 0 AND s.scheduled_at > datetime('now','localtime')
|
||||||
|
ORDER BY s.scheduled_at ASC
|
||||||
|
`).all();
|
||||||
|
client.publish(`${BASE_TOPIC}/dashboard/push_erinnerungen`, String(offen.length), { retain: true });
|
||||||
|
client.publish(`${BASE_TOPIC}/dashboard/push_erinnerungen_attributes`, JSON.stringify({
|
||||||
|
erinnerungen: offen,
|
||||||
|
}), { retain: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wird jede Minute vom Scheduler aufgerufen — alle Kanban-Karten über alle
|
||||||
|
// Nutzer-Boards hinweg (Kanban ist pro Nutzer, hier systemweit aggregiert)
|
||||||
|
function publishKanban() {
|
||||||
|
if (!client?.connected) return;
|
||||||
|
const karten = db.prepare(`
|
||||||
|
SELECT k.title, k.priority, col.title AS spalte, u.username
|
||||||
|
FROM kanban_cards k
|
||||||
|
JOIN kanban_columns col ON col.id = k.column_id
|
||||||
|
JOIN users u ON u.id = k.user_id
|
||||||
|
ORDER BY u.username ASC, col.position ASC, k.position ASC
|
||||||
|
`).all();
|
||||||
|
const proNutzer = {};
|
||||||
|
for (const k of karten) proNutzer[k.username] = (proNutzer[k.username] || 0) + 1;
|
||||||
|
|
||||||
|
client.publish(`${BASE_TOPIC}/kanban/karten`, String(karten.length), { retain: true });
|
||||||
|
client.publish(`${BASE_TOPIC}/kanban/karten_attributes`, JSON.stringify({
|
||||||
|
karten,
|
||||||
|
pro_nutzer: proNutzer,
|
||||||
|
}), { retain: true });
|
||||||
|
}
|
||||||
|
|
||||||
// Wird von index.js gesetzt, um Zirkel-Requires zu vermeiden
|
// Wird von index.js gesetzt, um Zirkel-Requires zu vermeiden
|
||||||
function setKoepiCheckHandler(fn) { koepiCheckHandler = fn; }
|
function setKoepiCheckHandler(fn) { koepiCheckHandler = fn; }
|
||||||
function setMediaAckHandler(fn) { mediaAckHandler = fn; }
|
function setMediaAckHandler(fn) { mediaAckHandler = fn; }
|
||||||
@@ -553,6 +612,8 @@ module.exports = {
|
|||||||
publishPresence,
|
publishPresence,
|
||||||
publishMediaAnfragen,
|
publishMediaAnfragen,
|
||||||
publishDruckStatistik,
|
publishDruckStatistik,
|
||||||
|
publishPushErinnerungen,
|
||||||
|
publishKanban,
|
||||||
setKoepiCheckHandler,
|
setKoepiCheckHandler,
|
||||||
setMediaAckHandler,
|
setMediaAckHandler,
|
||||||
setMediaAckAllHandler,
|
setMediaAckAllHandler,
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ router.post('/restore', authenticate, requireAdmin, upload.single('database'), (
|
|||||||
// GET /api/admin/users – alle Benutzer mit Statistiken
|
// GET /api/admin/users – alle Benutzer mit Statistiken
|
||||||
router.get('/users', authenticate, requireAdmin, (req, res) => {
|
router.get('/users', authenticate, requireAdmin, (req, res) => {
|
||||||
const users = db.prepare(`
|
const users = db.prepare(`
|
||||||
SELECT u.id, u.username, u.role, u.created_at, u.last_active_at, u.hidden,
|
SELECT u.id, u.username, u.role, u.created_at, u.last_active_at, u.hidden, u.hidden_tools,
|
||||||
CASE WHEN p.user_id IS NOT NULL THEN 1 ELSE 0 END as has_pushover
|
CASE WHEN p.user_id IS NOT NULL THEN 1 ELSE 0 END as has_pushover
|
||||||
FROM users u
|
FROM users u
|
||||||
LEFT JOIN pushover_settings p ON p.user_id = u.id
|
LEFT JOIN pushover_settings p ON p.user_id = u.id
|
||||||
@@ -47,7 +47,9 @@ router.get('/users', authenticate, requireAdmin, (req, res) => {
|
|||||||
const files = db.prepare('SELECT COUNT(*) c FROM files WHERE user_id=?').get(u.id).c;
|
const files = db.prepare('SELECT COUNT(*) c FROM files WHERE user_id=?').get(u.id).c;
|
||||||
const noteLen = (db.prepare('SELECT content FROM notes WHERE user_id=?').get(u.id)?.content || '').length;
|
const noteLen = (db.prepare('SELECT content FROM notes WHERE user_id=?').get(u.id)?.content || '').length;
|
||||||
const icals = db.prepare('SELECT COUNT(*) c FROM calendar_feeds WHERE user_id=?').get(u.id).c;
|
const icals = db.prepare('SELECT COUNT(*) c FROM calendar_feeds WHERE user_id=?').get(u.id).c;
|
||||||
return { ...u, archiv, bestellung, snippets_c, todos, links, files, icals, noteLen };
|
let hidden_tools = [];
|
||||||
|
try { hidden_tools = JSON.parse(u.hidden_tools || '[]'); } catch {}
|
||||||
|
return { ...u, hidden_tools, archiv, bestellung, snippets_c, todos, links, files, icals, noteLen };
|
||||||
});
|
});
|
||||||
res.json(stats);
|
res.json(stats);
|
||||||
});
|
});
|
||||||
@@ -170,6 +172,18 @@ router.put('/users/:id/hidden', authenticate, requireAdmin, (req, res) => {
|
|||||||
res.json({ hidden: newHidden });
|
res.json({ hidden: newHidden });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// PUT /api/admin/users/:id/hidden-tools – Sidebar-Sichtbarkeit für einen User setzen
|
||||||
|
// body: { hidden_tools: ['schocken', 'paywallkiller', ...] } (tool-ids aus toolRegistry.js)
|
||||||
|
router.put('/users/:id/hidden-tools', authenticate, requireAdmin, (req, res) => {
|
||||||
|
const user = db.prepare('SELECT * FROM users WHERE id=?').get(req.params.id);
|
||||||
|
if (!user) return res.status(404).json({ error: 'Nicht gefunden' });
|
||||||
|
const { hidden_tools } = req.body;
|
||||||
|
if (!Array.isArray(hidden_tools)) return res.status(400).json({ error: 'hidden_tools muss ein Array sein' });
|
||||||
|
const cleaned = [...new Set(hidden_tools.filter(t => typeof t === 'string'))];
|
||||||
|
db.prepare('UPDATE users SET hidden_tools=? WHERE id=?').run(JSON.stringify(cleaned), user.id);
|
||||||
|
res.json({ hidden_tools: cleaned });
|
||||||
|
});
|
||||||
|
|
||||||
// Test-Pushover an beliebigen User senden
|
// Test-Pushover an beliebigen User senden
|
||||||
router.post('/users/:id/test-push', authenticate, requireAdmin, async (req, res) => {
|
router.post('/users/:id/test-push', authenticate, requireAdmin, async (req, res) => {
|
||||||
const user = db.prepare('SELECT * FROM users WHERE id=?').get(req.params.id);
|
const user = db.prepare('SELECT * FROM users WHERE id=?').get(req.params.id);
|
||||||
|
|||||||
@@ -166,4 +166,14 @@ router.put('/preferences', authenticate, (req, res) => {
|
|||||||
res.json(merged);
|
res.json(merged);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ── GET /api/auth/hidden-tools ────────────────────────────────────────────────
|
||||||
|
// Liefert die vom Admin für diesen Nutzer ausgeblendeten Sidebar-Bereiche.
|
||||||
|
// Bewusst NICHT über /preferences (das ist selbst-editierbar) — Sichtbarkeit
|
||||||
|
// darf nur der Admin ändern.
|
||||||
|
router.get('/hidden-tools', authenticate, (req, res) => {
|
||||||
|
const user = db.prepare('SELECT hidden_tools FROM users WHERE id=?').get(req.user.id);
|
||||||
|
try { res.json(JSON.parse(user?.hidden_tools || '[]')); }
|
||||||
|
catch { res.json([]); }
|
||||||
|
});
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
|||||||
@@ -155,7 +155,7 @@ function Login({ onLogin }) {
|
|||||||
|
|
||||||
// ── Update Modal ──────────────────────────────────────────────────────────────
|
// ── Update Modal ──────────────────────────────────────────────────────────────
|
||||||
// ── Desktop Sidebar ───────────────────────────────────────────────────────────
|
// ── 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 [appsOpen, setAppsOpen] = useState(true);
|
||||||
const [collapsed, setCollapsed] = useState(false);
|
const [collapsed, setCollapsed] = useState(false);
|
||||||
const [collapsedGroups, setCollapsedGroups] = useState({});
|
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 && <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'}/>}
|
{!collapsed && <ChevronIcon size={13} color="rgba(255,255,255,0.35)" dir={appsOpen?'down':'right'}/>}
|
||||||
</button>
|
</button>
|
||||||
{appsOpen && getGroupedTools(isAdmin).map(([group, tools]) => {
|
{appsOpen && getGroupedTools(isAdmin, hiddenTools).map(([group, tools]) => {
|
||||||
const isGroupCollapsed = !!collapsedGroups[group];
|
const isGroupCollapsed = !!collapsedGroups[group];
|
||||||
return (
|
return (
|
||||||
<div key={group}>
|
<div key={group}>
|
||||||
@@ -296,7 +296,7 @@ function Sidebar({ active, setActive, user, onLogout, unreadMsgs=0, hexWarsTurns
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ── Mobile Bottom Navigation ──────────────────────────────────────────────────
|
// ── 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 [menuOpen, setMenuOpen] = useState(false);
|
||||||
const [appsOpen, setAppsOpen] = useState(false);
|
const [appsOpen, setAppsOpen] = useState(false);
|
||||||
const [mobileCollapsedGroups, setMobileCollapsedGroups] = useState({});
|
const [mobileCollapsedGroups, setMobileCollapsedGroups] = useState({});
|
||||||
@@ -360,7 +360,7 @@ function BottomNav({ active, setActive, user, onLogout, unreadMsgs=0, hexWarsTur
|
|||||||
{/* Apps Sheet mit Gruppenüberschriften */}
|
{/* Apps Sheet mit Gruppenüberschriften */}
|
||||||
<Sheet open={appsOpen} onClose={()=>setAppsOpen(false)}>
|
<Sheet open={appsOpen} onClose={()=>setAppsOpen(false)}>
|
||||||
<div style={{ overflowY:'auto', maxHeight:'60vh', paddingBottom:8 }}>
|
<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];
|
const isGrpCollapsed = !!mobileCollapsedGroups[group];
|
||||||
return (
|
return (
|
||||||
<div key={group}>
|
<div key={group}>
|
||||||
@@ -2380,6 +2380,8 @@ function UserManagement({ toast }) {
|
|||||||
const [resetId, setResetId] = useState(null);
|
const [resetId, setResetId] = useState(null);
|
||||||
const [resetPw, setResetPw] = useState('');
|
const [resetPw, setResetPw] = useState('');
|
||||||
const [delId, setDelId] = useState(null);
|
const [delId, setDelId] = useState(null);
|
||||||
|
const [visId, setVisId] = useState(null);
|
||||||
|
const [visSelection, setVisSelection] = useState([]);
|
||||||
const { confirm, ConfirmDialog } = useConfirm();
|
const { confirm, ConfirmDialog } = useConfirm();
|
||||||
|
|
||||||
const load = () => {
|
const load = () => {
|
||||||
@@ -2411,6 +2413,24 @@ function UserManagement({ toast }) {
|
|||||||
} catch(e) { toast(e.message,'error'); }
|
} 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 (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<ConfirmDialog/>
|
<ConfirmDialog/>
|
||||||
@@ -2448,6 +2468,51 @@ function UserManagement({ toast }) {
|
|||||||
</div>
|
</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 */}
|
{/* Benutzerliste */}
|
||||||
<Sec title="BENUTZER">
|
<Sec title="BENUTZER">
|
||||||
{loading && <div style={{color:'rgba(255,255,255,0.4)',fontFamily:'monospace',fontSize:12}}>Lädt…</div>}
|
{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('');}}
|
<button onClick={()=>{setResetId(u.id);setResetPw('');}}
|
||||||
style={{...S.btn('#ffe66d',true),fontSize:10,padding:'4px 8px'}}>🔑</button>
|
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
|
<button
|
||||||
onClick={async ()=>{
|
onClick={async ()=>{
|
||||||
if (u.role==='admin') return;
|
if (u.role==='admin') return;
|
||||||
@@ -3702,6 +3772,7 @@ export default function App() {
|
|||||||
return window.matchMedia('(display-mode: standalone)').matches && !sessionStorage.getItem('splash_shown');
|
return window.matchMedia('(display-mode: standalone)').matches && !sessionStorage.getItem('splash_shown');
|
||||||
});
|
});
|
||||||
const [user,setUser]=useState(null);
|
const [user,setUser]=useState(null);
|
||||||
|
const [hiddenTools,setHiddenTools]=useState([]);
|
||||||
const [active,setActiveRaw]=useState('dashboard');
|
const [active,setActiveRaw]=useState('dashboard');
|
||||||
const [devToolsNav, setDevToolsNav] = useState(null);
|
const [devToolsNav, setDevToolsNav] = useState(null);
|
||||||
const pollUnackedFavs = () => api('/tools/media/favorites/unacked').then(d=>setUnackedFavs({count:d.count||0,isAdmin:d.isAdmin})).catch(()=>{});
|
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;
|
if(!user)return;
|
||||||
},[user]);
|
},[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
|
// Schlüssel sofort nach Login initialisieren und Public Key registrieren
|
||||||
useEffect(()=>{
|
useEffect(()=>{
|
||||||
if(!user)return;
|
if(!user)return;
|
||||||
@@ -3806,7 +3891,7 @@ export default function App() {
|
|||||||
if(!authChecked) return <div style={{background:'#0d0d0f',minHeight:'100vh'}}/>;
|
if(!authChecked) return <div style={{background:'#0d0d0f',minHeight:'100vh'}}/>;
|
||||||
if(!user) return <Login onLogin={u=>setUser(u)}/>;
|
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
|
// Mobile padding bottom für fixed nav
|
||||||
const isNachrichten = active === 'nachrichten';
|
const isNachrichten = active === 'nachrichten';
|
||||||
@@ -3832,9 +3917,9 @@ export default function App() {
|
|||||||
<div style={{display:'flex',flexDirection:'row',height:'100dvh',background:'#111114'}}>
|
<div style={{display:'flex',flexDirection:'row',height:'100dvh',background:'#111114'}}>
|
||||||
{!mobile && (
|
{!mobile && (
|
||||||
<Sidebar active={active} setActive={setActive} user={user}
|
<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}>
|
<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}/>}
|
{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>
|
</div>
|
||||||
{mobile && (
|
{mobile && (
|
||||||
<BottomNav active={active} setActive={setActive} user={user}
|
<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}/>
|
<ScrollToTop scrollRef={mainRef}/>
|
||||||
<Toast msg={toastMsg.msg} type={toastMsg.type}/>
|
<Toast msg={toastMsg.msg} type={toastMsg.type}/>
|
||||||
|
|||||||
Reference in New Issue
Block a user