diff --git a/backend/src/tools/whiteboard/routes.js b/backend/src/tools/whiteboard/routes.js
index 6f9540d..b7c3d06 100644
--- a/backend/src/tools/whiteboard/routes.js
+++ b/backend/src/tools/whiteboard/routes.js
@@ -76,6 +76,13 @@ router.get('/unread', authenticate, (req, res) => {
res.json({ count });
});
+// ── Unread IDs (welche Whiteboards konkret) ───────────────────────────────────
+router.get('/unread-ids', authenticate, (req, res) => {
+ const me = uid(req);
+ const rows = db.prepare('SELECT whiteboard_id FROM whiteboard_unread WHERE user_id=?').all(me);
+ res.json({ ids: rows.map(r => r.whiteboard_id) });
+});
+
// ── Liste ─────────────────────────────────────────────────────────────────────
router.get('/', authenticate, (req, res) => {
const me = uid(req);
diff --git a/frontend/src/tools/media.jsx b/frontend/src/tools/media.jsx
index 7d104c8..4ef0b2f 100644
--- a/frontend/src/tools/media.jsx
+++ b/frontend/src/tools/media.jsx
@@ -678,6 +678,50 @@ function FavCard({ f, onOpenModal, onRemove }) {
}
// ── Favoriten Kalender ────────────────────────────────────────────────────────
+// ── Admin User Group mit einklappbaren quittierten Favoriten ─────────────────
+function AdminUserGroup({ username, unackedItems, ackedItems, FavRow }) {
+ const [ackedOpen, setAckedOpen] = useState(false);
+ const total = unackedItems.length + ackedItems.length;
+ return (
+
+
+
+ 👤 {username}
+
+
+ {total} Favorit{total!==1?'en':''}
+
+ {unackedItems.length > 0 && (
+
+ {unackedItems.length} neu
+
+ )}
+
+
+ {unackedItems.map(f => )}
+
+ {ackedItems.length > 0 && (
+
+
+ {ackedOpen && (
+
+ {ackedItems.map(f => )}
+
+ )}
+
+ )}
+
+ );
+}
+
function FavoritenKalender({ onOpenModal }) {
const [favs, setFavs] = useState([]);
const [allFavs, setAllFavs] = useState([]);
@@ -712,7 +756,7 @@ function FavoritenKalender({ onOpenModal }) {
if (loading) return ⏳ Lädt…;
if (error) return ;
- // Admin-Ansicht: alle Favoriten nach User gruppiert
+ // Admin-Ansicht: alle Favoriten nach User gruppiert, quittierte eingeklappt
if (isAdmin && adminTab) {
const unacked = allFavs.filter(f => !f.acknowledged).length;
const userGroups = {};
@@ -720,6 +764,52 @@ function FavoritenKalender({ onOpenModal }) {
if (!userGroups[f.username]) userGroups[f.username] = [];
userGroups[f.username].push(f);
});
+
+ // FavRow: einzelne Favoriten-Zeile mit Modal-Klick
+ const FavRow = ({ f }) => (
+ onOpenModal({ id: f.tmdb_id, media_type: f.media_type || 'movie' })}>
+ {f.poster_path
+ ?

+ :
+ {f.media_type==='tv'?'📺':'🎬'}
+
+ }
+
+
+ {f.title}
+ {f.media_type==='tv' && SERIE}
+
+
+ {f.added_at ? new Date(f.added_at).toLocaleDateString('de-DE') : '—'}
+ {!!f.acknowledged && ✓ quittiert}
+
+
+ {!f.acknowledged ? (
+
+ ) : (
+
+ )}
+
+ );
+
return (
@@ -744,69 +834,13 @@ function FavoritenKalender({ onOpenModal }) {
{!allFavs.length
?
Noch keine Favoriten von irgendjemandem.
- : Object.entries(userGroups).map(([username, items]) => (
-
-
-
- 👤 {username}
-
-
- {items.length} Favorit{items.length!==1?'en':''}
-
- {items.filter(f=>!f.acknowledged).length > 0 && (
-
- {items.filter(f=>!f.acknowledged).length} neu
-
- )}
-
-
- {items.map(f => (
-
- {f.poster_path
- ?

- :
- {f.media_type==='tv'?'📺':'🎬'}
-
- }
-
-
- {f.title}
- {f.media_type==='tv' && SERIE}
-
-
- ⭐ {f.added_at ? new Date(f.added_at).toLocaleDateString('de-DE') : '—'}
- {!!f.acknowledged && ✓}
-
-
- {!f.acknowledged ? (
-
- ) : (
-
- )}
-
- ))}
-
-
- ))
+ : Object.entries(userGroups).map(([username, items]) => {
+ const unackedItems = items.filter(f => !f.acknowledged);
+ const ackedItems = items.filter(f => f.acknowledged);
+ return (
+
+ );
+ })
}
);
diff --git a/frontend/src/tools/whiteboard.jsx b/frontend/src/tools/whiteboard.jsx
index 3784b45..43791b8 100644
--- a/frontend/src/tools/whiteboard.jsx
+++ b/frontend/src/tools/whiteboard.jsx
@@ -265,6 +265,7 @@ export default function Whiteboard({ toast, mobile }) {
// View
const [view, setView] = useState('list');
const [boards, setBoards] = useState([]);
+ const [unreadIds, setUnreadIds] = useState(new Set());
const [loading, setLoading] = useState(true);
const [current, setCurrent] = useState(null);
const [creating, setCreating] = useState(false);
@@ -379,7 +380,16 @@ export default function Whiteboard({ toast, mobile }) {
// ── Laden / Speichern ────────────────────────────────────────────────────
const loadBoards = useCallback(() => {
setLoading(true);
- api('/tools/whiteboard').then(d=>setBoards(d.whiteboards||[])).catch(()=>toast('Fehler','error')).finally(()=>setLoading(false));
+ Promise.all([
+ api('/tools/whiteboard'),
+ api('/tools/whiteboard/unread').catch(() => ({ count: 0 })),
+ ]).then(([d, u]) => {
+ setBoards(d.whiteboards || []);
+ // unread count kommt global — wir markieren alle als "möglicherweise unread"
+ // getGameForUser gibt uns keine IDs, also nutzen wir den globalen count als Indikator
+ }).catch(() => toast('Fehler', 'error')).finally(() => setLoading(false));
+ // Einzelne unread IDs vom Backend holen
+ api('/tools/whiteboard/unread-ids').then(d => setUnreadIds(new Set(d.ids || []))).catch(() => {});
}, []);
const renameBoard = async (id, title) => {
@@ -394,6 +404,7 @@ export default function Whiteboard({ toast, mobile }) {
useEffect(()=>{ loadBoards(); },[loadBoards]);
const openBoard = async (board) => {
+ setUnreadIds(prev => { const s = new Set(prev); s.delete(board.id); return s; });
try {
const d = await api(`/tools/whiteboard/${board.id}/data`);
const els = JSON.parse(typeof d.elements==='string'?d.elements:JSON.stringify(d.elements||[]));
@@ -684,7 +695,13 @@ export default function Whiteboard({ toast, mobile }) {
{boards.map(b=>(
{ if(editingId!==b.id) openBoard(b); }}>
-
🖊
+
+ 🖊
+ {unreadIds.has(b.id) && (
+
+ )}
+
{editingId===b.id?(
setEditTitle(e.target.value)}