diff --git a/backend/src/db.js b/backend/src/db.js index bac62a8..bb3a39e 100644 --- a/backend/src/db.js +++ b/backend/src/db.js @@ -566,6 +566,8 @@ if (!db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='mov db.exec("ALTER TABLE movie_favorites ADD COLUMN user_notified INTEGER NOT NULL DEFAULT 0"); if (!cols.includes('media_type')) db.exec("ALTER TABLE movie_favorites ADD COLUMN media_type TEXT NOT NULL DEFAULT 'movie'"); + if (!cols.includes('admin_seen')) + db.exec("ALTER TABLE movie_favorites ADD COLUMN admin_seen INTEGER NOT NULL DEFAULT 0"); } // xREL Badge Cache (persistent, überlebt Container-Restarts) diff --git a/backend/src/tools/media/routes.js b/backend/src/tools/media/routes.js index 5ca6294..89530ae 100644 --- a/backend/src/tools/media/routes.js +++ b/backend/src/tools/media/routes.js @@ -866,12 +866,13 @@ router.post('/favorites/user-notify', authenticate, (req, res) => { res.json({ ok: true }); }); -// GET /favorites/unacked – unquittierte Zählung (Admin: neue Favoriten; User: quittierte aber unbestätigte) +// GET /favorites/unacked – unquittierte Zählung (Admin: neue Requests; User: quittierte aber unbestätigte) router.get('/favorites/unacked', authenticate, (req, res) => { const isAdmin = req.user.role === 'admin'; if (isAdmin) { const count = db.prepare('SELECT COUNT(*) as n FROM movie_favorites WHERE acknowledged=0').get(); - res.json({ count: count.n, isAdmin: true }); + const newCount = db.prepare('SELECT COUNT(*) as n FROM movie_favorites WHERE acknowledged=0 AND admin_seen=0').get(); + res.json({ count: count.n, newCount: newCount.n, isAdmin: true }); } else { // User: Favoriten die vom Admin quittiert wurden aber User noch nicht gesehen hat const count = db.prepare('SELECT COUNT(*) as n FROM movie_favorites WHERE user_id=? AND acknowledged=1 AND user_notified=0').get(req.user.id); @@ -879,6 +880,14 @@ router.get('/favorites/unacked', authenticate, (req, res) => { } }); +// POST /favorites/mark-seen – Admin hat die offenen Requests angesehen (noch +// nicht zwingend quittiert) — nimmt ihnen das "neu"-Label im Dashboard +router.post('/favorites/mark-seen', authenticate, (req, res) => { + if (req.user.role !== 'admin') return res.status(403).json({ error: 'Kein Zugriff' }); + db.prepare('UPDATE movie_favorites SET admin_seen=1 WHERE acknowledged=0 AND admin_seen=0').run(); + res.json({ ok: true }); +}); + // ── Quittier-Logik (wiederverwendbar: HTTP-Routen + MQTT-Buttons) ─────────── @@ -993,7 +1002,7 @@ router.post('/favorites/:id/acknowledge', authenticate, requireAdmin, (req, res) router.post('/favorites/:id/unacknowledge', authenticate, requireAdmin, (req, res) => { const fav = db.prepare('SELECT * FROM movie_favorites WHERE id=?').get(req.params.id); if (!fav) return res.status(404).json({ error: 'Nicht gefunden' }); - db.prepare('UPDATE movie_favorites SET acknowledged=0, acknowledged_at=NULL, user_notified=0 WHERE id=?').run(fav.id); + db.prepare('UPDATE movie_favorites SET acknowledged=0, acknowledged_at=NULL, user_notified=0, admin_seen=0 WHERE id=?').run(fav.id); res.json({ ok: true }); // Pushover an den User — Quittierung zurückgezogen (fire & forget) try { diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 882a3a7..a25fb97 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -2070,7 +2070,9 @@ function Dashboard({ toast, mobile, setActive, setDevToolsNav, unreadMsgs=0, unr padding:'4px 10px', fontSize:9, fontFamily:'monospace', letterSpacing:1, }}> {unackedFavs?.isAdmin - ? `⭐ ${unackedFavs.count} neue Favorit${unackedFavs.count!==1?'en':''}` + ? ((unackedFavs.newCount || 0) > 0 + ? `🆕 ${unackedFavs.count} neue Request${unackedFavs.count!==1?'s':''}` + : `📋 ${unackedFavs.count} Request${unackedFavs.count!==1?'s':''}`) : `✅ ${unackedFavs.count} Favorit${unackedFavs.count!==1?'en':' wurde'} bestätigt` } @@ -3784,7 +3786,7 @@ export default function App() { 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(()=>{}); + const pollUnackedFavs = () => api('/tools/media/favorites/unacked').then(d=>setUnackedFavs({count:d.count||0,newCount:d.newCount||0,isAdmin:d.isAdmin})).catch(()=>{}); const setActive = (page) => { if (page === 'dashboard' && window.__newBuildAvailable && window.__safeReloadIfNewBuild?.()) return; @@ -3801,7 +3803,7 @@ export default function App() { const [unreadBoard, setUnreadBoard] = useState(0); const [unreadPromoted, setUnreadPromoted] = useState(0); const [unreadChangelog,setUnreadChangelog]= useState(0); - const [unackedFavs, setUnackedFavs] = useState({count:0,isAdmin:false}); + const [unackedFavs, setUnackedFavs] = useState({count:0,newCount:0,isAdmin:false}); const [mediaInitTab, setMediaInitTab] = useState(null); const [dashboardKey, setDashboardKey] = useState(0); @@ -3856,7 +3858,9 @@ export default function App() { useEffect(()=>{ if(!user)return; - pollUnackedFavs(); const ti=setInterval(pollUnackedFavs,30000); return()=>clearInterval(ti); + pollUnackedFavs(); const ti=setInterval(pollUnackedFavs,30000); + window.addEventListener('favorites-seen', pollUnackedFavs); + return()=>{ clearInterval(ti); window.removeEventListener('favorites-seen', pollUnackedFavs); }; },[user]); useEffect(()=>{ diff --git a/frontend/src/tools/media.jsx b/frontend/src/tools/media.jsx index b4b24bd..8110e16 100644 --- a/frontend/src/tools/media.jsx +++ b/frontend/src/tools/media.jsx @@ -768,7 +768,15 @@ function FavoritenKalender({ onOpenModal }) { api('/tools/media/favorites/all').catch(() => null), ]).then(([mine, all]) => { setFavs(mine); - if (all) { setAllFavs(all); setIsAdmin(true); } + if (all) { + setAllFavs(all); + setIsAdmin(true); + // Admin hat die offenen Requests jetzt gesehen — nimmt ihnen das + // "neu"-Label im Dashboard, bis sie tatsächlich quittiert werden + api('/tools/media/favorites/mark-seen', { method:'POST', body:{} }) + .then(() => window.dispatchEvent(new CustomEvent('favorites-seen'))) + .catch(() => {}); + } }).catch(e => setError(e.message)).finally(() => setLoading(false)); }, []);