diff --git a/backend/routes/admin.py b/backend/routes/admin.py index c4156da..8bcdff2 100644 --- a/backend/routes/admin.py +++ b/backend/routes/admin.py @@ -728,6 +728,65 @@ async def wiki_fetch_photos(limit: int = 50, user=Depends(require_mod)): # ------------------------------------------------------------------ # DELETE /api/admin/wiki/zuchter/{id} — Züchter-Eintrag löschen (Admin/Mod) # ------------------------------------------------------------------ +@router.get("/social") +async def admin_social_stats(user=Depends(require_mod)): + """Social-Media-Übersicht für Admins — alle Manager, alle Plattformen.""" + with db() as conn: + # Pro Manager: Name + Counts + managers = conn.execute(""" + SELECT u.id, u.name, + COUNT(sc.id) AS total, + COUNT(CASE WHEN sc.status='published' THEN 1 END) AS published, + COUNT(CASE WHEN sc.status='idea' THEN 1 END) AS ideas, + COUNT(CASE WHEN sc.status='scheduled' THEN 1 END) AS scheduled, + COUNT(CASE WHEN sc.post_url IS NOT NULL + AND sc.post_url != '' + AND sc.status='published' THEN 1 END) AS with_link + FROM users u + JOIN social_content sc ON sc.created_by = u.id + GROUP BY u.id + ORDER BY published DESC + """).fetchall() + + # Veröffentlichte Posts nach Plattform + by_platform = conn.execute(""" + SELECT platform, COUNT(*) AS n + FROM social_content + WHERE status='published' + GROUP BY platform + ORDER BY n DESC + """).fetchall() + + # Veröffentlichte Posts nach Monat (letzte 6 Monate) + by_month = conn.execute(""" + SELECT strftime('%Y-%m', published_at) AS monat, COUNT(*) AS n + FROM social_content + WHERE status='published' AND published_at IS NOT NULL + GROUP BY monat + ORDER BY monat DESC + LIMIT 6 + """).fetchall() + + # Letzte veröffentlichte Posts mit Link (für Abrechnung/Nachweis) + recent_published = conn.execute(""" + SELECT sc.id, sc.topic, sc.platform, sc.category, + sc.published_at, sc.post_url, sc.ai_score, + u.name AS manager + FROM social_content sc + LEFT JOIN users u ON u.id = sc.created_by + WHERE sc.status = 'published' + ORDER BY sc.published_at DESC + LIMIT 50 + """).fetchall() + + return { + "managers": [dict(r) for r in managers], + "by_platform": [dict(r) for r in by_platform], + "by_month": [dict(r) for r in by_month], + "recent_published": [dict(r) for r in recent_published], + } + + @router.delete("/wiki/zuchter/{zuchter_id}", status_code=204) async def admin_delete_zuchter(zuchter_id: int, user=Depends(require_mod)): with db() as conn: diff --git a/backend/static/js/pages/admin.js b/backend/static/js/pages/admin.js index 8ed377d..5d41cf4 100644 --- a/backend/static/js/pages/admin.js +++ b/backend/static/js/pages/admin.js @@ -14,6 +14,7 @@ window.Page_admin = (() => { { id: 'nutzer', label: 'Nutzer', icon: 'users' }, { id: 'moderation', label: 'Moderation', icon: 'shield-check' }, { id: 'forum', label: 'Forum & Meldungen', icon: 'chat-circle-dots' }, + { id: 'social', label: 'Social Media', icon: 'camera' }, { id: 'analytics', label: 'Analytics', icon: 'target' }, { id: 'system', label: 'System', icon: 'gear' }, { id: 'jobs', label: 'Jobs', icon: 'clock' }, @@ -81,6 +82,7 @@ window.Page_admin = (() => { case 'nutzer': await _renderUsers(el); break; case 'moderation': await _renderModeration(el); break; case 'forum': await _renderForum(el); break; + case 'social': await _renderSocial(el); break; case 'analytics': await _renderAnalytics(el); break; case 'system': await _renderSystem(el); break; case 'jobs': await _renderJobs(el); break; @@ -91,6 +93,126 @@ window.Page_admin = (() => { } } + // ------------------------------------------------------------------ + // TAB: SOCIAL MEDIA + async function _renderSocial(el) { + const d = await API.get('/admin/social'); + + const _PL = { instagram: '📸 Instagram', tiktok: '🎵 TikTok', both: '📱 Beide' }; + const _fmt = iso => iso ? new Date(iso).toLocaleDateString('de-DE', {day:'2-digit',month:'2-digit',year:'numeric'}) : '–'; + + // Manager-Tabelle + const managerRows = d.managers.map(m => ` + + ${_esc(m.name)} + ${m.published} + ${m.with_link} + ${m.published > 0 ? ` + (${Math.round(m.with_link/m.published*100)}%)` : ''} + ${m.scheduled} + ${m.ideas} + ${m.total} + `).join(''); + + // Plattform-Balken + const maxPlat = Math.max(...d.by_platform.map(p => p.n), 1); + const platBars = d.by_platform.map(p => ` +
+
${_PL[p.platform]||p.platform}
+
+
+
+
${p.n}
+
`).join(''); + + // Monats-Timeline + const maxMonth = Math.max(...d.by_month.map(m => m.n), 1); + const monthBars = [...d.by_month].reverse().map(m => ` +
+
${m.monat}
+
+
+
+
${m.n}
+
`).join(''); + + // Veröffentlichte Posts (mit/ohne Link) + const postRows = d.recent_published.map(p => ` + + ${_fmt(p.published_at)} + ${_esc(p.topic)} + ${_PL[p.platform]||p.platform||'–'} + ${_esc(p.category||'–')} + ${p.ai_score ? '⭐'.repeat(p.ai_score) : '–'} + ${_esc(p.manager||'–')} + ${p.post_url + ? `🔗 Link` + : ``} + `).join(''); + + el.innerHTML = ` +
+ +
+
+ Veröffentlicht nach Plattform +
+ ${platBars || '
Noch keine Posts
'} +
+ +
+
+ Posts pro Monat +
+ ${monthBars || '
Noch keine Posts
'} +
+
+ + + ${d.managers.length ? ` +
+
Manager-Übersicht
+
+ + + + + + + + + + ${managerRows} +
ManagerVeröffentlichtMit LinkGeplantIdeenGesamt
+
+
` : ''} + + +
+
+ Veröffentlichte Posts (letzte 50) +
+
+ + + + + + ${postRows || ``} +
DatumThemaPlattformKategorieScoreManagerLink
Noch keine Posts
+
+
`; + } + // ------------------------------------------------------------------ // TAB: ANALYTICS async function _renderAnalytics(el) { diff --git a/backend/static/sw.js b/backend/static/sw.js index 620e75f..93bc520 100644 --- a/backend/static/sw.js +++ b/backend/static/sw.js @@ -3,7 +3,7 @@ Offline-Cache + Push Notifications + Tile-Cache ============================================================ */ -const CACHE_VERSION = 'by-v369'; +const CACHE_VERSION = 'by-v370'; const CACHE_STATIC = `${CACHE_VERSION}-static`; const CACHE_TILES = 'ban-yaro-tiles-v1'; // bleibt über SW-Updates erhalten