Refactor app architecture and clean local artifacts
This commit is contained in:
@@ -1,126 +1,102 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
import { FileClock, Search, UserCog } from '@lucide/vue'
|
||||
import { UserCog } from '@lucide/vue'
|
||||
|
||||
import AdminAuditDetailDrawer from '../../components/admin/AdminAuditDetailDrawer.vue'
|
||||
import AdminAuditFocusPanel from '../../components/admin/AdminAuditFocusPanel.vue'
|
||||
import AdminAuditLogList from '../../components/admin/AdminAuditLogList.vue'
|
||||
import AdminAuditOverviewBar from '../../components/admin/AdminAuditOverviewBar.vue'
|
||||
import AdminPageHeader from '../../components/admin/AdminPageHeader.vue'
|
||||
import Card from '../../components/ui/Card.vue'
|
||||
import { useAwardsStore } from '../../stores/awards'
|
||||
import { useAdminAuditManager } from '../../components/admin/useAdminAuditManager'
|
||||
|
||||
const store = useAwardsStore()
|
||||
const query = ref('')
|
||||
const auditEntries = computed(() => store.admin.auditEntries)
|
||||
const filteredAuditEntries = computed(() => {
|
||||
const search = query.value.trim().toLowerCase()
|
||||
if (!search) return auditEntries.value
|
||||
return auditEntries.value.filter((entry) =>
|
||||
[entry.adminTwitchUserId, entry.actionType, entry.entityType, entry.entityId, entry.summary]
|
||||
.join(' ')
|
||||
.toLowerCase()
|
||||
.includes(search),
|
||||
)
|
||||
})
|
||||
const adminCounts = computed(() => {
|
||||
const counts = new Map<string, number>()
|
||||
for (const entry of auditEntries.value) counts.set(entry.adminTwitchUserId, (counts.get(entry.adminTwitchUserId) ?? 0) + 1)
|
||||
return [...counts.entries()].map(([admin, count]) => ({ admin, count }))
|
||||
})
|
||||
const logStats = computed(() => [
|
||||
{ label: 'Audit-Einträge', value: auditEntries.value.length },
|
||||
{ label: 'Admins aktiv', value: adminCounts.value.length },
|
||||
{ label: 'Sichtbar', value: filteredAuditEntries.value.length },
|
||||
])
|
||||
const actionCounts = computed(() => {
|
||||
const counts = new Map<string, number>()
|
||||
for (const entry of auditEntries.value) counts.set(entry.actionType, (counts.get(entry.actionType) ?? 0) + 1)
|
||||
return [...counts.entries()]
|
||||
.map(([action, count]) => ({ action, count }))
|
||||
.sort((a, b) => b.count - a.count || a.action.localeCompare(b.action))
|
||||
})
|
||||
const {
|
||||
query,
|
||||
selectedAdmin,
|
||||
selectedAction,
|
||||
entityFilter,
|
||||
fromDate,
|
||||
toDate,
|
||||
loadingAudit,
|
||||
loadingMore,
|
||||
auditError,
|
||||
exportMessage,
|
||||
selectedEntry,
|
||||
auditRows,
|
||||
adminCounts,
|
||||
actionCounts,
|
||||
entityCounts,
|
||||
logStats,
|
||||
focusCards,
|
||||
activeFilterCount,
|
||||
lastLoadedLabel,
|
||||
emptyStateText,
|
||||
pageSummaryLabel,
|
||||
hasMore,
|
||||
filterPresets,
|
||||
appliedPresetKey,
|
||||
entityOptions,
|
||||
loadAuditEntries,
|
||||
loadNextPage,
|
||||
exportAuditCsv,
|
||||
clearFilters,
|
||||
applyPreset,
|
||||
openAuditEntry,
|
||||
closeAuditEntry,
|
||||
} = useAdminAuditManager()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="space-y-6">
|
||||
<AdminPageHeader
|
||||
eyebrow="Team-Audit"
|
||||
title="Admin-Aktionen nachvollziehen"
|
||||
description="Diese Seite ist die Log-Quelle für Team-Handlungen: wer hat Kategorien, Kandidaten, Clips, Reviews oder Risk-Flags bearbeitet. Risikoentscheidungen selbst bleiben im Risiko-Bereich."
|
||||
eyebrow="Audit-Log"
|
||||
description="Nachvollziehbare Admin-Aktionen mit Suche, Metadaten, Quick-Filtern und CSV-Export."
|
||||
:icon="UserCog"
|
||||
/>
|
||||
|
||||
<Card class="p-5">
|
||||
<div class="grid gap-3 lg:grid-cols-[minmax(0,1fr)_420px]">
|
||||
<label class="relative block">
|
||||
<Search class="pointer-events-none absolute left-4 top-1/2 h-4 w-4 -translate-y-1/2 text-violet-400" />
|
||||
<input v-model="query" class="h-12 w-full rounded-2xl border border-violet-200 bg-white pl-11 pr-4 text-sm outline-none transition focus:border-violet-400 focus:ring-4 focus:ring-violet-100" placeholder="Nach Admin, Aktion, User, IP oder Objekt suchen" />
|
||||
</label>
|
||||
<div class="grid grid-cols-3 gap-2">
|
||||
<div v-for="stat in logStats" :key="stat.label" class="rounded-2xl border border-violet-50 bg-violet-50/50 px-3 py-2">
|
||||
<p class="truncate text-[10px] font-semibold uppercase tracking-[0.12em] text-slate-500">{{ stat.label }}</p>
|
||||
<strong class="text-lg text-violet-800">{{ stat.value }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
<AdminAuditOverviewBar
|
||||
v-model:query="query"
|
||||
v-model:entity-filter="entityFilter"
|
||||
v-model:from-date="fromDate"
|
||||
v-model:to-date="toDate"
|
||||
:stats="logStats"
|
||||
:entity-options="entityOptions"
|
||||
:filter-presets="filterPresets"
|
||||
:applied-preset-key="appliedPresetKey"
|
||||
:export-message="exportMessage"
|
||||
:last-loaded-label="lastLoadedLabel"
|
||||
:loading="loadingAudit"
|
||||
:has-rows="auditRows.length > 0"
|
||||
:active-filter-count="activeFilterCount"
|
||||
@refresh="loadAuditEntries()"
|
||||
@export="exportAuditCsv"
|
||||
@clear="clearFilters"
|
||||
@apply-preset="applyPreset"
|
||||
/>
|
||||
|
||||
<section class="grid gap-6 xl:grid-cols-[0.86fr_1.14fr]">
|
||||
<Card class="p-6">
|
||||
<p class="text-xs font-semibold uppercase tracking-[0.24em] text-violet-500">Admins</p>
|
||||
<h2 class="mt-2 font-[Cormorant_Garamond] text-4xl text-violet-800">Aktivität</h2>
|
||||
<div class="mt-5 space-y-3">
|
||||
<div v-for="item in adminCounts" :key="item.admin" class="flex items-center justify-between rounded-2xl border border-violet-100 bg-white/90 px-4 py-3">
|
||||
<span class="font-semibold text-slate-900">{{ item.admin }}</span>
|
||||
<span class="rounded-full bg-violet-50 px-3 py-1 text-sm font-semibold text-violet-700">{{ item.count }}</span>
|
||||
</div>
|
||||
<p v-if="adminCounts.length === 0" class="rounded-2xl border border-dashed border-violet-100 px-4 py-8 text-center text-sm text-slate-500">
|
||||
Noch keine Admin-Aktivität vorhanden.
|
||||
</p>
|
||||
</div>
|
||||
</Card>
|
||||
<p v-if="auditError" class="rounded-2xl border border-rose-200 bg-rose-50 px-4 py-3 text-sm font-semibold text-rose-700">{{ auditError }}</p>
|
||||
|
||||
<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">Audit Log</p>
|
||||
<h2 class="mt-2 font-[Cormorant_Garamond] text-4xl text-violet-800">Letzte Aktionen</h2>
|
||||
</div>
|
||||
<div class="max-h-[520px] divide-y divide-violet-50 overflow-y-auto">
|
||||
<div v-for="entry in filteredAuditEntries" :key="entry.id" class="px-5 py-4">
|
||||
<div class="flex flex-wrap items-start justify-between gap-3">
|
||||
<div>
|
||||
<p class="font-semibold text-slate-900">{{ entry.summary }}</p>
|
||||
<p class="mt-1 text-sm text-slate-500">{{ entry.adminTwitchUserId }} · {{ entry.actionType }} · {{ entry.entityType }} {{ entry.entityId }}</p>
|
||||
</div>
|
||||
<span class="text-sm text-slate-500">{{ new Date(entry.createdAt).toLocaleString('de-DE') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<p v-if="filteredAuditEntries.length === 0" class="px-5 py-10 text-center text-sm text-slate-500">Keine Log-Einträge gefunden.</p>
|
||||
</div>
|
||||
</Card>
|
||||
<section class="grid gap-6 2xl:grid-cols-[360px_minmax(0,1fr)]">
|
||||
<AdminAuditFocusPanel
|
||||
v-model:selected-admin="selectedAdmin"
|
||||
v-model:selected-action="selectedAction"
|
||||
v-model:selected-entity="entityFilter"
|
||||
:focus-cards="focusCards"
|
||||
:admin-counts="adminCounts"
|
||||
:action-counts="actionCounts"
|
||||
:entity-counts="entityCounts"
|
||||
/>
|
||||
|
||||
<AdminAuditLogList
|
||||
:entries="auditRows"
|
||||
:loading="loadingAudit"
|
||||
:loading-more="loadingMore"
|
||||
:has-more="hasMore"
|
||||
:page-summary-label="pageSummaryLabel"
|
||||
:empty-text="emptyStateText"
|
||||
@open-detail="openAuditEntry"
|
||||
@load-more="loadNextPage"
|
||||
/>
|
||||
</section>
|
||||
|
||||
<Card class="overflow-hidden">
|
||||
<div class="border-b border-violet-100 p-5">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="grid h-10 w-10 place-items-center rounded-2xl bg-violet-100 text-violet-700">
|
||||
<FileClock class="h-5 w-5" />
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs font-semibold uppercase tracking-[0.24em] text-violet-500">Aktionstypen</p>
|
||||
<h2 class="font-[Cormorant_Garamond] text-3xl text-violet-800">Was wurde bearbeitet?</h2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="divide-y divide-violet-50">
|
||||
<div v-for="item in actionCounts" :key="item.action" class="grid gap-3 px-5 py-4 lg:grid-cols-[minmax(0,1fr)_120px] lg:items-center">
|
||||
<div>
|
||||
<p class="font-semibold text-slate-900">{{ item.action }}</p>
|
||||
<p class="mt-1 text-sm text-slate-500">Audit-Kategorie für Team-Aktionen</p>
|
||||
</div>
|
||||
<span class="rounded-full border border-violet-100 bg-violet-50 px-3 py-1 text-center text-sm font-semibold text-violet-700">{{ item.count }}</span>
|
||||
</div>
|
||||
<p v-if="actionCounts.length === 0" class="px-5 py-10 text-center text-sm text-slate-500">
|
||||
Noch keine Aktionstypen vorhanden.
|
||||
</p>
|
||||
</div>
|
||||
</Card>
|
||||
<AdminAuditDetailDrawer :entry="selectedEntry" @close="closeAuditEntry" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user