Expand admin navigation and pages

This commit is contained in:
AzuTear
2026-06-18 00:21:40 +02:00
parent 178f014a4a
commit 0fa763667d
9 changed files with 1097 additions and 35 deletions
+82 -35
View File
@@ -1,7 +1,20 @@
<script setup lang="ts">
import { computed, onMounted } from 'vue'
import { RouterLink, RouterView, useRoute } from 'vue-router'
import { AlertTriangle, CalendarCog, LayoutDashboard, Sparkles, Users } from '@lucide/vue'
import {
AlertTriangle,
BarChart3,
CalendarCog,
ClipboardList,
Film,
LayoutDashboard,
Settings,
Sparkles,
Tags,
UserCog,
Users,
Vote,
} from '@lucide/vue'
import Card from '../../components/ui/Card.vue'
import { useAwardsStore } from '../../stores/awards'
@@ -11,12 +24,39 @@ const route = useRoute()
const store = useAwardsStore()
const authStore = useAuthStore()
const navItems = [
{ label: 'Dashboard', to: '/admin/dashboard', description: 'Status, Trends, Schnellzugriffe', icon: LayoutDashboard, badge: () => null },
{ label: 'Jahre', to: '/admin/years', description: 'Jahresstatus, Kategorien, Limits', icon: CalendarCog, badge: () => `${store.adminSeasonDetail.categories.length}` },
{ label: 'Kandidaten', to: '/admin/candidates', description: 'Kandidatenbasis pro Jahr 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: 'Risiko & Audit', to: '/admin/risk', description: 'Hinweise pruefen, Aktionen nachvollziehen', icon: AlertTriangle, badge: () => `${store.admin.riskFlags.length}` },
const navGroups = [
{
label: 'Betrieb',
items: [
{ label: 'Dashboard', to: '/admin/dashboard', description: 'Live-Lage und Aufgaben', icon: LayoutDashboard, badge: () => null },
{ label: 'Nominierungen', to: '/admin/nominations', description: 'Eingang und Backlog', icon: ClipboardList, badge: () => `${store.adminSeasonDetail.pendingNominations.length}` },
{ label: 'Voting', to: '/admin/voting', description: 'Stimmen und Readiness', icon: Vote, badge: () => `${store.admin.metrics.find((metric) => metric.label === 'Stimmen')?.value ?? 0}` },
],
},
{
label: 'Inhalte',
items: [
{ label: 'Jahre', to: '/admin/years', description: 'Jahresstatus und Setup', icon: CalendarCog, badge: () => `${store.adminSeasons.length}` },
{ label: 'Kategorien', to: '/admin/categories', description: 'Struktur und Limits', icon: Tags, badge: () => `${store.adminSeasonDetail.categories.length}` },
{ label: 'Kandidaten', to: '/admin/candidates', description: 'Kandidatenbasis pflegen', icon: Users, badge: () => `${store.adminSeasonDetail.candidates.length}` },
{ label: 'Clips', to: '/admin/clips', description: 'Clip-Kategorien pruefen', icon: Film, badge: () => `${store.adminSeasonDetail.categories.filter((category) => `${category.groupName} ${category.name}`.toLowerCase().includes('clip')).length}` },
],
},
{
label: 'Kontrolle',
items: [
{ label: 'Reviews', to: '/admin/reviews', description: 'Freitext-Faelle entscheiden', icon: Sparkles, badge: () => `${store.adminSeasonDetail.pendingNominations.length}` },
{ label: 'Risiko & Audit', to: '/admin/risk', description: 'Flags pruefen', icon: AlertTriangle, badge: () => `${store.admin.riskFlags.length}` },
{ label: 'User & Logs', to: '/admin/users-logs', description: 'User-Spuren und Aktionen', icon: UserCog, badge: () => `${store.admin.auditEntries.length}` },
],
},
{
label: 'Auswertung',
items: [
{ label: 'Analytics', to: '/admin/analytics', description: 'Metriken und Rankings', icon: BarChart3, badge: () => `${store.admin.topCategories.length}` },
{ label: 'Einstellungen', to: '/admin/settings', description: 'Public-Status und Checks', icon: Settings, badge: () => null },
],
},
]
const currentSeason = computed(() => store.adminSeasonDetail)
@@ -26,6 +66,10 @@ const seasonSummary = computed(() => [
{ label: 'Reviews', value: currentSeason.value.pendingNominations.length },
])
function isActive(to: string) {
return route.path === to
}
onMounted(async () => {
if (!authStore.isAdmin) return
await store.initializeAdminWorkspace()
@@ -34,38 +78,41 @@ onMounted(async () => {
<template>
<div class="pb-10">
<div class="grid gap-6 xl:grid-cols-[260px_minmax(0,1fr)]">
<div class="grid gap-6 xl:grid-cols-[292px_minmax(0,1fr)]">
<aside class="space-y-3 xl:sticky xl:top-4 xl:h-fit">
<Card class="p-3">
<nav class="space-y-2">
<RouterLink
v-for="item in navItems"
:key="item.to"
:to="item.to"
class="group block rounded-2xl border px-3.5 py-3 transition"
:class="route.path === item.to ? 'border-violet-200 bg-gradient-to-br from-violet-50 via-white to-[#f7eef8] text-violet-950 shadow-[0_14px_34px_rgba(168,145,214,0.14)]' : 'border-transparent bg-white/55 text-slate-700 hover:border-violet-100 hover:bg-violet-50/70'"
>
<div class="flex items-center gap-3">
<div
class="grid h-10 w-10 shrink-0 place-items-center rounded-2xl transition"
:class="route.path === item.to ? 'bg-white text-violet-700 shadow-sm' : 'bg-violet-50 text-violet-600 group-hover:bg-white'"
>
<component :is="item.icon" class="h-4.5 w-4.5" />
</div>
<div class="min-w-0 flex-1">
<div class="flex min-w-0 items-center justify-between gap-3">
<p class="truncate text-sm font-semibold leading-5">{{ item.label }}</p>
<span
v-if="item.badge()"
class="grid h-7 min-w-7 shrink-0 place-items-center rounded-full border border-violet-200 bg-white px-2 text-xs font-semibold text-violet-700 shadow-sm"
>
{{ item.badge() }}
</span>
<nav class="space-y-4">
<section v-for="group in navGroups" :key="group.label" class="space-y-1.5">
<p class="px-2 text-[10px] font-semibold uppercase tracking-[0.2em] text-slate-400">{{ group.label }}</p>
<RouterLink
v-for="item in group.items"
:key="item.to"
:to="item.to"
class="group block rounded-2xl border px-3 py-2.5 transition"
:class="isActive(item.to) ? 'border-violet-200 bg-gradient-to-br from-violet-50 via-white to-[#f7eef8] text-violet-950 shadow-[0_14px_34px_rgba(168,145,214,0.14)]' : 'border-transparent bg-white/55 text-slate-700 hover:border-violet-100 hover:bg-violet-50/70'"
>
<div class="flex items-center gap-3">
<div
class="grid h-9 w-9 shrink-0 place-items-center rounded-xl transition"
:class="isActive(item.to) ? 'bg-white text-violet-700 shadow-sm' : 'bg-violet-50 text-violet-600 group-hover:bg-white'"
>
<component :is="item.icon" class="h-4.5 w-4.5" />
</div>
<div class="min-w-0 flex-1">
<div class="flex min-w-0 items-center justify-between gap-3">
<p class="truncate text-sm font-semibold leading-5">{{ item.label }}</p>
<span
v-if="item.badge()"
class="grid h-6 min-w-6 shrink-0 place-items-center rounded-full border border-violet-200 bg-white px-2 text-[11px] font-semibold text-violet-700 shadow-sm"
>
{{ item.badge() }}
</span>
</div>
<p class="mt-0.5 truncate text-xs leading-5 text-slate-500">{{ item.description }}</p>
</div>
<p class="mt-0.5 truncate text-xs leading-5 text-slate-500">{{ item.description }}</p>
</div>
</div>
</RouterLink>
</RouterLink>
</section>
</nav>
</Card>