Refine admin workflows and team access
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user