Refine admin panel workflows

This commit is contained in:
AzuTear
2026-06-18 14:29:40 +02:00
parent f4512eba2e
commit d015fcbe6f
14 changed files with 481 additions and 598 deletions
@@ -1,7 +1,7 @@
<script setup lang="ts">
import { computed, reactive, ref, watch } from 'vue'
import Select from 'primevue/select'
import { ChevronLeft, ChevronRight, Pencil, Search, Trash2, TriangleAlert, UserPlus, Users, X } from '@lucide/vue'
import { ChevronLeft, ChevronRight, Layers3, Pencil, Search, Trash2, TriangleAlert, UserPlus, Users, X } from '@lucide/vue'
import AdminPageHeader from '../../components/admin/AdminPageHeader.vue'
import AdminSeasonToolbar from '../../components/admin/AdminSeasonToolbar.vue'
@@ -33,6 +33,23 @@ const categoryFilterOptions = computed(() => [{ label: 'Alle Kategorien', value:
const categoryLabelMap = computed(() =>
Object.fromEntries(seasonDetail.value.categories.map((c) => [c.id, `${c.groupName} · ${c.name}`])),
)
const duplicateCandidateKeys = computed(() => {
const counts = new Map<string, number>()
for (const candidate of seasonDetail.value.candidates) {
const categoryKey = `${candidate.categoryId}`
const nameKey = `${categoryKey}:name:${candidate.displayName.trim().toLowerCase()}`
const slugKey = `${categoryKey}:slug:${candidate.channelSlug.trim().toLowerCase()}`
counts.set(nameKey, (counts.get(nameKey) ?? 0) + 1)
if (candidate.channelSlug.trim()) counts.set(slugKey, (counts.get(slugKey) ?? 0) + 1)
}
return counts
})
const duplicateCandidateCount = computed(() =>
seasonDetail.value.candidates.filter((candidate) =>
(duplicateCandidateKeys.value.get(`${candidate.categoryId}:name:${candidate.displayName.trim().toLowerCase()}`) ?? 0) > 1 ||
(duplicateCandidateKeys.value.get(`${candidate.categoryId}:slug:${candidate.channelSlug.trim().toLowerCase()}`) ?? 0) > 1,
).length,
)
const filteredCandidates = computed(() => {
const query = search.value.trim().toLowerCase()
@@ -151,6 +168,36 @@ async function confirmDelete() {
<AdminSeasonToolbar />
<section class="grid gap-4 md:grid-cols-3">
<Card 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">Kandidaten</p>
<strong class="mt-3 block text-3xl text-violet-900">{{ seasonDetail.candidates.length }}</strong>
</div>
<Users class="h-6 w-6 text-violet-500" />
</div>
</Card>
<Card 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">Kategorien</p>
<strong class="mt-3 block text-3xl text-violet-900">{{ seasonDetail.categories.length }}</strong>
</div>
<UserPlus class="h-6 w-6 text-violet-500" />
</div>
</Card>
<Card class="p-5" :class="duplicateCandidateCount > 0 ? 'border-amber-200 bg-amber-50/60' : ''">
<div class="flex items-start justify-between gap-4">
<div>
<p class="text-xs font-semibold uppercase tracking-[0.18em]" :class="duplicateCandidateCount > 0 ? 'text-amber-600' : 'text-violet-500'">Mögliche Duplikate</p>
<strong class="mt-3 block text-3xl" :class="duplicateCandidateCount > 0 ? 'text-amber-700' : 'text-violet-900'">{{ duplicateCandidateCount }}</strong>
</div>
<Layers3 class="h-6 w-6" :class="duplicateCandidateCount > 0 ? 'text-amber-500' : 'text-violet-500'" />
</div>
</Card>
</section>
<Card class="overflow-hidden">
<!-- Toolbar -->
<div class="flex flex-col gap-4 border-b border-violet-100 p-5 lg:flex-row lg:items-center">
@@ -203,6 +250,12 @@ async function confirmDelete() {
<div class="min-w-0">
<p class="truncate font-semibold text-slate-800">{{ candidate.displayName }}</p>
<p class="truncate text-xs text-slate-500">{{ candidate.channelSlug }}</p>
<p
v-if="(duplicateCandidateKeys.get(`${candidate.categoryId}:name:${candidate.displayName.trim().toLowerCase()}`) ?? 0) > 1 || (duplicateCandidateKeys.get(`${candidate.categoryId}:slug:${candidate.channelSlug.trim().toLowerCase()}`) ?? 0) > 1"
class="mt-1 text-xs font-semibold text-amber-700"
>
Mögliches Duplikat in dieser Kategorie
</p>
</div>
</div>
<div class="min-w-0">