Unify frontend German UI copy
This commit is contained in:
@@ -80,10 +80,10 @@ async function login(role: 'viewer' | 'admin') {
|
|||||||
<div class="rounded-full border border-violet-100 bg-violet-50/70 px-4 py-2 text-sm text-violet-800">
|
<div class="rounded-full border border-violet-100 bg-violet-50/70 px-4 py-2 text-sm text-violet-800">
|
||||||
{{ authStore.session.displayName }} · {{ authStore.session.role }}
|
{{ authStore.session.displayName }} · {{ authStore.session.role }}
|
||||||
</div>
|
</div>
|
||||||
<Button variant="ghost" @click="authStore.logout()">Logout</Button>
|
<Button variant="ghost" @click="authStore.logout()">Abmelden</Button>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<Button variant="ghost" @click="loginOpen = !loginOpen">Sign in</Button>
|
<Button variant="ghost" @click="loginOpen = !loginOpen">Einloggen</Button>
|
||||||
<Button @click="login('viewer')">Mit Twitch Login</Button>
|
<Button @click="login('viewer')">Mit Twitch Login</Button>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
@@ -91,13 +91,13 @@ async function login(role: 'viewer' | 'admin') {
|
|||||||
|
|
||||||
<div v-if="loginOpen && !authStore.session" class="rounded-[28px] border border-violet-100 bg-white/80 p-5">
|
<div v-if="loginOpen && !authStore.session" class="rounded-[28px] border border-violet-100 bg-white/80 p-5">
|
||||||
<div class="grid gap-4 md:grid-cols-3">
|
<div class="grid gap-4 md:grid-cols-3">
|
||||||
<input v-model="loginForm.displayName" type="text" class="rounded-2xl border border-violet-200 px-4 py-3" placeholder="Display Name" />
|
<input v-model="loginForm.displayName" type="text" class="rounded-2xl border border-violet-200 px-4 py-3" placeholder="Anzeigename" />
|
||||||
<input v-model="loginForm.twitchUserId" type="text" class="rounded-2xl border border-violet-200 px-4 py-3" placeholder="Twitch User ID" />
|
<input v-model="loginForm.twitchUserId" type="text" class="rounded-2xl border border-violet-200 px-4 py-3" placeholder="Twitch Nutzer-ID" />
|
||||||
<div class="flex flex-wrap gap-3">
|
<div class="flex flex-wrap gap-3">
|
||||||
<Button :disabled="authStore.loading" @click="login('viewer')">
|
<Button :disabled="authStore.loading" @click="login('viewer')">
|
||||||
{{ authStore.loading ? 'Loggt ein ...' : 'Viewer Login' }}
|
{{ authStore.loading ? 'Loggt ein ...' : 'Viewer-Login' }}
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="secondary" :disabled="authStore.loading" @click="login('admin')">Admin Login</Button>
|
<Button variant="secondary" :disabled="authStore.loading" @click="login('admin')">Admin-Login</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p v-if="loginError" class="mt-3 text-sm text-rose-700">{{ loginError }}</p>
|
<p v-if="loginError" class="mt-3 text-sm text-rose-700">{{ loginError }}</p>
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ const currentSeason = computed(() => store.adminSeasonDetail)
|
|||||||
{{ currentSeason.currentPhase || 'Kein Status' }}
|
{{ currentSeason.currentPhase || 'Kein Status' }}
|
||||||
</span>
|
</span>
|
||||||
<span class="rounded-full border border-violet-100 bg-white px-3 py-1.5 text-xs font-semibold text-slate-600">
|
<span class="rounded-full border border-violet-100 bg-white px-3 py-1.5 text-xs font-semibold text-slate-600">
|
||||||
{{ currentSeason.isCurrent ? 'Public-Jahr' : 'Nicht aktiv' }}
|
{{ currentSeason.isCurrent ? 'Oeffentliches Jahr' : 'Nicht aktiv' }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -89,14 +89,14 @@ const fallbackArchive: WinnerArchiveResponse = {
|
|||||||
const fallbackAdmin: AdminDashboardResponse = {
|
const fallbackAdmin: AdminDashboardResponse = {
|
||||||
metrics: [
|
metrics: [
|
||||||
{ label: 'Nominierungen', value: 12341, note: '+12.4% vs. gestern' },
|
{ label: 'Nominierungen', value: 12341, note: '+12.4% vs. gestern' },
|
||||||
{ label: 'Votes', value: 587231, note: '+8.7% vs. gestern' },
|
{ label: 'Stimmen', value: 587231, note: '+8.7% vs. gestern' },
|
||||||
{ label: 'Kategorien', value: 28, note: 'aktiv im Jahr 2026' },
|
{ label: 'Kategorien', value: 28, note: 'aktiv im Jahr 2026' },
|
||||||
{ label: 'Reviews offen', value: 47, note: '14 neu' },
|
{ label: 'Reviews offen', value: 47, note: '14 neu' },
|
||||||
],
|
],
|
||||||
activities: [
|
activities: [
|
||||||
{ label: 'Neue Nominierung in Best New VTuber', age: 'vor 2 Min.' },
|
{ label: 'Neue Nominierung in Bester neuer VTuber', age: 'vor 2 Min.' },
|
||||||
{ label: 'Clip-Dublette erkannt in Clip des Jahres', age: 'vor 7 Min.' },
|
{ label: 'Clip-Dublette erkannt in Clip des Jahres', age: 'vor 7 Min.' },
|
||||||
{ label: 'Alias-Merge fuer Hoshimi Miyu reviewt', age: 'vor 18 Min.' },
|
{ label: 'Alias-Zusammenfuehrung fuer Hoshimi Miyu geprueft', age: 'vor 18 Min.' },
|
||||||
],
|
],
|
||||||
topCategories: [
|
topCategories: [
|
||||||
{ category: 'VTuber des Jahres', votes: 186321 },
|
{ category: 'VTuber des Jahres', votes: 186321 },
|
||||||
@@ -132,7 +132,7 @@ const fallbackAdmin: AdminDashboardResponse = {
|
|||||||
|
|
||||||
const fallbackAdminSeasons: AdminSeasonListItem[] = [
|
const fallbackAdminSeasons: AdminSeasonListItem[] = [
|
||||||
{ id: 1, year: 2026, name: 'VTuber Star Awards 2026', currentPhase: 'Community Voting', isCurrent: true, categoryCount: 4 },
|
{ id: 1, year: 2026, name: 'VTuber Star Awards 2026', currentPhase: 'Community Voting', isCurrent: true, categoryCount: 4 },
|
||||||
{ id: 2, year: 2025, name: 'VTuber Star Awards 2025', currentPhase: 'Archived', isCurrent: false, categoryCount: 3 },
|
{ id: 2, year: 2025, name: 'VTuber Star Awards 2025', currentPhase: 'Archiviert', isCurrent: false, categoryCount: 3 },
|
||||||
]
|
]
|
||||||
|
|
||||||
const fallbackAdminSeasonDetail: AdminSeasonDetailResponse = {
|
const fallbackAdminSeasonDetail: AdminSeasonDetailResponse = {
|
||||||
@@ -144,7 +144,7 @@ const fallbackAdminSeasonDetail: AdminSeasonDetailResponse = {
|
|||||||
categories: [
|
categories: [
|
||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
groupName: 'Main Awards',
|
groupName: 'Hauptpreise',
|
||||||
name: 'VTuber des Jahres',
|
name: 'VTuber des Jahres',
|
||||||
slug: 'vtuber-des-jahres',
|
slug: 'vtuber-des-jahres',
|
||||||
description: 'Die groesste Auszeichnung des Jahres.',
|
description: 'Die groesste Auszeichnung des Jahres.',
|
||||||
|
|||||||
@@ -55,10 +55,10 @@ const heroYear = computed(() => store.overview.year)
|
|||||||
<Card class="min-h-[210px] p-7">
|
<Card class="min-h-[210px] p-7">
|
||||||
<div class="flex items-center gap-3 text-violet-600">
|
<div class="flex items-center gap-3 text-violet-600">
|
||||||
<Sparkles class="h-5 w-5 text-amber-500" />
|
<Sparkles class="h-5 w-5 text-amber-500" />
|
||||||
<span class="text-xs font-semibold uppercase tracking-[0.25em]">Community powered</span>
|
<span class="text-xs font-semibold uppercase tracking-[0.25em]">Von der Community getragen</span>
|
||||||
</div>
|
</div>
|
||||||
<p class="mt-5 text-sm leading-7 text-slate-600">
|
<p class="mt-5 text-sm leading-7 text-slate-600">
|
||||||
Twitch Login only, keine Konto-Huerde, editierbare Votes und Nominierungen bis zur Deadline.
|
Nur Twitch Login, keine Konto-Huerde, editierbare Stimmen und Nominierungen bis zur Deadline.
|
||||||
</p>
|
</p>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
@@ -81,17 +81,17 @@ const heroYear = computed(() => store.overview.year)
|
|||||||
{{ store.overview.currentPhase }}
|
{{ store.overview.currentPhase }}
|
||||||
</h2>
|
</h2>
|
||||||
<p class="mt-2 max-w-md text-slate-600">
|
<p class="mt-2 max-w-md text-slate-600">
|
||||||
Login bleibt leichtgewichtig: Twitch only, kein separates Community-Konto.
|
Login bleibt leichtgewichtig: nur Twitch, kein separates Community-Konto.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="rounded-[26px] border border-violet-100 bg-violet-50/70 px-5 py-5 text-sm text-slate-700">
|
<div class="rounded-[26px] border border-violet-100 bg-violet-50/70 px-5 py-5 text-sm text-slate-700">
|
||||||
<p class="text-xs font-semibold uppercase tracking-[0.25em] text-violet-500">Session Status</p>
|
<p class="text-xs font-semibold uppercase tracking-[0.25em] text-violet-500">Sitzungsstatus</p>
|
||||||
<p class="mt-2 font-semibold text-violet-800">
|
<p class="mt-2 font-semibold text-violet-800">
|
||||||
{{ authStore.isLoggedIn ? `${authStore.session?.displayName} · ${authStore.session?.role}` : 'Noch nicht eingeloggt' }}
|
{{ authStore.isLoggedIn ? `${authStore.session?.displayName} · ${authStore.session?.role}` : 'Noch nicht eingeloggt' }}
|
||||||
</p>
|
</p>
|
||||||
<p class="mt-2 leading-7 text-slate-600">
|
<p class="mt-2 leading-7 text-slate-600">
|
||||||
{{ authStore.isLoggedIn ? 'Nominierung und Voting sind jetzt direkt freigeschaltet.' : 'Bitte oben im Header einloggen, um Nominierung, Voting oder Admin zu nutzen.' }}
|
{{ authStore.isLoggedIn ? 'Nominierung und Voting sind jetzt direkt freigeschaltet.' : 'Bitte oben im Kopfbereich einloggen, um Nominierung, Voting oder Admin zu nutzen.' }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -150,7 +150,7 @@ const heroYear = computed(() => store.overview.year)
|
|||||||
</div>
|
</div>
|
||||||
<div class="rounded-[26px] border border-white/70 bg-white/72 px-5 py-5">
|
<div class="rounded-[26px] border border-white/70 bg-white/72 px-5 py-5">
|
||||||
<p class="text-xs font-semibold uppercase tracking-[0.25em] text-violet-500">Winner Model</p>
|
<p class="text-xs font-semibold uppercase tracking-[0.25em] text-violet-500">Winner Model</p>
|
||||||
<p class="mt-2 font-[Cormorant_Garamond] text-3xl text-violet-800">Community only</p>
|
<p class="mt-2 font-[Cormorant_Garamond] text-3xl text-violet-800">Nur Community</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="rounded-[26px] border border-white/70 bg-white/72 px-5 py-5">
|
<div class="rounded-[26px] border border-white/70 bg-white/72 px-5 py-5">
|
||||||
<p class="text-xs font-semibold uppercase tracking-[0.25em] text-violet-500">Login</p>
|
<p class="text-xs font-semibold uppercase tracking-[0.25em] text-violet-500">Login</p>
|
||||||
@@ -245,7 +245,7 @@ const heroYear = computed(() => store.overview.year)
|
|||||||
<ul class="mt-5 space-y-3 text-slate-600">
|
<ul class="mt-5 space-y-3 text-slate-600">
|
||||||
<li>Pro Kategorie keine doppelte Nominierung derselben Person.</li>
|
<li>Pro Kategorie keine doppelte Nominierung derselben Person.</li>
|
||||||
<li>Regeln werden direkt im Formular sichtbar gemacht.</li>
|
<li>Regeln werden direkt im Formular sichtbar gemacht.</li>
|
||||||
<li>Freitext-Ideen und Alias-Faelle gehen spaeter in die Review Queue.</li>
|
<li>Freitext-Ideen und Alias-Faelle gehen spaeter in die Review-Liste.</li>
|
||||||
</ul>
|
</ul>
|
||||||
<div class="mt-6">
|
<div class="mt-6">
|
||||||
<RouterLink to="/nominations">
|
<RouterLink to="/nominations">
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ async function submitNomination() {
|
|||||||
<ul class="mt-5 space-y-4 text-slate-600">
|
<ul class="mt-5 space-y-4 text-slate-600">
|
||||||
<li>Pro Kategorie nur eine Nominierung derselben Person.</li>
|
<li>Pro Kategorie nur eine Nominierung derselben Person.</li>
|
||||||
<li>Insgesamt maximal drei Nominierungen in diesem Draft.</li>
|
<li>Insgesamt maximal drei Nominierungen in diesem Draft.</li>
|
||||||
<li>Freitext-Ideen landen spaeter in der Review Queue.</li>
|
<li>Freitext-Ideen landen spaeter in der Review-Liste.</li>
|
||||||
<li>Bereits gespeicherte Entwuerfe koennen bis zur Deadline bearbeitet werden.</li>
|
<li>Bereits gespeicherte Entwuerfe koennen bis zur Deadline bearbeitet werden.</li>
|
||||||
</ul>
|
</ul>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -91,7 +91,7 @@ async function submitNomination() {
|
|||||||
<div class="grid gap-6 lg:grid-cols-[0.8fr_1.2fr]">
|
<div class="grid gap-6 lg:grid-cols-[0.8fr_1.2fr]">
|
||||||
<div class="space-y-5">
|
<div class="space-y-5">
|
||||||
<p v-if="!authStore.isLoggedIn" class="rounded-[26px] border border-amber-200 bg-amber-50 px-5 py-4 text-sm text-amber-700">
|
<p v-if="!authStore.isLoggedIn" class="rounded-[26px] border border-amber-200 bg-amber-50 px-5 py-4 text-sm text-amber-700">
|
||||||
Bitte zuerst ueber den Header mit einem Twitch-Account einloggen, damit die Nominierung gespeichert werden kann.
|
Bitte zuerst ueber den Kopfbereich mit einem Twitch-Account einloggen, damit die Nominierung gespeichert werden kann.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<label class="text-sm font-semibold text-slate-600">Kategorie</label>
|
<label class="text-sm font-semibold text-slate-600">Kategorie</label>
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ async function submitVote() {
|
|||||||
<div class="grid gap-7 lg:grid-cols-[0.72fr_1.28fr]">
|
<div class="grid gap-7 lg:grid-cols-[0.72fr_1.28fr]">
|
||||||
<div class="space-y-5">
|
<div class="space-y-5">
|
||||||
<p v-if="!authStore.isLoggedIn" class="rounded-[26px] border border-amber-200 bg-amber-50 px-5 py-4 text-sm text-amber-700">
|
<p v-if="!authStore.isLoggedIn" class="rounded-[26px] border border-amber-200 bg-amber-50 px-5 py-4 text-sm text-amber-700">
|
||||||
Bitte zuerst ueber den Header mit einem Twitch-Account einloggen, damit deine Stimme gespeichert werden kann.
|
Bitte zuerst ueber den Kopfbereich mit einem Twitch-Account einloggen, damit deine Stimme gespeichert werden kann.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div class="space-y-3">
|
<div class="space-y-3">
|
||||||
|
|||||||
@@ -136,7 +136,7 @@ function clearFilters() {
|
|||||||
<template>
|
<template>
|
||||||
<div class="space-y-6">
|
<div class="space-y-6">
|
||||||
<AdminPageHeader
|
<AdminPageHeader
|
||||||
eyebrow="Candidates"
|
eyebrow="Kandidaten"
|
||||||
title="Kandidatenbasis pflegen"
|
title="Kandidatenbasis pflegen"
|
||||||
description="Schneller finden, sauber pruefen, gezielt bearbeiten: die Kandidaten sind jetzt nach Jahr, Kategorie und Handle besser steuerbar."
|
description="Schneller finden, sauber pruefen, gezielt bearbeiten: die Kandidaten sind jetzt nach Jahr, Kategorie und Handle besser steuerbar."
|
||||||
/>
|
/>
|
||||||
@@ -160,7 +160,7 @@ function clearFilters() {
|
|||||||
<div class="border-b border-violet-100 bg-white/70 p-6">
|
<div class="border-b border-violet-100 bg-white/70 p-6">
|
||||||
<div class="flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between">
|
<div class="flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between">
|
||||||
<div>
|
<div>
|
||||||
<p class="text-xs font-semibold uppercase tracking-[0.26em] text-violet-500">Kandidaten Center</p>
|
<p class="text-xs font-semibold uppercase tracking-[0.26em] text-violet-500">Kandidatenbereich</p>
|
||||||
<h2 class="mt-2 font-[Cormorant_Garamond] text-4xl text-violet-800">Suchen, pruefen, aktualisieren</h2>
|
<h2 class="mt-2 font-[Cormorant_Garamond] text-4xl text-violet-800">Suchen, pruefen, aktualisieren</h2>
|
||||||
<p class="mt-2 max-w-2xl text-sm leading-6 text-slate-500">
|
<p class="mt-2 max-w-2xl text-sm leading-6 text-slate-500">
|
||||||
Filtere nach Kategorie oder Handle und bearbeite nur den Kandidaten, der wirklich geaendert werden muss.
|
Filtere nach Kategorie oder Handle und bearbeite nur den Kandidaten, der wirklich geaendert werden muss.
|
||||||
@@ -229,8 +229,8 @@ function clearFilters() {
|
|||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
<label class="space-y-2">
|
<label class="space-y-2">
|
||||||
<span class="text-xs font-semibold uppercase tracking-[0.18em] text-slate-500">Display Name</span>
|
<span class="text-xs font-semibold uppercase tracking-[0.18em] text-slate-500">Anzeigename</span>
|
||||||
<input v-model="candidateForms[candidate.id].displayName" type="text" class="h-12 w-full rounded-2xl border border-violet-200 bg-white px-4 text-sm outline-none transition focus:border-violet-400 focus:ring-4 focus:ring-violet-100" placeholder="Display Name" />
|
<input v-model="candidateForms[candidate.id].displayName" type="text" class="h-12 w-full rounded-2xl border border-violet-200 bg-white px-4 text-sm outline-none transition focus:border-violet-400 focus:ring-4 focus:ring-violet-100" placeholder="Anzeigename" />
|
||||||
</label>
|
</label>
|
||||||
<label class="space-y-2">
|
<label class="space-y-2">
|
||||||
<span class="text-xs font-semibold uppercase tracking-[0.18em] text-slate-500">Handle</span>
|
<span class="text-xs font-semibold uppercase tracking-[0.18em] text-slate-500">Handle</span>
|
||||||
@@ -275,7 +275,7 @@ function clearFilters() {
|
|||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
<label class="space-y-2">
|
<label class="space-y-2">
|
||||||
<span class="text-xs font-semibold uppercase tracking-[0.18em] text-slate-500">Display Name</span>
|
<span class="text-xs font-semibold uppercase tracking-[0.18em] text-slate-500">Anzeigename</span>
|
||||||
<input v-model="newCandidateForm.displayName" type="text" class="h-12 w-full rounded-2xl border border-violet-200 px-4 text-sm outline-none transition focus:border-violet-400 focus:ring-4 focus:ring-violet-100" placeholder="z.B. Jayuhime" />
|
<input v-model="newCandidateForm.displayName" type="text" class="h-12 w-full rounded-2xl border border-violet-200 px-4 text-sm outline-none transition focus:border-violet-400 focus:ring-4 focus:ring-violet-100" placeholder="z.B. Jayuhime" />
|
||||||
</label>
|
</label>
|
||||||
<label class="space-y-2">
|
<label class="space-y-2">
|
||||||
@@ -302,7 +302,7 @@ function clearFilters() {
|
|||||||
<Card class="p-5">
|
<Card class="p-5">
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<Sparkles class="h-5 w-5 text-amber-500" />
|
<Sparkles class="h-5 w-5 text-amber-500" />
|
||||||
<p class="text-xs font-semibold uppercase tracking-[0.24em] text-violet-500">Workflow</p>
|
<p class="text-xs font-semibold uppercase tracking-[0.24em] text-violet-500">Ablauf</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-4 space-y-3 text-sm leading-6 text-slate-600">
|
<div class="mt-4 space-y-3 text-sm leading-6 text-slate-600">
|
||||||
<p class="flex gap-3"><Users class="mt-0.5 h-4 w-4 shrink-0 text-violet-500" /> Erst Kandidaten suchen, damit du keine Duplikate anlegst.</p>
|
<p class="flex gap-3"><Users class="mt-0.5 h-4 w-4 shrink-0 text-violet-500" /> Erst Kandidaten suchen, damit du keine Duplikate anlegst.</p>
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ const quickActions = computed(() => [
|
|||||||
hint: 'Freitext-Nominierungen bearbeiten',
|
hint: 'Freitext-Nominierungen bearbeiten',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Offene Risk Flags',
|
label: 'Offene Risikohinweise',
|
||||||
value: store.admin.riskFlags.length,
|
value: store.admin.riskFlags.length,
|
||||||
to: '/admin/risk',
|
to: '/admin/risk',
|
||||||
hint: 'auffaellige Muster pruefen',
|
hint: 'auffaellige Muster pruefen',
|
||||||
@@ -44,9 +44,9 @@ const quickActions = computed(() => [
|
|||||||
<template>
|
<template>
|
||||||
<div class="space-y-6">
|
<div class="space-y-6">
|
||||||
<AdminPageHeader
|
<AdminPageHeader
|
||||||
eyebrow="Dashboard"
|
eyebrow="Uebersicht"
|
||||||
title="Was braucht gerade Aufmerksamkeit?"
|
title="Was braucht gerade Aufmerksamkeit?"
|
||||||
description="Das Dashboard zeigt dir zuerst die offenen Arbeitsstapel und fuehrt dich mit Quick Actions direkt in den passenden Bereich."
|
description="Die Uebersicht zeigt dir zuerst die offenen Arbeitsstapel und fuehrt dich mit Schnellzugriffen direkt in den passenden Bereich."
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div class="grid gap-4 md:grid-cols-2 xl:grid-cols-4">
|
<div class="grid gap-4 md:grid-cols-2 xl:grid-cols-4">
|
||||||
@@ -76,10 +76,10 @@ const quickActions = computed(() => [
|
|||||||
|
|
||||||
<div class="grid gap-6 xl:grid-cols-[1.05fr_0.95fr]">
|
<div class="grid gap-6 xl:grid-cols-[1.05fr_0.95fr]">
|
||||||
<Card class="p-7">
|
<Card class="p-7">
|
||||||
<h2 class="font-[Cormorant_Garamond] text-4xl text-violet-800">Top Kategorien nach Votes</h2>
|
<h2 class="font-[Cormorant_Garamond] text-4xl text-violet-800">Top Kategorien nach Stimmen</h2>
|
||||||
<DataTable :value="topCategories" class="mt-6" striped-rows>
|
<DataTable :value="topCategories" class="mt-6" striped-rows>
|
||||||
<Column field="category" header="Kategorie" />
|
<Column field="category" header="Kategorie" />
|
||||||
<Column field="votes" header="Votes">
|
<Column field="votes" header="Stimmen">
|
||||||
<template #body="{ data }">
|
<template #body="{ data }">
|
||||||
{{ Number(data.votes).toLocaleString('de-DE') }}
|
{{ Number(data.votes).toLocaleString('de-DE') }}
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -12,11 +12,11 @@ const store = useAwardsStore()
|
|||||||
const authStore = useAuthStore()
|
const authStore = useAuthStore()
|
||||||
|
|
||||||
const navItems = [
|
const navItems = [
|
||||||
{ label: 'Dashboard', to: '/admin/dashboard', description: 'Status, Trends, Quick Actions', icon: LayoutDashboard, badge: () => null },
|
{ label: 'Uebersicht', to: '/admin/dashboard', description: 'Status, Trends, Schnellzugriffe', icon: LayoutDashboard, badge: () => null },
|
||||||
{ label: 'Jahre', to: '/admin/years', description: 'Jahresstatus, Kategorien, Limits', icon: CalendarCog, badge: () => `${store.adminSeasonDetail.categories.length}` },
|
{ label: 'Jahre', to: '/admin/years', description: 'Jahresstatus, Kategorien, Limits', icon: CalendarCog, badge: () => `${store.adminSeasonDetail.categories.length}` },
|
||||||
{ label: 'Candidates', to: '/admin/candidates', description: 'Kandidatenbasis pro Jahr pflegen', icon: Users, badge: () => `${store.adminSeasonDetail.candidates.length}` },
|
{ label: 'Kandidaten', to: '/admin/candidates', description: 'Kandidatenbasis pro Jahr pflegen', icon: Users, badge: () => `${store.adminSeasonDetail.candidates.length}` },
|
||||||
{ label: 'Reviews', to: '/admin/reviews', description: 'Freitext-Nominierungen bearbeiten', icon: Sparkles, badge: () => `${store.adminSeasonDetail.pendingNominations.length}` },
|
{ label: 'Reviews', to: '/admin/reviews', description: 'Freitext-Nominierungen bearbeiten', icon: Sparkles, badge: () => `${store.adminSeasonDetail.pendingNominations.length}` },
|
||||||
{ label: 'Risk & Audit', to: '/admin/risk', description: 'Flags pruefen, Aktionen nachvollziehen', icon: AlertTriangle, badge: () => `${store.admin.riskFlags.length}` },
|
{ label: 'Risiko & Audit', to: '/admin/risk', description: 'Hinweise pruefen, Aktionen nachvollziehen', icon: AlertTriangle, badge: () => `${store.admin.riskFlags.length}` },
|
||||||
]
|
]
|
||||||
|
|
||||||
const currentSeason = computed(() => store.adminSeasonDetail)
|
const currentSeason = computed(() => store.adminSeasonDetail)
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ async function rejectNomination(nominationId: number) {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await store.rejectAdminNomination(nominationId, selectedSeasonId.value)
|
await store.rejectAdminNomination(nominationId, selectedSeasonId.value)
|
||||||
adminMessage.value = 'Nominierung wurde aus der Review Queue entfernt.'
|
adminMessage.value = 'Nominierung wurde aus der Review-Liste entfernt.'
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
adminError.value = error instanceof Error ? error.message : 'Nominierung konnte nicht verworfen werden.'
|
adminError.value = error instanceof Error ? error.message : 'Nominierung konnte nicht verworfen werden.'
|
||||||
} finally {
|
} finally {
|
||||||
@@ -94,7 +94,7 @@ async function rejectNomination(nominationId: number) {
|
|||||||
<Card class="p-7">
|
<Card class="p-7">
|
||||||
<div class="flex items-center justify-between gap-4">
|
<div class="flex items-center justify-between gap-4">
|
||||||
<div>
|
<div>
|
||||||
<h2 class="font-[Cormorant_Garamond] text-4xl text-violet-800">Review Queue</h2>
|
<h2 class="font-[Cormorant_Garamond] text-4xl text-violet-800">Review-Liste</h2>
|
||||||
<p class="mt-2 text-sm text-slate-500">Freitext-Nominierungen und Alias-Faelle, die das Team direkt in Kandidaten ueberfuehren oder verwerfen kann.</p>
|
<p class="mt-2 text-sm text-slate-500">Freitext-Nominierungen und Alias-Faelle, die das Team direkt in Kandidaten ueberfuehren oder verwerfen kann.</p>
|
||||||
</div>
|
</div>
|
||||||
<span class="text-sm uppercase tracking-[0.2em] text-slate-500">
|
<span class="text-sm uppercase tracking-[0.2em] text-slate-500">
|
||||||
@@ -114,7 +114,7 @@ async function rejectNomination(nominationId: number) {
|
|||||||
v-model="reviewFilter"
|
v-model="reviewFilter"
|
||||||
type="text"
|
type="text"
|
||||||
class="rounded-2xl border border-violet-200 px-4 py-3"
|
class="rounded-2xl border border-violet-200 px-4 py-3"
|
||||||
placeholder="Nach Kategorie, Nominierung oder User filtern"
|
placeholder="Nach Kategorie, Nominierung oder Nutzer filtern"
|
||||||
/>
|
/>
|
||||||
<div class="rounded-2xl border border-violet-100 bg-violet-50/60 px-4 py-3 text-sm text-slate-600">
|
<div class="rounded-2xl border border-violet-100 bg-violet-50/60 px-4 py-3 text-sm text-slate-600">
|
||||||
Tipp: Suche erst nach dem Problemfall, dann entscheide uebernehmen oder verwerfen.
|
Tipp: Suche erst nach dem Problemfall, dann entscheide uebernehmen oder verwerfen.
|
||||||
@@ -145,7 +145,7 @@ async function rejectNomination(nominationId: number) {
|
|||||||
v-model="reviewForms[nomination.id].displayName"
|
v-model="reviewForms[nomination.id].displayName"
|
||||||
type="text"
|
type="text"
|
||||||
class="rounded-2xl border border-violet-200 px-4 py-3"
|
class="rounded-2xl border border-violet-200 px-4 py-3"
|
||||||
placeholder="Display Name"
|
placeholder="Anzeigename"
|
||||||
/>
|
/>
|
||||||
<input
|
<input
|
||||||
v-model="reviewForms[nomination.id].channelSlug"
|
v-model="reviewForms[nomination.id].channelSlug"
|
||||||
@@ -157,7 +157,7 @@ async function rejectNomination(nominationId: number) {
|
|||||||
v-model="reviewForms[nomination.id].platform"
|
v-model="reviewForms[nomination.id].platform"
|
||||||
type="text"
|
type="text"
|
||||||
class="rounded-2xl border border-violet-200 px-4 py-3"
|
class="rounded-2xl border border-violet-200 px-4 py-3"
|
||||||
placeholder="Platform"
|
placeholder="Plattform"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -43,9 +43,9 @@ async function resolveRiskFlag(riskFlagId: number, status = 'resolved') {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await store.resolveRiskFlag(riskFlagId, status)
|
await store.resolveRiskFlag(riskFlagId, status)
|
||||||
adminMessage.value = `Risk Flag ${riskFlagId} wurde als ${status} markiert.`
|
adminMessage.value = `Risikohinweis ${riskFlagId} wurde aktualisiert.`
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
adminError.value = error instanceof Error ? error.message : 'Risk Flag konnte nicht aktualisiert werden.'
|
adminError.value = error instanceof Error ? error.message : 'Risikohinweis konnte nicht aktualisiert werden.'
|
||||||
} finally {
|
} finally {
|
||||||
riskSaving.value = null
|
riskSaving.value = null
|
||||||
}
|
}
|
||||||
@@ -55,7 +55,7 @@ async function resolveRiskFlag(riskFlagId: number, status = 'resolved') {
|
|||||||
<template>
|
<template>
|
||||||
<div class="space-y-6">
|
<div class="space-y-6">
|
||||||
<AdminPageHeader
|
<AdminPageHeader
|
||||||
eyebrow="Risk & Audit"
|
eyebrow="Risiko & Audit"
|
||||||
title="Auffaellige Muster und Admin-Aktionen verfolgen"
|
title="Auffaellige Muster und Admin-Aktionen verfolgen"
|
||||||
description="Dieser Bereich trennt operative Risiko-Sichtung von der Nachvollziehbarkeit. So findest du sowohl offene Flags als auch bereits ausgefuehrte Eingriffe deutlich schneller."
|
description="Dieser Bereich trennt operative Risiko-Sichtung von der Nachvollziehbarkeit. So findest du sowohl offene Flags als auch bereits ausgefuehrte Eingriffe deutlich schneller."
|
||||||
/>
|
/>
|
||||||
@@ -64,7 +64,7 @@ async function resolveRiskFlag(riskFlagId: number, status = 'resolved') {
|
|||||||
<Card class="p-7">
|
<Card class="p-7">
|
||||||
<div class="flex items-center justify-between gap-4">
|
<div class="flex items-center justify-between gap-4">
|
||||||
<div>
|
<div>
|
||||||
<h2 class="font-[Cormorant_Garamond] text-4xl text-violet-800">Risk Center</h2>
|
<h2 class="font-[Cormorant_Garamond] text-4xl text-violet-800">Risikopruefung</h2>
|
||||||
<p class="mt-2 text-sm text-slate-500">Auffaellige Login-, Nominierungs- und Voting-Muster fuer die manuelle Sichtung.</p>
|
<p class="mt-2 text-sm text-slate-500">Auffaellige Login-, Nominierungs- und Voting-Muster fuer die manuelle Sichtung.</p>
|
||||||
</div>
|
</div>
|
||||||
<span class="text-sm uppercase tracking-[0.2em] text-slate-500">
|
<span class="text-sm uppercase tracking-[0.2em] text-slate-500">
|
||||||
@@ -84,10 +84,10 @@ async function resolveRiskFlag(riskFlagId: number, status = 'resolved') {
|
|||||||
v-model="riskFilter"
|
v-model="riskFilter"
|
||||||
type="text"
|
type="text"
|
||||||
class="rounded-2xl border border-violet-200 px-4 py-3"
|
class="rounded-2xl border border-violet-200 px-4 py-3"
|
||||||
placeholder="Risk Flags nach Typ, User oder IP filtern"
|
placeholder="Risikohinweise nach Typ, Nutzer oder IP filtern"
|
||||||
/>
|
/>
|
||||||
<div class="rounded-2xl border border-violet-100 bg-violet-50/60 px-4 py-3 text-sm text-slate-600">
|
<div class="rounded-2xl border border-violet-100 bg-violet-50/60 px-4 py-3 text-sm text-slate-600">
|
||||||
Tipp: Filtere erst auf den Problemtyp und resolve dann nur den geprueften Fall.
|
Tipp: Filtere erst auf den Problemtyp und markiere dann nur den geprueften Fall.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -114,19 +114,19 @@ async function resolveRiskFlag(riskFlagId: number, status = 'resolved') {
|
|||||||
|
|
||||||
<div class="mt-4 flex flex-wrap justify-end gap-3">
|
<div class="mt-4 flex flex-wrap justify-end gap-3">
|
||||||
<Button :disabled="riskSaving === flag.id" variant="secondary" @click="resolveRiskFlag(flag.id, 'dismissed')">
|
<Button :disabled="riskSaving === flag.id" variant="secondary" @click="resolveRiskFlag(flag.id, 'dismissed')">
|
||||||
{{ riskSaving === flag.id ? 'Speichert ...' : 'Dismiss' }}
|
{{ riskSaving === flag.id ? 'Speichert ...' : 'Verwerfen' }}
|
||||||
</Button>
|
</Button>
|
||||||
<Button :disabled="riskSaving === flag.id" @click="resolveRiskFlag(flag.id, 'resolved')">
|
<Button :disabled="riskSaving === flag.id" @click="resolveRiskFlag(flag.id, 'resolved')">
|
||||||
{{ riskSaving === flag.id ? 'Speichert ...' : 'Resolve' }}
|
{{ riskSaving === flag.id ? 'Speichert ...' : 'Erledigt markieren' }}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p v-if="riskFlags.length === 0" class="rounded-[26px] border border-dashed border-violet-100 px-5 py-6 text-sm text-slate-500">
|
<p v-if="riskFlags.length === 0" class="rounded-[26px] border border-dashed border-violet-100 px-5 py-6 text-sm text-slate-500">
|
||||||
Keine offenen Risk Flags vorhanden.
|
Keine offenen Risikohinweise vorhanden.
|
||||||
</p>
|
</p>
|
||||||
<p v-else-if="filteredRiskFlags.length === 0" class="rounded-[26px] border border-dashed border-violet-100 px-5 py-6 text-sm text-slate-500">
|
<p v-else-if="filteredRiskFlags.length === 0" class="rounded-[26px] border border-dashed border-violet-100 px-5 py-6 text-sm text-slate-500">
|
||||||
Keine Risk Flags passen zum aktuellen Filter.
|
Keine Risikohinweise passen zum aktuellen Filter.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -134,7 +134,7 @@ async function resolveRiskFlag(riskFlagId: number, status = 'resolved') {
|
|||||||
<Card class="p-7">
|
<Card class="p-7">
|
||||||
<div class="flex items-center justify-between gap-4">
|
<div class="flex items-center justify-between gap-4">
|
||||||
<div>
|
<div>
|
||||||
<h2 class="font-[Cormorant_Garamond] text-4xl text-violet-800">Audit Log</h2>
|
<h2 class="font-[Cormorant_Garamond] text-4xl text-violet-800">Audit-Protokoll</h2>
|
||||||
<p class="mt-2 text-sm text-slate-500">Nachvollziehbare Admin-Aktionen fuer Kategorie-, Kandidaten- und Review-Aenderungen.</p>
|
<p class="mt-2 text-sm text-slate-500">Nachvollziehbare Admin-Aktionen fuer Kategorie-, Kandidaten- und Review-Aenderungen.</p>
|
||||||
</div>
|
</div>
|
||||||
<span class="text-sm uppercase tracking-[0.2em] text-slate-500">
|
<span class="text-sm uppercase tracking-[0.2em] text-slate-500">
|
||||||
@@ -147,7 +147,7 @@ async function resolveRiskFlag(riskFlagId: number, status = 'resolved') {
|
|||||||
v-model="auditFilter"
|
v-model="auditFilter"
|
||||||
type="text"
|
type="text"
|
||||||
class="w-full rounded-2xl border border-violet-200 px-4 py-3"
|
class="w-full rounded-2xl border border-violet-200 px-4 py-3"
|
||||||
placeholder="Audit-Eintraege nach Aktion, Admin oder Entitaet filtern"
|
placeholder="Audit-Eintraege nach Aktion, Admin oder Objekt filtern"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -147,8 +147,8 @@ async function createCategory() {
|
|||||||
<Card class="p-7">
|
<Card class="p-7">
|
||||||
<div class="flex flex-col gap-6">
|
<div class="flex flex-col gap-6">
|
||||||
<div>
|
<div>
|
||||||
<h2 class="font-[Cormorant_Garamond] text-4xl text-violet-800">Jahres-Setup</h2>
|
<h2 class="font-[Cormorant_Garamond] text-4xl text-violet-800">Jahres-Einrichtung</h2>
|
||||||
<p class="mt-2 text-sm text-slate-500">Phase, Current-Status und Basiskontext fuer das aktive Award-Jahr.</p>
|
<p class="mt-2 text-sm text-slate-500">Phase, aktueller Status und Basiskontext fuer das aktive Award-Jahr.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="space-y-3">
|
<div class="space-y-3">
|
||||||
@@ -162,7 +162,7 @@ async function createCategory() {
|
|||||||
|
|
||||||
<label class="flex items-center gap-3 rounded-2xl border border-violet-100 bg-violet-50/60 px-4 py-4 text-sm text-slate-700">
|
<label class="flex items-center gap-3 rounded-2xl border border-violet-100 bg-violet-50/60 px-4 py-4 text-sm text-slate-700">
|
||||||
<input v-model="seasonForm.isCurrent" type="checkbox" class="h-4 w-4 accent-violet-600" />
|
<input v-model="seasonForm.isCurrent" type="checkbox" class="h-4 w-4 accent-violet-600" />
|
||||||
Dieses Jahr ist das aktuelle Public-Jahr
|
Dieses Jahr ist das aktuell oeffentliche Award-Jahr
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<div class="flex flex-wrap items-center gap-4">
|
<div class="flex flex-wrap items-center gap-4">
|
||||||
@@ -186,13 +186,13 @@ async function createCategory() {
|
|||||||
<Card class="p-7">
|
<Card class="p-7">
|
||||||
<h2 class="font-[Cormorant_Garamond] text-4xl text-violet-800">Neue Kategorie</h2>
|
<h2 class="font-[Cormorant_Garamond] text-4xl text-violet-800">Neue Kategorie</h2>
|
||||||
<div class="mt-6 space-y-4">
|
<div class="mt-6 space-y-4">
|
||||||
<input v-model="newCategoryForm.groupName" type="text" class="w-full rounded-2xl border border-violet-200 px-4 py-3" placeholder="Group Name" />
|
<input v-model="newCategoryForm.groupName" type="text" class="w-full rounded-2xl border border-violet-200 px-4 py-3" placeholder="Gruppenname" />
|
||||||
<input v-model="newCategoryForm.name" type="text" class="w-full rounded-2xl border border-violet-200 px-4 py-3" placeholder="Kategorie-Name" />
|
<input v-model="newCategoryForm.name" type="text" class="w-full rounded-2xl border border-violet-200 px-4 py-3" placeholder="Kategorie-Name" />
|
||||||
<input v-model="newCategoryForm.slug" type="text" class="w-full rounded-2xl border border-violet-200 px-4 py-3" placeholder="Slug" />
|
<input v-model="newCategoryForm.slug" type="text" class="w-full rounded-2xl border border-violet-200 px-4 py-3" placeholder="Slug" />
|
||||||
<textarea v-model="newCategoryForm.description" class="min-h-28 w-full rounded-2xl border border-violet-200 px-4 py-3" placeholder="Beschreibung" />
|
<textarea v-model="newCategoryForm.description" class="min-h-28 w-full rounded-2xl border border-violet-200 px-4 py-3" placeholder="Beschreibung" />
|
||||||
<div class="grid gap-4 sm:grid-cols-2">
|
<div class="grid gap-4 sm:grid-cols-2">
|
||||||
<input v-model="newCategoryForm.sortOrder" type="number" class="rounded-2xl border border-violet-200 px-4 py-3" placeholder="Sortierung" />
|
<input v-model="newCategoryForm.sortOrder" type="number" class="rounded-2xl border border-violet-200 px-4 py-3" placeholder="Sortierung" />
|
||||||
<input v-model="newCategoryForm.maxNomineesPerUser" type="number" class="rounded-2xl border border-violet-200 px-4 py-3" placeholder="Max Nominees" />
|
<input v-model="newCategoryForm.maxNomineesPerUser" type="number" class="rounded-2xl border border-violet-200 px-4 py-3" placeholder="Max. Nominierungen" />
|
||||||
</div>
|
</div>
|
||||||
<Button :disabled="categorySaving === 'new' || !selectedSeasonId" @click="createCategory">
|
<Button :disabled="categorySaving === 'new' || !selectedSeasonId" @click="createCategory">
|
||||||
{{ categorySaving === 'new' ? 'Erstellt ...' : 'Kategorie anlegen' }}
|
{{ categorySaving === 'new' ? 'Erstellt ...' : 'Kategorie anlegen' }}
|
||||||
@@ -231,7 +231,7 @@ async function createCategory() {
|
|||||||
class="rounded-[26px] border border-violet-100 bg-white/90 p-5"
|
class="rounded-[26px] border border-violet-100 bg-white/90 p-5"
|
||||||
>
|
>
|
||||||
<div class="grid gap-4 md:grid-cols-2">
|
<div class="grid gap-4 md:grid-cols-2">
|
||||||
<input v-model="editForms[category.id].groupName" type="text" class="rounded-2xl border border-violet-200 px-4 py-3" placeholder="Group" />
|
<input v-model="editForms[category.id].groupName" type="text" class="rounded-2xl border border-violet-200 px-4 py-3" placeholder="Gruppe" />
|
||||||
<input v-model="editForms[category.id].name" type="text" class="rounded-2xl border border-violet-200 px-4 py-3" placeholder="Name" />
|
<input v-model="editForms[category.id].name" type="text" class="rounded-2xl border border-violet-200 px-4 py-3" placeholder="Name" />
|
||||||
<input v-model="editForms[category.id].slug" type="text" class="rounded-2xl border border-violet-200 px-4 py-3" placeholder="Slug" />
|
<input v-model="editForms[category.id].slug" type="text" class="rounded-2xl border border-violet-200 px-4 py-3" placeholder="Slug" />
|
||||||
<input v-model="editForms[category.id].sortOrder" type="number" class="rounded-2xl border border-violet-200 px-4 py-3" placeholder="Sortierung" />
|
<input v-model="editForms[category.id].sortOrder" type="number" class="rounded-2xl border border-violet-200 px-4 py-3" placeholder="Sortierung" />
|
||||||
|
|||||||
Reference in New Issue
Block a user