Files
vtuber-awards/frontend/src/components/admin/AdminRiskHistorySection.vue
T

155 lines
6.3 KiB
Vue

<script setup lang="ts">
import { ChevronLeft, ChevronRight, ExternalLink, RotateCcw } from '@lucide/vue'
import { RouterLink } from 'vue-router'
import Button from '../ui/Button.vue'
import type { AdminRiskFlag } from '../../types/awards'
import type { RiskResolutionStatus } from './useAdminRiskManager'
import { riskSeverityClass, riskStatusLabel } from './useAdminRiskManager'
defineProps<{
riskHistory: AdminRiskFlag[]
totalHistory: number
historyStatusFilter: 'all' | 'resolved' | 'dismissed'
historyStatusFilters: Array<{ key: 'all' | 'resolved' | 'dismissed'; label: string; count: number }>
historyStats: Array<{ label: string; value: number }>
saving: number | null
page: number
pageLabel: string
hasPrevious: boolean
hasMore: boolean
}>()
const emit = defineEmits<{
'update:historyStatusFilter': [value: 'all' | 'resolved' | 'dismissed']
decide: [riskFlagId: number, status: RiskResolutionStatus]
page: [page: number]
}>()
</script>
<template>
<section class="border-t border-violet-100 bg-violet-50/25 p-6">
<div class="flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between">
<div>
<h2 class="text-lg font-bold text-slate-900">Entscheidungsprotokoll</h2>
<p class="mt-1 text-sm leading-6 text-slate-500">
Geprüfte Hinweise mit Notiz, Reviewer und Reopen-Möglichkeit.
</p>
</div>
<div class="flex flex-wrap gap-2">
<button
v-for="filter in historyStatusFilters"
:key="filter.key"
type="button"
class="rounded-full border px-3 py-2 text-xs font-semibold transition"
:class="historyStatusFilter === filter.key ? 'border-violet-200 bg-white text-violet-800' : 'border-violet-100 bg-violet-50 text-slate-600 hover:bg-white'"
@click="emit('update:historyStatusFilter', filter.key)"
>
{{ filter.label }} · {{ filter.count }}
</button>
</div>
</div>
<div class="mt-5 grid gap-2 sm:grid-cols-3">
<div v-for="stat in historyStats" :key="stat.label" class="rounded-2xl border border-violet-100 bg-white/80 px-3 py-2">
<p class="text-xs font-semibold text-slate-500">{{ stat.label }}</p>
<strong class="mt-1 block text-lg text-violet-800">{{ stat.value }}</strong>
</div>
</div>
<div class="mt-5 grid gap-3 xl:grid-cols-2">
<article
v-for="flag in riskHistory"
:key="flag.id"
class="rounded-[22px] border border-violet-100 bg-white/90 p-4 shadow-sm shadow-violet-100/40"
>
<div class="flex flex-wrap items-start justify-between gap-3">
<div class="min-w-0">
<div class="flex flex-wrap items-center gap-2">
<span
class="rounded-full px-3 py-1 text-xs font-semibold"
:class="flag.status === 'resolved' ? 'bg-emerald-50 text-emerald-700' : 'bg-rose-50 text-rose-700'"
>
{{ riskStatusLabel(flag.status) }}
</span>
<span class="rounded-full border px-3 py-1 text-xs font-semibold" :class="riskSeverityClass(flag.severity)">
{{ flag.severity }}
</span>
</div>
<h3 class="mt-3 text-base font-semibold text-slate-950">{{ flag.summary }}</h3>
<p class="mt-2 text-sm text-slate-500">
{{ flag.source }} · {{ flag.type }} · {{ flag.twitchUserId || 'unbekannter User' }}
</p>
</div>
<Button
variant="ghost"
size="sm"
class="gap-2 rounded-2xl border border-violet-200 bg-violet-50 px-3 text-violet-700 shadow-none hover:bg-violet-100"
:disabled="saving === flag.id"
@click="emit('decide', flag.id, 'open')"
>
<RotateCcw class="h-4 w-4" />
{{ saving === flag.id ? 'Öffnet ...' : 'Wieder öffnen' }}
</Button>
</div>
<div class="mt-4 grid gap-3 text-xs text-slate-500 sm:grid-cols-2">
<p>
<span class="block font-semibold text-slate-400">Entschieden von</span>
{{ flag.reviewedByTwitchId || 'nicht protokolliert' }}
</p>
<p>
<span class="block font-semibold text-slate-400">Zeitpunkt</span>
{{ flag.reviewedAt ? new Date(flag.reviewedAt).toLocaleString('de-DE') : 'nicht protokolliert' }}
</p>
</div>
<p class="mt-4 rounded-2xl border border-violet-100 bg-violet-50/50 px-4 py-3 text-sm text-slate-600">
{{ flag.reviewNote || 'Keine Review-Notiz gespeichert.' }}
</p>
<div v-if="flag.entityLinks.length" class="mt-3 flex flex-wrap gap-2">
<RouterLink
v-for="link in flag.entityLinks"
:key="`${flag.id}-${link.entityType}-${link.entityId}-${link.to}`"
:to="link.to"
class="inline-flex items-center gap-2 rounded-xl border border-violet-100 bg-violet-50 px-3 py-2 text-xs font-semibold text-violet-800 transition hover:border-violet-200 hover:bg-violet-100"
>
<ExternalLink class="h-3.5 w-3.5" />
{{ link.label }}
</RouterLink>
</div>
</article>
<p v-if="totalHistory === 0" class="rounded-[22px] border border-dashed border-violet-100 bg-white/70 px-5 py-6 text-sm text-slate-500">
Noch keine entschiedenen Risikohinweise vorhanden.
</p>
<p v-else-if="riskHistory.length === 0" class="rounded-[22px] border border-dashed border-violet-100 bg-white/70 px-5 py-6 text-sm text-slate-500">
Keine Historien-Einträge passen zum aktuellen Filter.
</p>
</div>
<div class="mt-5 flex items-center justify-between gap-3 rounded-2xl border border-violet-100 bg-white/75 px-3 py-2">
<Button
variant="ghost"
size="sm"
class="gap-2 border border-violet-100"
:disabled="!hasPrevious"
@click="emit('page', page - 1)"
>
<ChevronLeft class="h-4 w-4" />
Zurück
</Button>
<span class="text-sm font-semibold text-slate-500">{{ pageLabel }}</span>
<Button
variant="ghost"
size="sm"
class="gap-2 border border-violet-100"
:disabled="!hasMore"
@click="emit('page', page + 1)"
>
Weiter
<ChevronRight class="h-4 w-4" />
</Button>
</div>
</section>
</template>