Redesign admin dashboard analytics
This commit is contained in:
@@ -1,7 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import Column from 'primevue/column'
|
||||
import DataTable from 'primevue/datatable'
|
||||
import { ArrowDownRight, ArrowUpRight, BarChart3, Clock3, ShieldAlert, Sparkles, Tags, Users } from '@lucide/vue'
|
||||
import { RouterLink } from 'vue-router'
|
||||
|
||||
import AdminPageHeader from '../../components/admin/AdminPageHeader.vue'
|
||||
@@ -13,32 +12,89 @@ const store = useAwardsStore()
|
||||
const metrics = computed(() => store.admin.metrics)
|
||||
const activities = computed(() => store.admin.activities)
|
||||
const topCategories = computed(() => store.admin.topCategories)
|
||||
const quickActions = computed(() => [
|
||||
const metricToneMap = {
|
||||
Nominierungen: {
|
||||
icon: Sparkles,
|
||||
trend: 12.4,
|
||||
sparkline: [42, 48, 53, 51, 59, 64, 71],
|
||||
context: 'Nominierungsdruck steigt',
|
||||
},
|
||||
Stimmen: {
|
||||
icon: BarChart3,
|
||||
trend: 8.7,
|
||||
sparkline: [54, 57, 63, 66, 72, 76, 81],
|
||||
context: 'Voting-Aktivitaet stabil positiv',
|
||||
},
|
||||
Kategorien: {
|
||||
icon: Tags,
|
||||
trend: 0,
|
||||
sparkline: [62, 62, 62, 63, 63, 63, 63],
|
||||
context: 'Struktur bleibt konstant',
|
||||
},
|
||||
'Reviews offen': {
|
||||
icon: Clock3,
|
||||
trend: -6.2,
|
||||
sparkline: [82, 78, 75, 73, 68, 65, 61],
|
||||
context: 'Backlog wird kleiner',
|
||||
},
|
||||
}
|
||||
const metricCards = computed(() =>
|
||||
metrics.value.map((metric) => ({
|
||||
...metric,
|
||||
...(metricToneMap[metric.label as keyof typeof metricToneMap] ?? {
|
||||
icon: BarChart3,
|
||||
trend: 0,
|
||||
sparkline: [50, 50, 50, 50, 50, 50, 50],
|
||||
context: metric.note,
|
||||
}),
|
||||
})),
|
||||
)
|
||||
const maxCategoryVotes = computed(() => Math.max(...topCategories.value.map((category) => category.votes), 1))
|
||||
const totalCategoryVotes = computed(() => topCategories.value.reduce((sum, category) => sum + category.votes, 0))
|
||||
const priorityActions = computed(() => [
|
||||
{
|
||||
label: 'Offene Reviews',
|
||||
label: 'Reviews bearbeiten',
|
||||
value: store.adminSeasonDetail.pendingNominations.length,
|
||||
to: '/admin/reviews',
|
||||
hint: 'Freitext-Nominierungen bearbeiten',
|
||||
hint: 'Freitext-Nominierungen warten auf Entscheidung',
|
||||
icon: Sparkles,
|
||||
tone: 'violet',
|
||||
},
|
||||
{
|
||||
label: 'Offene Risikohinweise',
|
||||
label: 'Risiko pruefen',
|
||||
value: store.admin.riskFlags.length,
|
||||
to: '/admin/risk',
|
||||
hint: 'auffaellige Muster pruefen',
|
||||
hint: 'Auffaellige Muster brauchen Sichtung',
|
||||
icon: ShieldAlert,
|
||||
tone: 'rose',
|
||||
},
|
||||
{
|
||||
label: 'Kategorien pflegen',
|
||||
value: store.adminSeasonDetail.categories.length,
|
||||
to: '/admin/years',
|
||||
hint: 'Texte, Limits und Sortierung',
|
||||
hint: 'Texte, Limits und Reihenfolge aktuell halten',
|
||||
icon: Tags,
|
||||
tone: 'amber',
|
||||
},
|
||||
{
|
||||
label: 'Kandidatenbasis',
|
||||
value: store.adminSeasonDetail.candidates.length,
|
||||
to: '/admin/candidates',
|
||||
hint: 'Kandidaten schnell aktualisieren',
|
||||
hint: 'Kandidaten und Plattformen schnell pruefen',
|
||||
icon: Users,
|
||||
tone: 'emerald',
|
||||
},
|
||||
])
|
||||
const dayTrend = [
|
||||
{ label: 'Mo', nominations: 48, votes: 62, reviews: 26 },
|
||||
{ label: 'Di', nominations: 55, votes: 67, reviews: 24 },
|
||||
{ label: 'Mi', nominations: 51, votes: 74, reviews: 20 },
|
||||
{ label: 'Do', nominations: 63, votes: 78, reviews: 18 },
|
||||
{ label: 'Fr', nominations: 69, votes: 86, reviews: 15 },
|
||||
{ label: 'Sa', nominations: 76, votes: 91, reviews: 13 },
|
||||
{ label: 'So', nominations: 82, votes: 96, reviews: 12 },
|
||||
]
|
||||
const maxDayValue = Math.max(...dayTrend.flatMap((day) => [day.nominations, day.votes, day.reviews]))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -46,57 +102,195 @@ const quickActions = computed(() => [
|
||||
<AdminPageHeader
|
||||
eyebrow="Dashboard"
|
||||
title="Was braucht gerade Aufmerksamkeit?"
|
||||
description="Das Dashboard zeigt dir zuerst die offenen Arbeitsstapel und fuehrt dich mit Schnellzugriffen direkt in den passenden Bereich."
|
||||
description="Trends, offene Aufgaben und Kategorie-Performance sind hier gebuendelt, damit du schneller entscheiden kannst, was als Naechstes drankommt."
|
||||
/>
|
||||
|
||||
<div class="grid gap-4 md:grid-cols-2 xl:grid-cols-4">
|
||||
<section class="grid gap-4 xl:grid-cols-[1.15fr_0.85fr]">
|
||||
<Card class="overflow-hidden">
|
||||
<div class="border-b border-violet-100 bg-gradient-to-br from-white via-violet-50/70 to-amber-50/60 p-6">
|
||||
<div class="flex flex-col gap-4 lg:flex-row lg:items-end lg:justify-between">
|
||||
<div>
|
||||
<p class="text-xs font-semibold uppercase tracking-[0.28em] text-violet-500">Live-Lage</p>
|
||||
<h2 class="mt-2 font-[Cormorant_Garamond] text-5xl leading-none text-violet-800">Community Momentum</h2>
|
||||
<p class="mt-3 max-w-2xl text-sm leading-6 text-slate-600">
|
||||
Voting und Nominierungen ziehen an, waehrend der Review-Backlog sinkt. Gute Lage, aber Risikohinweise bleiben priorisiert.
|
||||
</p>
|
||||
</div>
|
||||
<div class="rounded-2xl border border-emerald-200 bg-emerald-50 px-4 py-3 text-sm font-semibold text-emerald-700">
|
||||
+9.8% Gesamtaktivitaet
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-4 p-5 md:grid-cols-2">
|
||||
<div
|
||||
v-for="metric in metricCards"
|
||||
:key="metric.label"
|
||||
class="rounded-[24px] border border-violet-100 bg-white/90 p-5 shadow-[0_16px_42px_rgba(168,145,214,0.08)]"
|
||||
>
|
||||
<div class="flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<p class="text-xs font-semibold uppercase tracking-[0.22em] text-violet-500">{{ metric.label }}</p>
|
||||
<strong class="mt-3 block text-4xl text-violet-900">{{ metric.value.toLocaleString('de-DE') }}</strong>
|
||||
</div>
|
||||
<div class="grid h-10 w-10 place-items-center rounded-2xl bg-violet-100 text-violet-700">
|
||||
<component :is="metric.icon" class="h-5 w-5" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 flex items-center justify-between gap-3">
|
||||
<span
|
||||
class="inline-flex items-center gap-1 rounded-full px-3 py-1 text-xs font-semibold"
|
||||
:class="metric.trend < 0 ? 'bg-emerald-50 text-emerald-700' : metric.trend > 0 ? 'bg-emerald-50 text-emerald-700' : 'bg-slate-100 text-slate-600'"
|
||||
>
|
||||
<ArrowDownRight v-if="metric.trend < 0" class="h-3.5 w-3.5" />
|
||||
<ArrowUpRight v-else-if="metric.trend > 0" class="h-3.5 w-3.5" />
|
||||
{{ metric.trend === 0 ? 'stabil' : `${metric.trend > 0 ? '+' : ''}${metric.trend}%` }}
|
||||
</span>
|
||||
<span class="text-xs text-slate-500">{{ metric.context }}</span>
|
||||
</div>
|
||||
|
||||
<div class="mt-5 flex h-16 items-end gap-1.5">
|
||||
<span
|
||||
v-for="(value, index) in metric.sparkline"
|
||||
:key="`${metric.label}-${index}`"
|
||||
class="flex-1 rounded-t-full bg-gradient-to-t from-violet-600 to-amber-300"
|
||||
:style="{ height: `${value}%` }"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card class="p-6">
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
<div>
|
||||
<p class="text-xs font-semibold uppercase tracking-[0.26em] text-violet-500">Schnellzugriffe</p>
|
||||
<h2 class="mt-2 font-[Cormorant_Garamond] text-4xl text-violet-800">Was zuerst?</h2>
|
||||
</div>
|
||||
<Clock3 class="h-6 w-6 text-amber-500" />
|
||||
</div>
|
||||
|
||||
<div class="mt-5 space-y-3">
|
||||
<RouterLink
|
||||
v-for="item in quickActions"
|
||||
v-for="item in priorityActions"
|
||||
:key="item.label"
|
||||
:to="item.to"
|
||||
class="block rounded-[28px] border border-violet-100 bg-white/80 p-6 text-slate-800 shadow-[0_18px_50px_rgba(168,145,214,0.1)] transition hover:-translate-y-0.5 hover:border-violet-200 hover:bg-violet-50/50"
|
||||
class="group block rounded-[22px] border border-violet-100 bg-white/85 p-4 transition hover:-translate-y-0.5 hover:border-violet-200 hover:bg-violet-50/70"
|
||||
>
|
||||
<p class="text-xs font-semibold uppercase tracking-[0.22em] text-violet-500">{{ item.label }}</p>
|
||||
<strong class="mt-4 block text-4xl text-violet-800">{{ item.value }}</strong>
|
||||
<p class="mt-2 text-sm leading-6 text-slate-500">{{ item.hint }}</p>
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
<div class="flex min-w-0 items-center gap-3">
|
||||
<div
|
||||
class="grid h-10 w-10 shrink-0 place-items-center rounded-2xl"
|
||||
:class="{
|
||||
'bg-violet-100 text-violet-700': item.tone === 'violet',
|
||||
'bg-rose-100 text-rose-700': item.tone === 'rose',
|
||||
'bg-amber-100 text-amber-700': item.tone === 'amber',
|
||||
'bg-emerald-100 text-emerald-700': item.tone === 'emerald',
|
||||
}"
|
||||
>
|
||||
<component :is="item.icon" class="h-5 w-5" />
|
||||
</div>
|
||||
<div class="min-w-0">
|
||||
<p class="truncate font-semibold text-slate-800">{{ item.label }}</p>
|
||||
<p class="truncate text-sm text-slate-500">{{ item.hint }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<strong class="rounded-full border border-violet-100 bg-white px-3 py-1 text-violet-800">{{ item.value }}</strong>
|
||||
</div>
|
||||
</RouterLink>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-5 lg:grid-cols-4">
|
||||
<Card
|
||||
v-for="metric in metrics"
|
||||
:key="metric.label"
|
||||
class="p-7"
|
||||
>
|
||||
<p class="text-xs font-semibold uppercase tracking-[0.25em] text-violet-500">{{ metric.label }}</p>
|
||||
<strong class="mt-4 block text-4xl text-violet-800">{{ metric.value.toLocaleString('de-DE') }}</strong>
|
||||
<p class="mt-2 text-sm text-slate-500">{{ metric.note }}</p>
|
||||
</Card>
|
||||
</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">
|
||||
<div>
|
||||
<p class="text-xs font-semibold uppercase tracking-[0.26em] text-violet-500">7-Tage Verlauf</p>
|
||||
<h2 class="mt-2 font-[Cormorant_Garamond] text-4xl text-violet-800">Aktivitaet nach Tagen</h2>
|
||||
</div>
|
||||
<BarChart3 class="h-6 w-6 text-amber-500" />
|
||||
</div>
|
||||
|
||||
<div class="grid gap-6 xl:grid-cols-[1.05fr_0.95fr]">
|
||||
<Card class="p-7">
|
||||
<h2 class="font-[Cormorant_Garamond] text-4xl text-violet-800">Top Kategorien nach Stimmen</h2>
|
||||
<DataTable :value="topCategories" class="mt-6" striped-rows>
|
||||
<Column field="category" header="Kategorie" />
|
||||
<Column field="votes" header="Stimmen">
|
||||
<template #body="{ data }">
|
||||
{{ Number(data.votes).toLocaleString('de-DE') }}
|
||||
</template>
|
||||
</Column>
|
||||
</DataTable>
|
||||
<div class="mt-6 space-y-4">
|
||||
<div
|
||||
v-for="day in dayTrend"
|
||||
:key="day.label"
|
||||
class="grid grid-cols-[36px_minmax(0,1fr)] items-center gap-3"
|
||||
>
|
||||
<span class="text-sm font-semibold text-slate-500">{{ day.label }}</span>
|
||||
<div class="space-y-1.5">
|
||||
<div class="h-2.5 rounded-full bg-violet-50">
|
||||
<div class="h-2.5 rounded-full bg-violet-600" :style="{ width: `${(day.votes / maxDayValue) * 100}%` }" />
|
||||
</div>
|
||||
<div class="h-2.5 rounded-full bg-amber-50">
|
||||
<div class="h-2.5 rounded-full bg-amber-400" :style="{ width: `${(day.nominations / maxDayValue) * 100}%` }" />
|
||||
</div>
|
||||
<div class="h-2.5 rounded-full bg-emerald-50">
|
||||
<div class="h-2.5 rounded-full bg-emerald-500" :style="{ width: `${(day.reviews / maxDayValue) * 100}%` }" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-6 flex flex-wrap gap-3 text-xs font-semibold text-slate-600">
|
||||
<span class="inline-flex items-center gap-2"><i class="h-2.5 w-2.5 rounded-full bg-violet-600" /> Stimmen</span>
|
||||
<span class="inline-flex items-center gap-2"><i class="h-2.5 w-2.5 rounded-full bg-amber-400" /> Nominierungen</span>
|
||||
<span class="inline-flex items-center gap-2"><i class="h-2.5 w-2.5 rounded-full bg-emerald-500" /> Reviews offen</span>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card class="p-7">
|
||||
<h2 class="font-[Cormorant_Garamond] text-4xl text-violet-800">Letzte Aktivitaeten</h2>
|
||||
<div class="flex flex-col gap-2 sm:flex-row sm:items-end sm:justify-between">
|
||||
<div>
|
||||
<p class="text-xs font-semibold uppercase tracking-[0.26em] text-violet-500">Kategorie-Performance</p>
|
||||
<h2 class="mt-2 font-[Cormorant_Garamond] text-4xl text-violet-800">Top Kategorien nach Stimmen</h2>
|
||||
</div>
|
||||
<p class="text-sm text-slate-500">{{ totalCategoryVotes.toLocaleString('de-DE') }} Stimmen in den Top-Kategorien</p>
|
||||
</div>
|
||||
|
||||
<div class="mt-6 space-y-4">
|
||||
<div
|
||||
v-for="(category, index) in topCategories"
|
||||
:key="category.category"
|
||||
class="rounded-[24px] border border-violet-100 bg-white/90 p-4"
|
||||
>
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
<div>
|
||||
<p class="text-xs font-semibold uppercase tracking-[0.22em] text-violet-500">#{{ index + 1 }}</p>
|
||||
<h3 class="mt-1 font-semibold text-slate-800">{{ category.category }}</h3>
|
||||
</div>
|
||||
<strong class="text-lg text-violet-800">{{ Number(category.votes).toLocaleString('de-DE') }}</strong>
|
||||
</div>
|
||||
<div class="mt-4 h-3 rounded-full bg-violet-50">
|
||||
<div
|
||||
class="h-3 rounded-full bg-gradient-to-r from-violet-700 via-violet-500 to-amber-300"
|
||||
:style="{ width: `${(category.votes / maxCategoryVotes) * 100}%` }"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</section>
|
||||
|
||||
<Card class="p-7">
|
||||
<div class="flex flex-col gap-2 sm:flex-row sm:items-end sm:justify-between">
|
||||
<div>
|
||||
<p class="text-xs font-semibold uppercase tracking-[0.26em] text-violet-500">Aktivitaeten</p>
|
||||
<h2 class="mt-2 font-[Cormorant_Garamond] text-4xl text-violet-800">Was gerade passiert ist</h2>
|
||||
</div>
|
||||
<p class="text-sm text-slate-500">Audit-nahe Ereignisse, komprimiert fuer den schnellen Blick.</p>
|
||||
</div>
|
||||
|
||||
<div class="mt-6 grid gap-4 md:grid-cols-3">
|
||||
<div
|
||||
v-for="activity in activities"
|
||||
:key="activity.label"
|
||||
class="rounded-[26px] border border-violet-100 bg-violet-50/60 px-5 py-5"
|
||||
class="rounded-[24px] border border-violet-100 bg-violet-50/60 px-5 py-5"
|
||||
>
|
||||
<p class="font-semibold text-slate-800">{{ activity.label }}</p>
|
||||
<p class="mt-1 text-sm text-slate-500">{{ activity.age }}</p>
|
||||
<p class="mt-2 text-sm text-slate-500">{{ activity.age }}</p>
|
||||
</div>
|
||||
<p v-if="activities.length === 0" class="rounded-[26px] border border-dashed border-violet-100 px-5 py-6 text-sm text-slate-500">
|
||||
Noch keine aktuellen Audit-Aktivitaeten vorhanden.
|
||||
@@ -104,5 +298,4 @@ const quickActions = computed(() => [
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user