Iterate admin pages with smarter workflows
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user