155 lines
6.3 KiB
Vue
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>
|