Admin: Social-Media-Tab mit Manager-Übersicht, Plattform-Auswertung und Post-Nachweis, SW by-v370
This commit is contained in:
parent
e2bb1a4b2d
commit
4f58a784c7
3 changed files with 182 additions and 1 deletions
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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 => `
|
||||
<tr>
|
||||
<td style="font-weight:600">${_esc(m.name)}</td>
|
||||
<td style="text-align:right">${m.published}</td>
|
||||
<td style="text-align:right">${m.with_link}
|
||||
${m.published > 0 ? `<span style="font-size:10px;color:var(--c-text-muted)">
|
||||
(${Math.round(m.with_link/m.published*100)}%)</span>` : ''}</td>
|
||||
<td style="text-align:right">${m.scheduled}</td>
|
||||
<td style="text-align:right">${m.ideas}</td>
|
||||
<td style="text-align:right;color:var(--c-text-muted)">${m.total}</td>
|
||||
</tr>`).join('');
|
||||
|
||||
// Plattform-Balken
|
||||
const maxPlat = Math.max(...d.by_platform.map(p => p.n), 1);
|
||||
const platBars = d.by_platform.map(p => `
|
||||
<div style="display:flex;align-items:center;gap:var(--space-3);margin-bottom:var(--space-2)">
|
||||
<div style="width:120px;flex-shrink:0;font-size:var(--text-sm)">${_PL[p.platform]||p.platform}</div>
|
||||
<div style="flex:1;background:var(--c-surface-2);border-radius:var(--radius-full);height:8px;overflow:hidden">
|
||||
<div style="width:${Math.round(p.n/maxPlat*100)}%;height:100%;
|
||||
background:var(--c-primary);border-radius:var(--radius-full)"></div>
|
||||
</div>
|
||||
<div style="width:28px;text-align:right;font-weight:600;font-size:var(--text-sm)">${p.n}</div>
|
||||
</div>`).join('');
|
||||
|
||||
// Monats-Timeline
|
||||
const maxMonth = Math.max(...d.by_month.map(m => m.n), 1);
|
||||
const monthBars = [...d.by_month].reverse().map(m => `
|
||||
<div style="display:flex;align-items:center;gap:var(--space-3);margin-bottom:var(--space-2)">
|
||||
<div style="width:55px;flex-shrink:0;font-size:11px;color:var(--c-text-muted)">${m.monat}</div>
|
||||
<div style="flex:1;background:var(--c-surface-2);border-radius:var(--radius-full);height:8px;overflow:hidden">
|
||||
<div style="width:${Math.round(m.n/maxMonth*100)}%;height:100%;
|
||||
background:var(--c-success);border-radius:var(--radius-full)"></div>
|
||||
</div>
|
||||
<div style="width:28px;text-align:right;font-weight:600;font-size:var(--text-sm)">${m.n}</div>
|
||||
</div>`).join('');
|
||||
|
||||
// Veröffentlichte Posts (mit/ohne Link)
|
||||
const postRows = d.recent_published.map(p => `
|
||||
<tr>
|
||||
<td style="color:var(--c-text-muted);white-space:nowrap">${_fmt(p.published_at)}</td>
|
||||
<td style="font-weight:500;max-width:200px;overflow:hidden;text-overflow:ellipsis;
|
||||
white-space:nowrap">${_esc(p.topic)}</td>
|
||||
<td>${_PL[p.platform]||p.platform||'–'}</td>
|
||||
<td style="font-size:10px;color:var(--c-text-muted)">${_esc(p.category||'–')}</td>
|
||||
<td>${p.ai_score ? '⭐'.repeat(p.ai_score) : '–'}</td>
|
||||
<td style="font-weight:500">${_esc(p.manager||'–')}</td>
|
||||
<td>${p.post_url
|
||||
? `<a href="${_esc(p.post_url)}" target="_blank" rel="noopener"
|
||||
style="font-size:11px;color:var(--c-primary)">🔗 Link</a>`
|
||||
: `<span style="font-size:11px;color:var(--c-text-muted)">–</span>`}</td>
|
||||
</tr>`).join('');
|
||||
|
||||
el.innerHTML = `
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:var(--space-4);margin-bottom:var(--space-4)">
|
||||
<!-- Plattform -->
|
||||
<div class="card" style="padding:var(--space-4)">
|
||||
<div style="font-size:var(--text-sm);font-weight:700;margin-bottom:var(--space-3)">
|
||||
Veröffentlicht nach Plattform
|
||||
</div>
|
||||
${platBars || '<div style="color:var(--c-text-muted);font-size:var(--text-sm)">Noch keine Posts</div>'}
|
||||
</div>
|
||||
<!-- Timeline -->
|
||||
<div class="card" style="padding:var(--space-4)">
|
||||
<div style="font-size:var(--text-sm);font-weight:700;margin-bottom:var(--space-3)">
|
||||
Posts pro Monat
|
||||
</div>
|
||||
${monthBars || '<div style="color:var(--c-text-muted);font-size:var(--text-sm)">Noch keine Posts</div>'}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Manager -->
|
||||
${d.managers.length ? `
|
||||
<div class="card adm-table-card" style="margin-bottom:var(--space-4)">
|
||||
<div style="padding:var(--space-3) var(--space-4);font-size:var(--text-xs);font-weight:700;
|
||||
text-transform:uppercase;letter-spacing:.05em;color:var(--c-text-secondary);
|
||||
border-bottom:1px solid var(--c-border)">Manager-Übersicht</div>
|
||||
<div class="adm-table-scroll">
|
||||
<table class="adm-table">
|
||||
<thead><tr>
|
||||
<th>Manager</th>
|
||||
<th style="text-align:right">Veröffentlicht</th>
|
||||
<th style="text-align:right">Mit Link</th>
|
||||
<th style="text-align:right">Geplant</th>
|
||||
<th style="text-align:right">Ideen</th>
|
||||
<th style="text-align:right">Gesamt</th>
|
||||
</tr></thead>
|
||||
<tbody>${managerRows}</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>` : ''}
|
||||
|
||||
<!-- Alle veröffentlichten Posts -->
|
||||
<div class="card adm-table-card">
|
||||
<div style="padding:var(--space-3) var(--space-4);font-size:var(--text-xs);font-weight:700;
|
||||
text-transform:uppercase;letter-spacing:.05em;color:var(--c-text-secondary);
|
||||
border-bottom:1px solid var(--c-border)">
|
||||
Veröffentlichte Posts (letzte 50)
|
||||
</div>
|
||||
<div class="adm-table-scroll">
|
||||
<table class="adm-table">
|
||||
<thead><tr>
|
||||
<th>Datum</th><th>Thema</th><th>Plattform</th>
|
||||
<th>Kategorie</th><th>Score</th><th>Manager</th><th>Link</th>
|
||||
</tr></thead>
|
||||
<tbody>${postRows || `<tr><td colspan="7" style="text-align:center;
|
||||
color:var(--c-text-muted);padding:var(--space-6)">Noch keine Posts</td></tr>`}</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// TAB: ANALYTICS
|
||||
async function _renderAnalytics(el) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue