From d015fcbe6f96f2405873d5d1711eabbb143b62d6 Mon Sep 17 00:00:00 2001 From: AzuTear Date: Thu, 18 Jun 2026 14:29:40 +0200 Subject: [PATCH] Refine admin panel workflows --- Backend/Program.cs | 2 +- frontend/src/lib/adminMetrics.ts | 10 + frontend/src/stores/awards.ts | 18 +- .../src/views/admin/AdminAnalyticsView.vue | 3 +- .../src/views/admin/AdminCandidatesView.vue | 55 +- frontend/src/views/admin/AdminClipsView.vue | 105 +++- .../src/views/admin/AdminDashboardView.vue | 6 +- frontend/src/views/admin/AdminLayoutView.vue | 7 +- frontend/src/views/admin/AdminReviewsView.vue | 14 +- frontend/src/views/admin/AdminRiskView.vue | 73 +-- frontend/src/views/admin/AdminSeasonsView.vue | 471 +++++------------- .../src/views/admin/AdminSettingsView.vue | 177 +++---- .../src/views/admin/AdminUsersLogsView.vue | 53 +- frontend/src/views/admin/AdminVotingView.vue | 85 ++-- 14 files changed, 481 insertions(+), 598 deletions(-) create mode 100644 frontend/src/lib/adminMetrics.ts 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

-

Award-Jahr steuern

+

{{ seasonDetail.name || 'Kein Jahr gewählt' }}

- 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.

-
- {{ seasonForm.isCurrent ? 'Öffentlich aktiv' : 'Intern vorbereitet' }} +
+ {{ form.isCurrent ? 'Öffentlich aktiv' : 'Intern vorbereitet' }}
+
+ +
+ -
-
-

Jahr

- {{ seasonDetail.year || '-' }} -
-
-

Kategorien

- {{ seasonDetail.categories.length }} -
-
-

Kandidaten

- {{ seasonDetail.candidates.length }} -
-
- -
-

- Speichert Phase und Public-Status für {{ seasonDetail.name }}. -

- -
-

{{ adminMessage }}

{{ adminError }}

-
- - -
-
-
- -
-
-

Neue Kategorie

-

Kategorie planen

-

- Lege zuerst Gruppe, Namen und Limit fest. Slug und Sortierung bestimmen später URL, Anzeige und Reihenfolge im Voting. -

-
-
-
- -
-
- - -
- -