Files
vtuber-awards/frontend/src/views/admin/AdminCandidatesView.vue
T
AzuTear b53c7fb736 Add viewer-range categories, nomination tracking, dynamic showact form, session timeout, share URLs, and workflow-per-season
Features:
- Category viewer ranges + subcategory templates (admin group modal, tree workspace)
- Nomination enrichment via TwitchTracker API (NominationEnrichmentService,
  TwitchTrackerViewerStatsProvider) with admin tracking rules editor
- Nomination group tracker: CategoryGroupName as primary identifier,
  CategoryId stays as nullable legacy field; StreamerIdentity table
- Dynamic showact application form builder (AdminShowactFormBuilder,
  ShowactApplicationSchedule)
- Session idle timeout setting (AdminSessionTimeoutCard)
- Share URLs for X and Discord (SiteSettings, public extras)
- Workflow rules now stored per season (falls back to global SiteSettings)
- New admin routes: settings/access, settings/workflows, tracking-rules
- New admin review workspace with subcategory tabs
- AdminCategoriesView rebuilt with group/subcategory modals

Migrations (all additive):
- AddShareUrls, AddShowactDynamicForm, AddCategoryViewerRanges,
  AddSessionIdleTimeoutSettings, AddSeasonSubcategoryTemplates,
  AddNominationGroupTrackerIdentity, AddShowactApplicationSchedule,
  AddSeasonWorkflowRulesJson

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-28 23:32:21 +02:00

198 lines
7.4 KiB
Vue

<script setup lang="ts">
import { Layers3, UserPlus, Users } from '@lucide/vue'
import AdminCandidateDeleteModal from '../../components/admin/AdminCandidateDeleteModal.vue'
import AdminCandidateEditorModal from '../../components/admin/AdminCandidateEditorModal.vue'
import AdminCandidatesFiltersBar from '../../components/admin/AdminCandidatesFiltersBar.vue'
import AdminCandidatesTable from '../../components/admin/AdminCandidatesTable.vue'
import AdminPageHeader from '../../components/admin/AdminPageHeader.vue'
import AdminSeasonToolbar from '../../components/admin/AdminSeasonToolbar.vue'
import { useAdminCandidateManager } from '../../components/admin/useAdminCandidateManager'
import { watchAdminToast } from '../../composables/useAdminToast'
import Card from '../../components/ui/Card.vue'
const {
seasonDetail,
saving,
deleting,
adminMessage,
adminError,
search,
categoryFilter,
readinessFilter,
page,
categoryOptions,
categoryFilterOptions,
categoryLabelMap,
duplicateCandidateKeys,
candidateIdentitySummaries,
candidateWorkflowNotices,
duplicateCandidateCount,
acceptedCandidateCount,
clipReadyCount,
actionNeededCount,
filteredCandidates,
pagedCandidates,
totalPages,
rangeStart,
rangeEnd,
modalOpen,
form,
modalTitle,
canSave,
candidatePlatformOptions,
selectedPlatformValue,
editorIdentitySummary,
editorRuleNotices,
acceptanceStatusOptions,
clipEmbedStatusOptions,
readinessFilterOptions,
candidateToDelete,
clearFilters,
openCreate,
openEdit,
handlePlatformSelection,
saveModal,
confirmDelete,
} = useAdminCandidateManager()
watchAdminToast(adminMessage, adminError)
</script>
<template>
<div class="space-y-6">
<AdminPageHeader
eyebrow="Kandidaten"
:icon="Users"
/>
<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">
<div class="flex items-start justify-between gap-4">
<div>
<p class="text-xs font-semibold uppercase tracking-[0.18em] text-emerald-600">Angenommen</p>
<strong class="mt-3 block text-3xl text-emerald-800">{{ acceptedCandidateCount }}</strong>
</div>
<UserPlus class="h-6 w-6 text-emerald-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-sky-600">Clip bereit</p>
<strong class="mt-3 block text-3xl text-sky-800">{{ clipReadyCount }}</strong>
</div>
<Layers3 class="h-6 w-6 text-sky-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>
<Card class="p-5" :class="actionNeededCount > 0 ? 'border-rose-200 bg-rose-50/60' : ''">
<div class="flex items-start justify-between gap-4">
<div>
<p class="text-xs font-semibold uppercase tracking-[0.18em]" :class="actionNeededCount > 0 ? 'text-rose-600' : 'text-violet-500'">Handlungsbedarf</p>
<strong class="mt-3 block text-3xl" :class="actionNeededCount > 0 ? 'text-rose-700' : 'text-violet-900'">{{ actionNeededCount }}</strong>
</div>
<UserPlus class="h-6 w-6" :class="actionNeededCount > 0 ? 'text-rose-500' : 'text-violet-500'" />
</div>
</Card>
</section>
<Card class="overflow-hidden">
<AdminCandidatesFiltersBar
:search="search"
:category-filter="categoryFilter"
:readiness-filter="readinessFilter"
:category-filter-options="categoryFilterOptions"
:readiness-filter-options="readinessFilterOptions"
@update:search="search = $event"
@update:category-filter="categoryFilter = $event"
@update:readiness-filter="readinessFilter = $event"
@clear-filters="clearFilters"
@open-create="openCreate"
/>
<AdminCandidatesTable
:paged-candidates="pagedCandidates"
:total-count="seasonDetail.candidates.length"
:filtered-count="filteredCandidates.length"
:page="page"
:total-pages="totalPages"
:range-start="rangeStart"
:range-end="rangeEnd"
:category-label-map="categoryLabelMap"
:duplicate-candidate-keys="duplicateCandidateKeys"
:candidate-identity-summaries="candidateIdentitySummaries"
:candidate-workflow-notices="candidateWorkflowNotices"
:acceptance-status-options="acceptanceStatusOptions"
@edit="openEdit"
@delete="candidateToDelete = $event"
@update:page="page = $event"
@open-create="openCreate"
/>
</Card>
<AdminCandidateEditorModal
:open="modalOpen"
:title="modalTitle"
:form="form"
:category-options="categoryOptions"
:candidate-platform-options="candidatePlatformOptions"
:selected-platform-value="selectedPlatformValue"
:identity-summary="editorIdentitySummary"
:rule-notices="editorRuleNotices"
:acceptance-status-options="acceptanceStatusOptions"
:clip-embed-status-options="clipEmbedStatusOptions"
:can-save="canSave"
:saving="saving"
@close="modalOpen = false"
@save="saveModal"
@update:category-id="form.categoryId = $event"
@update:display-name="form.displayName = $event"
@update:channel-slug="form.channelSlug = $event"
@update:platform="form.platform = $event"
@update:acceptance-status="form.acceptanceStatus = $event"
@update:acceptance-note="form.acceptanceNote = $event"
@update:clip-compilation-url="form.clipCompilationUrl = $event"
@update:clip-compilation-title="form.clipCompilationTitle = $event"
@update:clip-compilation-platform="form.clipCompilationPlatform = $event"
@update:clip-embed-status="form.clipEmbedStatus = $event"
@platform-selection="handlePlatformSelection"
/>
<AdminCandidateDeleteModal
:candidate="candidateToDelete"
:deleting="deleting"
@close="candidateToDelete = null"
@confirm="confirmDelete"
/>
</div>
</template>