diff --git a/Backend/Program.cs b/Backend/Program.cs index 8a841da..49ec0e7 100644 --- a/Backend/Program.cs +++ b/Backend/Program.cs @@ -758,7 +758,7 @@ app.MapGet("/api/admin/dashboard", async (HttpContext context, AwardsDbContext d new[] { new AdminMetricDto("Nominierungen", nominationCount, "+12.4% vs. gestern"), - new AdminMetricDto("Votes", voteCount, "+8.7% vs. gestern"), + new AdminMetricDto("Stimmen", voteCount, "+8.7% vs. gestern"), new AdminMetricDto("Kategorien", categoryCount, "aktiv im aktuellen Jahr"), new AdminMetricDto("Reviews offen", reviewCount, "Freitext und Dubletten"), }, diff --git a/frontend/src/lib/adminMetrics.ts b/frontend/src/lib/adminMetrics.ts new file mode 100644 index 0000000..2ea8186 --- /dev/null +++ b/frontend/src/lib/adminMetrics.ts @@ -0,0 +1,10 @@ +import type { AdminMetric } from '../types/awards' + +export function getMetricValue(metrics: AdminMetric[], labels: string[]) { + const normalizedLabels = labels.map((label) => label.toLowerCase()) + return metrics.find((metric) => normalizedLabels.includes(metric.label.toLowerCase()))?.value ?? 0 +} + +export function getVoteMetricValue(metrics: AdminMetric[]) { + return getMetricValue(metrics, ['Stimmen', 'Votes']) +} diff --git a/frontend/src/stores/awards.ts b/frontend/src/stores/awards.ts index 94e945d..fe8d7cc 100644 --- a/frontend/src/stores/awards.ts +++ b/frontend/src/stores/awards.ts @@ -216,6 +216,20 @@ const emptyAdminSeasonDetail: AdminSeasonDetailResponse = { clipSubmissions: [], } +/** + * Guarantee the array fields exist even if a (possibly older) backend omits them, + * so views can safely read `.length`/`.filter` without crashing the render. + */ +function normalizeSeasonDetail(detail: AdminSeasonDetailResponse): AdminSeasonDetailResponse { + return { + ...detail, + categories: detail.categories ?? [], + candidates: detail.candidates ?? [], + pendingNominations: detail.pendingNominations ?? [], + clipSubmissions: detail.clipSubmissions ?? [], + } +} + export const useAwardsStore = defineStore('awards', { state: () => ({ overview: fallbackOverview as OverviewResponse, @@ -259,7 +273,7 @@ export const useAwardsStore = defineStore('awards', { } if (this.adminSelectedSeasonId) { - this.adminSeasonDetail = await api.getAdminSeasonDetail(this.adminSelectedSeasonId) + this.adminSeasonDetail = normalizeSeasonDetail(await api.getAdminSeasonDetail(this.adminSelectedSeasonId)) } this.apiMode = 'api' } catch { @@ -272,7 +286,7 @@ export const useAwardsStore = defineStore('awards', { async loadAdminSeasonDetail(seasonId: number) { try { this.adminSelectedSeasonId = seasonId - this.adminSeasonDetail = await api.getAdminSeasonDetail(seasonId) + this.adminSeasonDetail = normalizeSeasonDetail(await api.getAdminSeasonDetail(seasonId)) this.apiMode = 'api' } catch { this.adminSeasonDetail = emptyAdminSeasonDetail diff --git a/frontend/src/views/admin/AdminAnalyticsView.vue b/frontend/src/views/admin/AdminAnalyticsView.vue index ee8f874..9dfd6b5 100644 --- a/frontend/src/views/admin/AdminAnalyticsView.vue +++ b/frontend/src/views/admin/AdminAnalyticsView.vue @@ -5,11 +5,12 @@ import { BarChart3, Clock3, Sparkles, Tags, Users, Vote } from '@lucide/vue' import AdminPageHeader from '../../components/admin/AdminPageHeader.vue' import AdminSeasonToolbar from '../../components/admin/AdminSeasonToolbar.vue' import Card from '../../components/ui/Card.vue' +import { getVoteMetricValue } from '../../lib/adminMetrics' import { useAwardsStore } from '../../stores/awards' const store = useAwardsStore() const seasonDetail = computed(() => store.adminSeasonDetail) -const totalVotes = computed(() => store.admin.metrics.find((metric) => metric.label === 'Stimmen')?.value ?? 0) +const totalVotes = computed(() => getVoteMetricValue(store.admin.metrics)) const totalNominations = computed(() => store.admin.metrics.find((metric) => metric.label === 'Nominierungen')?.value ?? 0) const maxVotes = computed(() => Math.max(...store.admin.topCategories.map((category) => category.votes), 1)) const categoryHealth = computed(() => diff --git a/frontend/src/views/admin/AdminCandidatesView.vue b/frontend/src/views/admin/AdminCandidatesView.vue index 205e138..cf34e8f 100644 --- a/frontend/src/views/admin/AdminCandidatesView.vue +++ b/frontend/src/views/admin/AdminCandidatesView.vue @@ -1,7 +1,7 @@ @@ -161,272 +88,142 @@ async function createCategory() {
Jahresstatus
-- Hier legst du fest, in welcher Phase das Jahr ist und ob genau dieses Jahr öffentlich für Community, Voting und Archiv sichtbar ist. + Der Status steuert die Admin-Orientierung und den Public-Kontext. Inhaltliche Pflege passiert über Kategorien, Kandidaten, Reviews und Clips.
Jahr
- {{ seasonDetail.year || '-' }} -Kategorien
- {{ seasonDetail.categories.length }} -Kandidaten
- {{ seasonDetail.candidates.length }} -- Speichert Phase und Public-Status für {{ seasonDetail.name }}. -
- -{{ adminMessage }}
{{ adminError }}
-Neue Kategorie
-- Lege zuerst Gruppe, Namen und Limit fest. Slug und Sortierung bestimmen später URL, Anzeige und Reihenfolge im Voting. -
-- Neue Kategorien sind sofort Teil des gewählten Award-Jahres und können danach unten weiter bearbeitet werden. -
-Season Snapshot
++ Schneller Überblick, ob das gewählte Jahr bereit für die nächste Award-Phase ist. +
+{{ item.label }}
+{{ item.note }}
+Kategorien
-- Prüfe Struktur, Slug, Limit und Kandidatenzahl pro Kategorie. Erst filtern, dann gezielt bearbeiten. -
-{{ stat.label }}
- {{ stat.value }} -Alle Jahre
+{{ category.description }}
-- Keine Kategorien passen zum aktuellen Filter. -
-Kategorie bearbeiten
-{{ selectedCategory.description }}
-+ Noch keine Award-Jahre aus der API geladen. +
Award-Jahr
-- Diese Einstellungen werden gespeichert und beeinflussen, welches Jahr als aktueller Public-Kontext gilt. -
- -{{ adminMessage }}
-{{ adminError }}
- -Checks
-{{ check.label }}
-{{ check.note }}
-{{ check.label }}
+{{ check.note }}
+Feature Gates
-Feature Gates
++ Die Gates sind aus aktuellen Daten abgeleitet und verlinken zum Ort, an dem der Zustand behoben wird. +
+Auffällige User
-Aktionstypen
+{{ user.twitchUserId }}
-{{ user.type }} · {{ user.ip }}
+{{ item.action }}
+Audit-Kategorie für Team-Aktionen
- Keine auffälligen User für den aktuellen Filter. +
+ Noch keine Aktionstypen vorhanden.
Ranking
-#{{ index + 1 }}
-- Noch keine Voting-Daten vorhanden. -
-Readiness
@@ -115,7 +98,7 @@ const votingChecklist = computed(() => [{{ category.name }}
-{{ category.groupName }} · {{ category.candidateCount }} Kandidaten
+{{ category.groupName }} · {{ category.candidateCount }} Kandidaten · {{ category.reviewCount }} Reviews
Sperrgründe
+{{ category.name }}
++ {{ category.candidateCount === 0 ? 'Keine Kandidaten gepflegt.' : `${category.reviewCount} offene Reviews vor Voting-Freigabe.` }} +
++ Keine Kategorie ist durch Inhalt oder Review-Backlog blockiert. +
+Voting Checkliste