Iterate admin pages with smarter workflows

This commit is contained in:
AzuTear
2026-06-18 01:00:58 +02:00
parent 0fa763667d
commit a2d6a5edc0
10 changed files with 422 additions and 36 deletions
@@ -1,6 +1,6 @@
<script setup lang="ts">
import { computed } from 'vue'
import { ArrowDownRight, ArrowUpRight, BarChart3, Clock3, ShieldAlert, Sparkles, Tags, Users } from '@lucide/vue'
import { ArrowDownRight, ArrowUpRight, BarChart3, Clock3, LayoutDashboard, ShieldAlert, Sparkles, Tags, Users } from '@lucide/vue'
import { RouterLink } from 'vue-router'
import AdminPageHeader from '../../components/admin/AdminPageHeader.vue'
@@ -109,7 +109,7 @@ const priorityActions = computed(() => [
{
label: 'Kategorien pflegen',
value: store.adminSeasonDetail.categories.length,
to: '/admin/years',
to: '/admin/categories',
hint: 'Texte, Limits und Reihenfolge aktuell halten',
icon: Tags,
tone: 'amber',
@@ -123,6 +123,38 @@ const priorityActions = computed(() => [
tone: 'emerald',
},
])
const operationChecks = computed(() => {
const categoriesWithoutCandidates = store.adminSeasonDetail.categories.filter((category) =>
!store.adminSeasonDetail.candidates.some((candidate) => candidate.categoryId === category.id),
)
const categoriesWithReviews = store.adminSeasonDetail.categories.filter((category) =>
store.adminSeasonDetail.pendingNominations.some((nomination) => nomination.categoryId === category.id),
)
return [
{
label: 'Kategorien ohne Kandidaten',
value: categoriesWithoutCandidates.length,
to: '/admin/categories',
state: categoriesWithoutCandidates.length === 0 ? 'ok' : 'warn',
note: categoriesWithoutCandidates.length === 0 ? 'Alle Kategorien sind besetzt.' : 'Vor Voting-Endspurt pruefen.',
},
{
label: 'Review-Backlog verteilt',
value: categoriesWithReviews.length,
to: '/admin/nominations',
state: categoriesWithReviews.length <= 1 ? 'ok' : 'warn',
note: categoriesWithReviews.length <= 1 ? 'Backlog ist fokussiert.' : 'Mehrere Kategorien brauchen Sichtung.',
},
{
label: 'Risk Flags offen',
value: store.admin.riskFlags.length,
to: '/admin/risk',
state: store.admin.riskFlags.length === 0 ? 'ok' : 'danger',
note: store.admin.riskFlags.length === 0 ? 'Keine offenen Hinweise.' : 'Missbrauchsschutz zuerst pruefen.',
},
]
})
</script>
<template>
@@ -131,6 +163,7 @@ const priorityActions = computed(() => [
eyebrow="Dashboard"
title="Was braucht gerade Aufmerksamkeit?"
description="Trends, offene Aufgaben und Kategorie-Performance sind hier gebuendelt, damit du schneller entscheiden kannst, was als Naechstes drankommt."
:icon="LayoutDashboard"
/>
<section class="grid gap-4 xl:grid-cols-[1.15fr_0.85fr]">
@@ -182,7 +215,7 @@ const priorityActions = computed(() => [
<span
v-for="(value, index) in metric.sparkline"
:key="`${metric.label}-${index}`"
class="flex-1 rounded-t-full bg-gradient-to-t from-[#d8c7ff] via-[#f8c8dc] to-[#b8f1e6]"
class="flex-1 rounded-t-full bg-gradient-to-t from-[#7c5cff] to-[#c4b5fd]"
:style="{ height: `${value}%` }"
/>
</div>
@@ -231,6 +264,30 @@ const priorityActions = computed(() => [
</Card>
</section>
<section class="grid gap-4 lg:grid-cols-3">
<RouterLink
v-for="check in operationChecks"
:key="check.label"
:to="check.to"
class="rounded-[24px] border bg-white/85 p-5 shadow-[0_16px_42px_rgba(168,145,214,0.08)] transition hover:-translate-y-0.5 hover:bg-violet-50/50"
:class="{
'border-emerald-100': check.state === 'ok',
'border-amber-100': check.state === 'warn',
'border-rose-100': check.state === 'danger',
}"
>
<div class="flex items-start justify-between gap-4">
<div>
<p class="text-xs font-semibold uppercase tracking-[0.2em] text-slate-500">{{ check.label }}</p>
<strong class="mt-3 block text-3xl" :class="check.state === 'danger' ? 'text-rose-700' : check.state === 'warn' ? 'text-amber-700' : 'text-emerald-700'">
{{ check.value }}
</strong>
<p class="mt-2 text-sm leading-5 text-slate-500">{{ check.note }}</p>
</div>
</div>
</RouterLink>
</section>
<section class="grid gap-6 xl:grid-cols-[0.92fr_1.08fr]">
<Card class="p-7">
<div class="flex items-center justify-between gap-4">
@@ -285,7 +342,7 @@ const priorityActions = computed(() => [
</div>
<div class="mt-4 h-3 rounded-full bg-[#f7f2ff]">
<div
class="h-3 rounded-full bg-gradient-to-r from-[#d8c7ff] via-[#f8c8dc] to-[#b8f1e6]"
class="h-3 rounded-full bg-gradient-to-r from-[#c4b5fd] to-[#7c5cff]"
:style="{ width: `${(category.votes / maxCategoryVotes) * 100}%` }"
/>
</div>