Arbeitskontext
Die gewaehlte Season steuert Kategorien, Kandidaten und Review-Queues im gesamten Admin-Bereich.
+
+
+ {{ currentSeason.currentPhase || 'Kein Status' }}
+
+
+ {{ currentSeason.isCurrent ? 'Public Season' : 'Nicht aktiv' }}
+
+
+
Season
(path: string): Promise {
Authorization: `Bearer ${token}`,
}
: undefined,
+ }).catch(() => {
+ throw new Error(`API nicht erreichbar (${API_URL}). Bitte Backend starten.`)
})
if (!response.ok) {
throw new Error(`API request failed for ${path}`)
@@ -47,6 +49,8 @@ async function sendJson(path: string, method: 'POST' | 'PUT', body: u
...(token ? { Authorization: `Bearer ${token}` } : {}),
},
body: JSON.stringify(body),
+ }).catch(() => {
+ throw new Error(`API nicht erreichbar (${API_URL}). Bitte Backend starten.`)
})
if (!response.ok) {
diff --git a/frontend/src/router.ts b/frontend/src/router.ts
index 0f1ddec..cb1c6cb 100644
--- a/frontend/src/router.ts
+++ b/frontend/src/router.ts
@@ -29,6 +29,7 @@ const router = createRouter({
component: NominationsView,
meta: {
requiresAuth: true,
+ keepAlive: true,
},
},
{
@@ -37,6 +38,7 @@ const router = createRouter({
component: VotingView,
meta: {
requiresAuth: true,
+ keepAlive: true,
},
},
{
@@ -59,26 +61,41 @@ const router = createRouter({
path: 'dashboard',
name: 'admin-dashboard',
component: AdminDashboardView,
+ meta: {
+ keepAlive: true,
+ },
},
{
path: 'seasons',
name: 'admin-seasons',
component: AdminSeasonsView,
+ meta: {
+ keepAlive: true,
+ },
},
{
path: 'candidates',
name: 'admin-candidates',
component: AdminCandidatesView,
+ meta: {
+ keepAlive: true,
+ },
},
{
path: 'reviews',
name: 'admin-reviews',
component: AdminReviewsView,
+ meta: {
+ keepAlive: true,
+ },
},
{
path: 'risk',
name: 'admin-risk',
component: AdminRiskView,
+ meta: {
+ keepAlive: true,
+ },
},
],
},
diff --git a/frontend/src/views/admin/AdminCandidatesView.vue b/frontend/src/views/admin/AdminCandidatesView.vue
index e5a7f85..fa715ee 100644
--- a/frontend/src/views/admin/AdminCandidatesView.vue
+++ b/frontend/src/views/admin/AdminCandidatesView.vue
@@ -2,6 +2,7 @@
import { computed, reactive, ref, watch } from 'vue'
import Select from 'primevue/select'
+import AdminPageHeader from '../../components/admin/AdminPageHeader.vue'
import AdminSeasonToolbar from '../../components/admin/AdminSeasonToolbar.vue'
import Button from '../../components/ui/Button.vue'
import Card from '../../components/ui/Card.vue'
@@ -28,12 +29,26 @@ const candidateForms = reactive store.adminSeasonDetail)
const selectedSeasonId = computed(() => store.adminSelectedSeasonId)
+const candidateFilter = ref('')
const categoryOptions = computed(() =>
seasonDetail.value.categories.map((category) => ({
label: `${category.groupName} · ${category.name}`,
value: category.id,
})),
)
+const categoryLabelMap = computed(() =>
+ Object.fromEntries(seasonDetail.value.categories.map((category) => [category.id, `${category.groupName} · ${category.name}`])),
+)
+const filteredCandidates = computed(() => {
+ const query = candidateFilter.value.trim().toLowerCase()
+ if (!query) return seasonDetail.value.candidates
+ return seasonDetail.value.candidates.filter((candidate) =>
+ [candidate.displayName, candidate.channelSlug, candidate.platform, categoryLabelMap.value[candidate.categoryId] ?? '']
+ .join(' ')
+ .toLowerCase()
+ .includes(query),
+ )
+})
watch(
seasonDetail,
@@ -92,6 +107,12 @@ async function createCandidate() {
+
+
@@ -102,13 +123,25 @@ async function createCandidate() {
Bekannte Kandidaten koennen pro Kategorie gepflegt und fuer Voting und Archiv genutzt werden.
- {{ seasonDetail.candidates.length }} Kandidaten
+ {{ filteredCandidates.length }} / {{ seasonDetail.candidates.length }} Kandidaten
+
+
+
+ Tipp: Nutze erst den Filter und aendere danach nur den Zielkandidaten.
+
+
+
@@ -131,6 +164,10 @@ async function createCandidate() {
+
+
+ Keine Kandidaten passen zum aktuellen Filter.
+
diff --git a/frontend/src/views/admin/AdminDashboardView.vue b/frontend/src/views/admin/AdminDashboardView.vue
index 7961f04..2e3933c 100644
--- a/frontend/src/views/admin/AdminDashboardView.vue
+++ b/frontend/src/views/admin/AdminDashboardView.vue
@@ -2,7 +2,9 @@
import { computed } from 'vue'
import Column from 'primevue/column'
import DataTable from 'primevue/datatable'
+import { RouterLink } from 'vue-router'
+import AdminPageHeader from '../../components/admin/AdminPageHeader.vue'
import Card from '../../components/ui/Card.vue'
import { useAwardsStore } from '../../stores/awards'
@@ -11,10 +13,55 @@ const store = useAwardsStore()
const metrics = computed(() => store.admin.metrics)
const activities = computed(() => store.admin.activities)
const topCategories = computed(() => store.admin.topCategories)
+const quickActions = computed(() => [
+ {
+ label: 'Offene Reviews',
+ value: store.adminSeasonDetail.pendingNominations.length,
+ to: '/admin/reviews',
+ hint: 'Freitext-Nominierungen bearbeiten',
+ },
+ {
+ label: 'Offene Risk Flags',
+ value: store.admin.riskFlags.length,
+ to: '/admin/risk',
+ hint: 'auffaellige Muster pruefen',
+ },
+ {
+ label: 'Kategorien pflegen',
+ value: store.adminSeasonDetail.categories.length,
+ to: '/admin/seasons',
+ hint: 'Texte, Limits und Sortierung',
+ },
+ {
+ label: 'Kandidatenbasis',
+ value: store.adminSeasonDetail.candidates.length,
+ to: '/admin/candidates',
+ hint: 'Kandidaten schnell aktualisieren',
+ },
+])
+
+
+
+
+ {{ item.label }}
+ {{ item.value }}
+ {{ item.hint }}
+
+
+
import { computed, onMounted } from 'vue'
import { RouterLink, RouterView, useRoute } from 'vue-router'
+import { Activity, AlertTriangle, CalendarCog, LayoutDashboard, Sparkles, Users } from '@lucide/vue'
import Card from '../../components/ui/Card.vue'
import { useAwardsStore } from '../../stores/awards'
@@ -11,14 +12,20 @@ const store = useAwardsStore()
const authStore = useAuthStore()
const navItems = [
- { label: 'Dashboard', to: '/admin/dashboard', description: 'KPIs, Trends, letzte Aktivitaet' },
- { label: 'Seasons', to: '/admin/seasons', description: 'Season-Status, Kategorien, Limits' },
- { label: 'Candidates', to: '/admin/candidates', description: 'Kandidatenbasis pro Season pflegen' },
- { label: 'Reviews', to: '/admin/reviews', description: 'Freitext-Nominierungen bearbeiten' },
- { label: 'Risk & Audit', to: '/admin/risk', description: 'Flags pruefen, Aktionen nachvollziehen' },
+ { label: 'Dashboard', to: '/admin/dashboard', description: 'Status, Trends, Quick Actions', icon: LayoutDashboard, badge: () => null },
+ { label: 'Seasons', to: '/admin/seasons', description: 'Season-Status, Kategorien, Limits', icon: CalendarCog, badge: () => `${store.adminSeasonDetail.categories.length}` },
+ { label: 'Candidates', to: '/admin/candidates', description: 'Kandidatenbasis pro Season pflegen', icon: Users, badge: () => `${store.adminSeasonDetail.candidates.length}` },
+ { label: 'Reviews', to: '/admin/reviews', description: 'Freitext-Nominierungen bearbeiten', icon: Sparkles, badge: () => `${store.adminSeasonDetail.pendingNominations.length}` },
+ { label: 'Risk & Audit', to: '/admin/risk', description: 'Flags pruefen, Aktionen nachvollziehen', icon: AlertTriangle, badge: () => `${store.admin.riskFlags.length}` },
]
const currentSeason = computed(() => store.adminSeasonDetail)
+const currentNavItem = computed(() => navItems.find((item) => route.path === item.to) ?? navItems[0])
+const seasonSummary = computed(() => [
+ { label: 'Kategorien', value: currentSeason.value.categories.length },
+ { label: 'Kandidaten', value: currentSeason.value.candidates.length },
+ { label: 'Reviews', value: currentSeason.value.pendingNominations.length },
+])
onMounted(async () => {
if (!authStore.isAdmin) return
@@ -36,10 +43,42 @@ onMounted(async () => {
Der Admin-Bereich ist in klar getrennte Arbeitszonen aufgeteilt, damit Season-Pflege, Review und Monitoring nicht mehr auf einer einzigen Seite kollidieren.
+
+
+
+
+
+
+
+
+
Aktueller Bereich
+
{{ currentNavItem.label }}
+
{{ currentNavItem.description }}
+
+
+
+
+
+
+
+
+
{{ item.label }}
+
{{ item.value }}
+
+
+
+
-
+
{
class="block rounded-[24px] border px-4 py-4 transition"
:class="route.path === item.to ? 'border-violet-200 bg-violet-100/80 text-violet-900' : 'border-transparent bg-white/70 text-slate-700 hover:border-violet-100 hover:bg-violet-50/70'"
>
- {{ item.label }}
- {{ item.description }}
+
+
+
+
+
+
+
{{ item.label }}
+
{{ item.description }}
+
+
+
+ {{ item.badge() }}
+
+
diff --git a/frontend/src/views/admin/AdminReviewsView.vue b/frontend/src/views/admin/AdminReviewsView.vue
index 2f71fb1..5c8006c 100644
--- a/frontend/src/views/admin/AdminReviewsView.vue
+++ b/frontend/src/views/admin/AdminReviewsView.vue
@@ -1,6 +1,7 @@