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>
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { BarChart3, CheckCircle2, ExternalLink, TrendingUp } from '@lucide/vue'
|
||||
import { RouterLink } from 'vue-router'
|
||||
import { RouterLink, useRoute, useRouter } from 'vue-router'
|
||||
|
||||
import AdminPageHeader from '../../components/admin/AdminPageHeader.vue'
|
||||
import AdminSeasonToolbar from '../../components/admin/AdminSeasonToolbar.vue'
|
||||
@@ -23,6 +23,8 @@ const {
|
||||
totalVotes,
|
||||
} = useAdminAnalyticsManager()
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const healthFilter = ref<string>('Alle')
|
||||
const groupFilter = ref<string>('Alle')
|
||||
|
||||
@@ -42,6 +44,43 @@ const healthStatusOptions = [
|
||||
{ key: 'Bereit', label: 'Bereit', dot: 'bg-sky-400' },
|
||||
{ key: 'Gewinner gesetzt', label: 'Gewinner', dot: 'bg-emerald-400' },
|
||||
]
|
||||
|
||||
function applyRouteFilters() {
|
||||
const rawHealth = Array.isArray(route.query.health) ? route.query.health[0] : route.query.health
|
||||
const rawGroup = Array.isArray(route.query.group) ? route.query.group[0] : route.query.group
|
||||
|
||||
healthFilter.value = healthStatusOptions.some((option) => option.key === rawHealth) ? String(rawHealth) : 'Alle'
|
||||
groupFilter.value = groupOptions.value.includes(String(rawGroup)) ? String(rawGroup) : 'Alle'
|
||||
}
|
||||
|
||||
watch(
|
||||
() => [route.query.health, route.query.group, groupOptions.value.join('|')] as const,
|
||||
() => {
|
||||
applyRouteFilters()
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
watch([healthFilter, groupFilter], ([nextHealth, nextGroup]) => {
|
||||
const currentHealth = Array.isArray(route.query.health) ? route.query.health[0] : route.query.health
|
||||
const currentGroup = Array.isArray(route.query.group) ? route.query.group[0] : route.query.group
|
||||
|
||||
const normalizedHealth = nextHealth === 'Alle' ? undefined : nextHealth
|
||||
const normalizedGroup = nextGroup === 'Alle' ? undefined : nextGroup
|
||||
|
||||
if (currentHealth === normalizedHealth && currentGroup === normalizedGroup) {
|
||||
return
|
||||
}
|
||||
|
||||
const nextQuery = { ...route.query } as Record<string, string>
|
||||
if (normalizedHealth) nextQuery.health = normalizedHealth
|
||||
else delete nextQuery.health
|
||||
|
||||
if (normalizedGroup) nextQuery.group = normalizedGroup
|
||||
else delete nextQuery.group
|
||||
|
||||
void router.replace({ query: nextQuery })
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -163,6 +202,14 @@ const healthStatusOptions = [
|
||||
<p class="mt-3 text-xs font-semibold uppercase tracking-[0.16em] text-slate-500">{{ insight.label }}</p>
|
||||
<strong class="mt-1 block text-2xl text-slate-950">{{ insight.value }}</strong>
|
||||
<p class="mt-1 text-xs leading-5 text-slate-500">{{ insight.note }}</p>
|
||||
<RouterLink
|
||||
v-if="'to' in insight && insight.to"
|
||||
:to="insight.to"
|
||||
class="mt-3 inline-flex items-center gap-1 text-xs font-semibold text-violet-700 hover:text-violet-800 hover:underline"
|
||||
>
|
||||
Öffnen
|
||||
<ExternalLink class="h-3.5 w-3.5" />
|
||||
</RouterLink>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
@@ -261,7 +308,7 @@ const healthStatusOptions = [
|
||||
<RouterLink
|
||||
v-for="category in filteredHealth"
|
||||
:key="category.id"
|
||||
to="/admin/categories"
|
||||
:to="`/admin/categories?group=${encodeURIComponent(category.groupName || 'Ohne Gruppe')}`"
|
||||
class="grid items-center gap-3 px-5 py-3.5 transition hover:bg-violet-50/50 sm:grid-cols-[minmax(0,1fr)_80px_80px_100px_140px]"
|
||||
>
|
||||
<!-- Name + group -->
|
||||
|
||||
Reference in New Issue
Block a user