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)
|
# 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)
|
@router.delete("/wiki/zuchter/{zuchter_id}", status_code=204)
|
||||||
async def admin_delete_zuchter(zuchter_id: int, user=Depends(require_mod)):
|
async def admin_delete_zuchter(zuchter_id: int, user=Depends(require_mod)):
|
||||||
with db() as conn:
|
with db() as conn:
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ window.Page_admin = (() => {
|
||||||
{ id: 'nutzer', label: 'Nutzer', icon: 'users' },
|
{ id: 'nutzer', label: 'Nutzer', icon: 'users' },
|
||||||
{ id: 'moderation', label: 'Moderation', icon: 'shield-check' },
|
{ id: 'moderation', label: 'Moderation', icon: 'shield-check' },
|
||||||
{ id: 'forum', label: 'Forum & Meldungen', icon: 'chat-circle-dots' },
|
{ id: 'forum', label: 'Forum & Meldungen', icon: 'chat-circle-dots' },
|
||||||
|
{ id: 'social', label: 'Social Media', icon: 'camera' },
|
||||||
{ id: 'analytics', label: 'Analytics', icon: 'target' },
|
{ id: 'analytics', label: 'Analytics', icon: 'target' },
|
||||||
{ id: 'system', label: 'System', icon: 'gear' },
|
{ id: 'system', label: 'System', icon: 'gear' },
|
||||||
{ id: 'jobs', label: 'Jobs', icon: 'clock' },
|
{ id: 'jobs', label: 'Jobs', icon: 'clock' },
|
||||||
|
|
@ -81,6 +82,7 @@ window.Page_admin = (() => {
|
||||||
case 'nutzer': await _renderUsers(el); break;
|
case 'nutzer': await _renderUsers(el); break;
|
||||||
case 'moderation': await _renderModeration(el); break;
|
case 'moderation': await _renderModeration(el); break;
|
||||||
case 'forum': await _renderForum(el); break;
|
case 'forum': await _renderForum(el); break;
|
||||||
|
case 'social': await _renderSocial(el); break;
|
||||||
case 'analytics': await _renderAnalytics(el); break;
|
case 'analytics': await _renderAnalytics(el); break;
|
||||||
case 'system': await _renderSystem(el); break;
|
case 'system': await _renderSystem(el); break;
|
||||||
case 'jobs': await _renderJobs(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
|
// TAB: ANALYTICS
|
||||||
async function _renderAnalytics(el) {
|
async function _renderAnalytics(el) {
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
Offline-Cache + Push Notifications + Tile-Cache
|
Offline-Cache + Push Notifications + Tile-Cache
|
||||||
============================================================ */
|
============================================================ */
|
||||||
|
|
||||||
const CACHE_VERSION = 'by-v369';
|
const CACHE_VERSION = 'by-v370';
|
||||||
const CACHE_STATIC = `${CACHE_VERSION}-static`;
|
const CACHE_STATIC = `${CACHE_VERSION}-static`;
|
||||||
const CACHE_TILES = 'ban-yaro-tiles-v1'; // bleibt über SW-Updates erhalten
|
const CACHE_TILES = 'ban-yaro-tiles-v1'; // bleibt über SW-Updates erhalten
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue