Refine admin panel workflows
This commit is contained in:
@@ -1,34 +1,39 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { BarChart3, CheckCircle2, Tags, Users, Vote } from '@lucide/vue'
|
||||
import { CheckCircle2, LockKeyhole, ShieldAlert, Tags, Vote } from '@lucide/vue'
|
||||
import { RouterLink } from 'vue-router'
|
||||
|
||||
import AdminPageHeader from '../../components/admin/AdminPageHeader.vue'
|
||||
import AdminSeasonToolbar from '../../components/admin/AdminSeasonToolbar.vue'
|
||||
import Card from '../../components/ui/Card.vue'
|
||||
import { getVoteMetricValue } from '../../lib/adminMetrics'
|
||||
import { useAwardsStore } from '../../stores/awards'
|
||||
|
||||
const store = useAwardsStore()
|
||||
const seasonDetail = computed(() => store.adminSeasonDetail)
|
||||
const totalVotes = computed(() => store.admin.metrics.find((metric) => metric.label === 'Stimmen')?.value ?? 0)
|
||||
const maxVotes = computed(() => Math.max(...store.admin.topCategories.map((category) => category.votes), 1))
|
||||
const totalVotes = computed(() => getVoteMetricValue(store.admin.metrics))
|
||||
const votingReadiness = computed(() =>
|
||||
seasonDetail.value.categories.map((category) => {
|
||||
const candidateCount = seasonDetail.value.candidates.filter((candidate) => candidate.categoryId === category.id).length
|
||||
const reviewCount = seasonDetail.value.pendingNominations.filter((nomination) => nomination.categoryId === category.id).length
|
||||
return {
|
||||
...category,
|
||||
candidateCount,
|
||||
ready: candidateCount > 0 && seasonDetail.value.currentPhase.toLowerCase().includes('voting'),
|
||||
reviewCount,
|
||||
ready: candidateCount > 0 && reviewCount === 0 && seasonDetail.value.currentPhase.toLowerCase().includes('voting'),
|
||||
}
|
||||
}),
|
||||
)
|
||||
const readyCount = computed(() => votingReadiness.value.filter((category) => category.ready).length)
|
||||
const notReadyCategories = computed(() => votingReadiness.value.filter((category) => !category.ready))
|
||||
const lockedCategories = computed(() =>
|
||||
votingReadiness.value.filter((category) => category.candidateCount === 0 || category.reviewCount > 0),
|
||||
)
|
||||
const stats = computed(() => [
|
||||
{ label: 'Stimmen gesamt', value: totalVotes.value, icon: Vote },
|
||||
{ label: 'Voting-ready', value: readyCount.value, icon: CheckCircle2 },
|
||||
{ label: 'Gesperrt', value: lockedCategories.value.length, icon: LockKeyhole },
|
||||
{ label: 'Kategorien', value: seasonDetail.value.categories.length, icon: Tags },
|
||||
{ label: 'Kandidaten', value: seasonDetail.value.candidates.length, icon: Users },
|
||||
])
|
||||
const votingChecklist = computed(() => [
|
||||
{
|
||||
@@ -39,7 +44,7 @@ const votingChecklist = computed(() => [
|
||||
},
|
||||
{
|
||||
label: 'Alle Kategorien haben Kandidaten',
|
||||
done: notReadyCategories.value.every((category) => category.candidateCount > 0) && seasonDetail.value.categories.length > 0,
|
||||
done: votingReadiness.value.every((category) => category.candidateCount > 0) && seasonDetail.value.categories.length > 0,
|
||||
note: `${notReadyCategories.value.filter((category) => category.candidateCount === 0).length} Kategorien ohne Kandidaten`,
|
||||
to: '/admin/categories',
|
||||
},
|
||||
@@ -49,6 +54,12 @@ const votingChecklist = computed(() => [
|
||||
note: `${seasonDetail.value.pendingNominations.length} offene Reviews`,
|
||||
to: '/admin/reviews',
|
||||
},
|
||||
{
|
||||
label: 'Risikohinweise geprüft',
|
||||
done: store.admin.riskFlags.length === 0,
|
||||
note: `${store.admin.riskFlags.length} offene Flags vor Ergebnisfreigabe`,
|
||||
to: '/admin/risk',
|
||||
},
|
||||
])
|
||||
</script>
|
||||
|
||||
@@ -56,8 +67,8 @@ const votingChecklist = computed(() => [
|
||||
<div class="space-y-6">
|
||||
<AdminPageHeader
|
||||
eyebrow="Voting"
|
||||
title="Voting-Status und Rankings"
|
||||
description="Prüfe, ob Kategorien Kandidaten besitzen, ob das Jahr in der richtigen Phase ist und welche Kategorien aktuell die meiste Aktivität erzeugen."
|
||||
title="Voting freigeben und absichern"
|
||||
description="Diese Ansicht ist jetzt operativ: Phase, Kandidatenbasis, offene Reviews und Risiko-Flags entscheiden, ob eine Kategorie fürs Community Voting bereit ist."
|
||||
:icon="Vote"
|
||||
/>
|
||||
|
||||
@@ -78,34 +89,6 @@ const votingChecklist = computed(() => [
|
||||
</section>
|
||||
|
||||
<section class="grid gap-6 xl:grid-cols-[1.08fr_0.92fr]">
|
||||
<Card class="p-6">
|
||||
<div class="flex items-end justify-between gap-4">
|
||||
<div>
|
||||
<p class="text-xs font-semibold uppercase tracking-[0.24em] text-violet-500">Ranking</p>
|
||||
<h2 class="mt-2 font-[Cormorant_Garamond] text-4xl text-violet-800">Top Kategorien</h2>
|
||||
</div>
|
||||
<BarChart3 class="h-6 w-6 text-violet-500" />
|
||||
</div>
|
||||
|
||||
<div class="mt-6 space-y-4">
|
||||
<div v-for="(category, index) in store.admin.topCategories" :key="category.category" class="rounded-[22px] 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.16em] text-violet-500">#{{ index + 1 }}</p>
|
||||
<h3 class="mt-1 font-semibold text-slate-900">{{ category.category }}</h3>
|
||||
</div>
|
||||
<strong class="text-violet-800">{{ category.votes.toLocaleString('de-DE') }}</strong>
|
||||
</div>
|
||||
<div class="mt-3 h-3 overflow-hidden rounded-full bg-violet-50">
|
||||
<div class="h-full rounded-full bg-gradient-to-r from-[#c4b5fd] to-[#7c5cff]" :style="{ width: `${(category.votes / maxVotes) * 100}%` }" />
|
||||
</div>
|
||||
</div>
|
||||
<p v-if="store.admin.topCategories.length === 0" class="rounded-2xl border border-dashed border-violet-100 px-5 py-8 text-center text-sm text-slate-500">
|
||||
Noch keine Voting-Daten vorhanden.
|
||||
</p>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card class="overflow-hidden">
|
||||
<div class="border-b border-violet-100 p-5">
|
||||
<p class="text-xs font-semibold uppercase tracking-[0.24em] text-violet-500">Readiness</p>
|
||||
@@ -115,7 +98,7 @@ const votingChecklist = computed(() => [
|
||||
<div v-for="category in votingReadiness" :key="category.id" class="grid grid-cols-[minmax(0,1fr)_auto] gap-4 px-5 py-4">
|
||||
<div class="min-w-0">
|
||||
<p class="truncate font-semibold text-slate-900">{{ category.name }}</p>
|
||||
<p class="mt-1 truncate text-sm text-slate-500">{{ category.groupName }} · {{ category.candidateCount }} Kandidaten</p>
|
||||
<p class="mt-1 truncate text-sm text-slate-500">{{ category.groupName }} · {{ category.candidateCount }} Kandidaten · {{ category.reviewCount }} Reviews</p>
|
||||
</div>
|
||||
<span class="h-fit rounded-full border px-3 py-1 text-xs font-semibold" :class="category.ready ? 'border-emerald-100 bg-emerald-50 text-emerald-700' : 'border-amber-100 bg-amber-50 text-amber-700'">
|
||||
{{ category.ready ? 'bereit' : 'prüfen' }}
|
||||
@@ -123,12 +106,38 @@ const votingChecklist = computed(() => [
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card class="p-6">
|
||||
<div class="flex items-start justify-between gap-4">
|
||||
<div>
|
||||
<p class="text-xs font-semibold uppercase tracking-[0.24em] text-violet-500">Sperrgründe</p>
|
||||
<h2 class="mt-2 font-[Cormorant_Garamond] text-4xl text-violet-800">Was blockiert?</h2>
|
||||
</div>
|
||||
<ShieldAlert class="h-6 w-6 text-amber-500" />
|
||||
</div>
|
||||
<div class="mt-5 space-y-3">
|
||||
<RouterLink
|
||||
v-for="category in lockedCategories"
|
||||
:key="category.id"
|
||||
:to="category.candidateCount === 0 ? '/admin/candidates' : '/admin/reviews'"
|
||||
class="block rounded-[22px] border border-amber-100 bg-amber-50/50 p-4 transition hover:bg-amber-50"
|
||||
>
|
||||
<p class="font-semibold text-slate-900">{{ category.name }}</p>
|
||||
<p class="mt-1 text-sm text-slate-600">
|
||||
{{ category.candidateCount === 0 ? 'Keine Kandidaten gepflegt.' : `${category.reviewCount} offene Reviews vor Voting-Freigabe.` }}
|
||||
</p>
|
||||
</RouterLink>
|
||||
<p v-if="lockedCategories.length === 0" class="rounded-[22px] border border-emerald-100 bg-emerald-50/50 px-5 py-6 text-sm text-emerald-700">
|
||||
Keine Kategorie ist durch Inhalt oder Review-Backlog blockiert.
|
||||
</p>
|
||||
</div>
|
||||
</Card>
|
||||
</section>
|
||||
|
||||
<Card class="p-6">
|
||||
<p class="text-xs font-semibold uppercase tracking-[0.24em] text-violet-500">Voting Checkliste</p>
|
||||
<h2 class="mt-2 font-[Cormorant_Garamond] text-4xl text-violet-800">Vor dem Public Push</h2>
|
||||
<div class="mt-5 grid gap-3 lg:grid-cols-3">
|
||||
<div class="mt-5 grid gap-3 lg:grid-cols-4">
|
||||
<RouterLink
|
||||
v-for="item in votingChecklist"
|
||||
:key="item.label"
|
||||
|
||||
Reference in New Issue
Block a user