162 lines
7.5 KiB
Vue
162 lines
7.5 KiB
Vue
<script setup lang="ts">
|
|
import { computed } from '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(() => 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,
|
|
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 },
|
|
])
|
|
const votingChecklist = computed(() => [
|
|
{
|
|
label: 'Voting-Phase aktiv',
|
|
done: seasonDetail.value.currentPhase.toLowerCase().includes('voting'),
|
|
note: seasonDetail.value.currentPhase || 'Keine Phase gesetzt',
|
|
to: '/admin/settings',
|
|
},
|
|
{
|
|
label: 'Alle Kategorien haben Kandidaten',
|
|
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',
|
|
},
|
|
{
|
|
label: 'Offene Reviews niedrig',
|
|
done: seasonDetail.value.pendingNominations.length === 0,
|
|
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>
|
|
|
|
<template>
|
|
<div class="space-y-6">
|
|
<AdminPageHeader
|
|
eyebrow="Voting"
|
|
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"
|
|
/>
|
|
|
|
<AdminSeasonToolbar />
|
|
|
|
<section class="grid gap-4 md:grid-cols-2 xl:grid-cols-4">
|
|
<Card v-for="stat in stats" :key="stat.label" class="p-5">
|
|
<div class="flex items-start justify-between gap-4">
|
|
<div>
|
|
<p class="text-xs font-semibold uppercase tracking-[0.18em] text-violet-500">{{ stat.label }}</p>
|
|
<strong class="mt-3 block text-3xl text-violet-900">{{ stat.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="stat.icon" class="h-5 w-5" />
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
</section>
|
|
|
|
<section class="grid gap-6 xl:grid-cols-[1.08fr_0.92fr]">
|
|
<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>
|
|
<h2 class="mt-2 font-[Cormorant_Garamond] text-4xl text-violet-800">Kategorie-Check</h2>
|
|
</div>
|
|
<div class="max-h-[620px] divide-y divide-violet-50 overflow-y-auto">
|
|
<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 · {{ 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' }}
|
|
</span>
|
|
</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-4">
|
|
<RouterLink
|
|
v-for="item in votingChecklist"
|
|
:key="item.label"
|
|
:to="item.to"
|
|
class="rounded-[22px] border p-4 transition hover:-translate-y-0.5 hover:bg-violet-50/50"
|
|
:class="item.done ? 'border-emerald-100 bg-emerald-50/40' : 'border-amber-100 bg-amber-50/50'"
|
|
>
|
|
<div class="flex items-start justify-between gap-3">
|
|
<div>
|
|
<p class="font-semibold text-slate-900">{{ item.label }}</p>
|
|
<p class="mt-1 text-sm leading-5 text-slate-500">{{ item.note }}</p>
|
|
</div>
|
|
<span class="rounded-full px-3 py-1 text-xs font-semibold" :class="item.done ? 'bg-emerald-100 text-emerald-700' : 'bg-amber-100 text-amber-700'">
|
|
{{ item.done ? 'ok' : 'prüfen' }}
|
|
</span>
|
|
</div>
|
|
</RouterLink>
|
|
</div>
|
|
</Card>
|
|
</div>
|
|
</template>
|