Refine admin workflows and team access

This commit is contained in:
AzuTear
2026-06-26 18:09:29 +02:00
parent b7804e10ea
commit 8b69dfbafb
71 changed files with 5066 additions and 751 deletions
+117 -24
View File
@@ -9,14 +9,17 @@ import {
Film,
LayoutDashboard,
FileClock,
Menu,
Settings,
Tags,
Trophy,
UserCog,
Users,
FileText,
X,
} from '@lucide/vue'
import AdminToastViewport from '../../components/admin/AdminToastViewport.vue'
import Card from '../../components/ui/Card.vue'
import { getRiskMetricValue } from '../../lib/adminMetrics'
import { useAwardsStore } from '../../stores/awards'
@@ -29,6 +32,7 @@ const openRiskCount = computed(() => getRiskMetricValue(store.admin.metrics))
const pendingClipCount = computed(() => store.adminSeasonDetail.clipSubmissions.filter((clip) => clip.status === 'pending').length)
const adminWorkspaceLoading = ref(false)
const adminWorkspaceLoaded = ref(false)
const mobileNavOpen = ref(false)
const fullNavGroups = computed(() => [
{
@@ -76,16 +80,15 @@ const navGroups = computed(() => {
})
const currentSeason = computed(() => store.adminSeasonDetail)
const seasonSummary = computed(() => [
{ label: 'Kategorien', value: currentSeason.value.categories.length },
{ label: 'Kandidaten', value: currentSeason.value.candidates.length },
{ label: 'Reviews', value: currentSeason.value.pendingNominations.length },
])
function isActive(to: string) {
return route.path === to
}
// Close mobile nav on route change
watch(() => route.path, () => {
mobileNavOpen.value = false
})
async function ensureAdminWorkspace() {
if (!authStore.hydrated || !authStore.canAccessAdmin || adminWorkspaceLoading.value) return
if (adminWorkspaceLoaded.value && store.apiMode === 'api') return
@@ -113,13 +116,119 @@ watch(
</script>
<template>
<div class="pb-10">
<AdminToastViewport />
<!-- Mobile top bar -->
<div class="sticky top-0 z-40 flex items-center gap-3 border-b border-violet-100 bg-white/90 px-4 py-3 backdrop-blur-sm xl:hidden">
<button
type="button"
class="grid h-9 w-9 shrink-0 place-items-center rounded-xl border border-violet-200 text-violet-600 transition hover:bg-violet-50"
aria-label="Navigation öffnen"
@click="mobileNavOpen = true"
>
<Menu class="h-5 w-5" />
</button>
<div class="min-w-0 flex-1">
<p class="truncate text-sm font-bold text-violet-900">
{{ currentSeason.year ? `Admin · ${currentSeason.year}` : 'Admin' }}
</p>
<p class="truncate text-xs text-slate-500">{{ currentSeason.currentPhase || currentSeason.name || 'Kein Jahr gewählt' }}</p>
</div>
<div v-if="store.adminSeasonDetail.pendingNominations.length > 0" class="shrink-0">
<span class="grid h-6 min-w-6 place-items-center rounded-full border border-violet-200 bg-violet-50 px-2 text-[11px] font-bold text-violet-700">
{{ store.adminSeasonDetail.pendingNominations.length }}
</span>
</div>
</div>
<!-- Mobile drawer (Teleport to body) -->
<Teleport to="body">
<Transition
enter-active-class="transition duration-200"
enter-from-class="opacity-0"
enter-to-class="opacity-100"
leave-active-class="transition duration-150"
leave-from-class="opacity-100"
leave-to-class="opacity-0"
>
<div v-if="mobileNavOpen" class="fixed inset-0 z-50 xl:hidden" @click.self="mobileNavOpen = false">
<!-- Backdrop -->
<div class="absolute inset-0 bg-slate-950/40 backdrop-blur-sm" @click="mobileNavOpen = false" />
<!-- Drawer panel -->
<Transition
enter-active-class="transition duration-200"
enter-from-class="-translate-x-full"
enter-to-class="translate-x-0"
leave-active-class="transition duration-150"
leave-from-class="translate-x-0"
leave-to-class="-translate-x-full"
>
<div v-if="mobileNavOpen" class="absolute inset-y-0 left-0 flex w-72 flex-col gap-3 overflow-y-auto bg-white/98 p-3 shadow-2xl backdrop-blur-md">
<!-- Drawer header -->
<div class="flex items-center justify-between gap-3 px-1 py-1">
<div class="min-w-0">
<p class="text-[10px] font-semibold uppercase tracking-[0.18em] text-violet-500">Admin</p>
<p class="truncate text-sm font-bold text-violet-900">{{ currentSeason.year || 'Kein Jahr' }} · {{ currentSeason.currentPhase || '' }}</p>
</div>
<button
type="button"
class="grid h-8 w-8 shrink-0 place-items-center rounded-xl border border-violet-200 text-violet-600 transition hover:bg-violet-50"
aria-label="Navigation schließen"
@click="mobileNavOpen = false"
>
<X class="h-4 w-4" />
</button>
</div>
<!-- Nav groups -->
<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>
</div>
</RouterLink>
</section>
</nav>
</div>
</Transition>
</div>
</Transition>
</Teleport>
<div class="admin-panel pb-10 pt-4 xl:pt-0">
<div class="grid gap-6 xl:grid-cols-[292px_minmax(0,1fr)]">
<main class="order-1 min-w-0 xl:order-2">
<RouterView />
</main>
<aside class="order-2 space-y-3 xl:order-1 xl:sticky xl:top-4 xl:h-fit">
<!-- Desktop sidebar (hidden on mobile) -->
<aside class="order-2 hidden space-y-3 xl:order-1 xl:block xl:sticky xl:top-4 xl:h-fit">
<Card class="p-3">
<nav class="space-y-4">
<section v-for="group in navGroups" :key="group.label" class="space-y-1.5">
@@ -155,22 +264,6 @@ watch(
</section>
</nav>
</Card>
<Card class="p-3">
<p class="text-[10px] font-semibold uppercase tracking-[0.18em] text-violet-500">Aktives Jahr</p>
<p class="mt-1 truncate text-sm font-semibold text-violet-800">{{ currentSeason.year || 'Kein Jahr' }} · {{ currentSeason.currentPhase || 'Kein Status' }}</p>
<p class="mt-1 truncate text-xs text-slate-500">{{ currentSeason.name || 'Bitte Jahr auswählen.' }}</p>
<div class="mt-3 grid grid-cols-3 gap-1.5">
<div
v-for="item in seasonSummary"
:key="item.label"
class="rounded-md border border-white/80 bg-white/75 px-2 py-1.5"
>
<p class="truncate text-[9px] font-semibold uppercase tracking-[0.12em] text-slate-500">{{ item.label }}</p>
<strong class="block text-base leading-5 text-violet-800">{{ item.value }}</strong>
</div>
</div>
</Card>
</aside>
</div>
</div>