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:
@@ -63,8 +63,13 @@ export const useAuthStore = defineStore('auth', {
|
||||
isTeamSession: (state) => Boolean(state.session?.teamLogin),
|
||||
},
|
||||
actions: {
|
||||
clearSession() {
|
||||
this.session = null
|
||||
writeStoredToken(null)
|
||||
},
|
||||
async hydrate() {
|
||||
if (!readStoredToken()) {
|
||||
this.session = null
|
||||
this.hydrated = true
|
||||
return
|
||||
}
|
||||
@@ -72,9 +77,8 @@ export const useAuthStore = defineStore('auth', {
|
||||
try {
|
||||
this.session = await api.getSession()
|
||||
} catch (error) {
|
||||
this.session = null
|
||||
if (error instanceof ApiRequestError && (error.status === 401 || error.status === 403)) {
|
||||
writeStoredToken(null)
|
||||
this.clearSession()
|
||||
}
|
||||
} finally {
|
||||
this.hydrated = true
|
||||
@@ -162,8 +166,7 @@ export const useAuthStore = defineStore('auth', {
|
||||
try {
|
||||
await api.logout()
|
||||
} finally {
|
||||
this.session = null
|
||||
writeStoredToken(null)
|
||||
this.clearSession()
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
@@ -171,8 +174,7 @@ export const useAuthStore = defineStore('auth', {
|
||||
this.loading = true
|
||||
try {
|
||||
const result = await api.deleteMyData()
|
||||
this.session = null
|
||||
writeStoredToken(null)
|
||||
this.clearSession()
|
||||
return result
|
||||
} finally {
|
||||
this.loading = false
|
||||
|
||||
+111
-12
@@ -9,6 +9,7 @@ import {
|
||||
createEmptyAdminSeasonDetail,
|
||||
createEmptyAdminOptionalFeatureSettings,
|
||||
createEmptyAdminSiteSettings,
|
||||
createEmptyAdminTrackingRulesResponse,
|
||||
createEmptyAdminWorkflowRulesResponse,
|
||||
createEmptyArchive,
|
||||
createEmptyDatabaseHealth,
|
||||
@@ -23,20 +24,27 @@ import type {
|
||||
CreateNominationPayload,
|
||||
CreateShowactApplicationPayload,
|
||||
CreateVotePayload,
|
||||
ReopenRejectedNominationPayload,
|
||||
RejectNominationPayload,
|
||||
ResolveRiskFlagPayload,
|
||||
SetAwardResultPayload,
|
||||
AdminAuditQueryOptions,
|
||||
AdminRiskQueryOptions,
|
||||
UpdateSeasonSubcategoryTemplatesPayload,
|
||||
UpdateSeasonPayload,
|
||||
UpdateClipStatusPayload,
|
||||
UpdateNominationLinkBlacklistPayload,
|
||||
UpdateOptionalFeatureSettingsPayload,
|
||||
UpdateNominationTrackingReviewPayload,
|
||||
UpdateShowactStatusPayload,
|
||||
UpdateSiteSettingsPayload,
|
||||
UpdateTrackingReviewNotesPayload,
|
||||
UpdateTrackingRulesPayload,
|
||||
UpdateTrackingSourcePayload,
|
||||
UpdateWorkflowRulesPayload,
|
||||
UpsertCandidatePayload,
|
||||
UpsertCategoryPayload,
|
||||
UpsertCategoryGroupPayload,
|
||||
UpsertSponsorPayload,
|
||||
} from '../types/awards'
|
||||
|
||||
@@ -57,7 +65,9 @@ export const useAwardsStore = defineStore('awards', {
|
||||
this.categories = await api.getSeasonCategories(this.overview.year)
|
||||
await this.loadPublicSponsors(this.overview.year)
|
||||
const overviewArchiveYears = this.overview.archiveYears ?? []
|
||||
const initialArchiveYear = overviewArchiveYears[0]?.year
|
||||
const initialArchiveYear = canExposeCurrentSeasonWinners(this.overview.currentPhase)
|
||||
? this.overview.year
|
||||
: overviewArchiveYears[0]?.year
|
||||
?? this.overview.winnersPreview[0]?.year
|
||||
?? this.overview.year - 1
|
||||
this.archive = await api.getWinnerArchive(initialArchiveYear)
|
||||
@@ -92,12 +102,11 @@ export const useAwardsStore = defineStore('awards', {
|
||||
},
|
||||
async loadAdmin() {
|
||||
try {
|
||||
const [admin, adminSeasons, adminSiteSettings, adminOptionalFeatureSettings, adminWorkflowRules, databaseHealth] = await Promise.all([
|
||||
const [admin, adminSeasons, adminSiteSettings, adminOptionalFeatureSettings, databaseHealth] = await Promise.all([
|
||||
api.getAdminDashboard(),
|
||||
api.getAdminSeasons(),
|
||||
api.getAdminSiteSettings(),
|
||||
api.getAdminOptionalFeatureSettings(),
|
||||
api.getAdminWorkflowRules(),
|
||||
api.getDatabaseHealth(),
|
||||
])
|
||||
|
||||
@@ -105,7 +114,6 @@ export const useAwardsStore = defineStore('awards', {
|
||||
this.adminSeasons = adminSeasons
|
||||
this.adminSiteSettings = adminSiteSettings
|
||||
this.adminOptionalFeatureSettings = adminOptionalFeatureSettings
|
||||
this.adminWorkflowRules = adminWorkflowRules
|
||||
this.databaseHealth = databaseHealth
|
||||
|
||||
if (!this.adminSelectedSeasonId || !this.adminSeasons.some((season) => season.id === this.adminSelectedSeasonId)) {
|
||||
@@ -113,26 +121,32 @@ export const useAwardsStore = defineStore('awards', {
|
||||
}
|
||||
|
||||
if (this.adminSelectedSeasonId) {
|
||||
this.adminSeasonDetail = normalizeSeasonDetail(await api.getAdminSeasonDetail(this.adminSelectedSeasonId))
|
||||
const [sponsors, showacts] = await Promise.all([
|
||||
const [seasonDetail, sponsors, showacts, adminWorkflowRules] = await Promise.all([
|
||||
api.getAdminSeasonDetail(this.adminSelectedSeasonId),
|
||||
api.getAdminSponsors(this.adminSelectedSeasonId),
|
||||
api.getAdminShowactApplications(this.adminSelectedSeasonId),
|
||||
api.getAdminWorkflowRules(this.adminSelectedSeasonId),
|
||||
])
|
||||
this.adminSeasonDetail = normalizeSeasonDetail(seasonDetail)
|
||||
this.adminSponsors = sponsors
|
||||
this.adminShowactApplications = showacts
|
||||
this.adminWorkflowRules = adminWorkflowRules
|
||||
} else {
|
||||
this.adminSeasonDetail = createEmptyAdminSeasonDetail()
|
||||
this.adminSponsors = []
|
||||
this.adminShowactApplications = []
|
||||
this.adminWorkflowRules = createEmptyAdminWorkflowRulesResponse()
|
||||
}
|
||||
this.apiMode = 'api'
|
||||
} catch {
|
||||
this.apiMode = 'fallback'
|
||||
this.admin = createEmptyAdminDashboard()
|
||||
this.adminSeasons = []
|
||||
this.adminSeasonDetail = createEmptyAdminSeasonDetail()
|
||||
this.adminSiteSettings = createEmptyAdminSiteSettings()
|
||||
this.adminOptionalFeatureSettings = createEmptyAdminOptionalFeatureSettings()
|
||||
this.adminWorkflowRules = createEmptyAdminWorkflowRulesResponse()
|
||||
this.adminTrackingRules = createEmptyAdminTrackingRulesResponse()
|
||||
this.adminRiskHistory = []
|
||||
this.adminRiskFlagsPage = createEmptyAdminRiskFlagsResponse()
|
||||
this.adminRiskHistoryPage = createEmptyAdminRiskFlagsResponse()
|
||||
@@ -155,30 +169,43 @@ export const useAwardsStore = defineStore('awards', {
|
||||
this.databaseHealth = databaseHealth
|
||||
this.apiMode = 'api'
|
||||
} catch {
|
||||
this.apiMode = 'fallback'
|
||||
this.adminSiteSettings = createEmptyAdminSiteSettings()
|
||||
this.adminOptionalFeatureSettings = createEmptyAdminOptionalFeatureSettings()
|
||||
this.databaseHealth = createEmptyDatabaseHealth()
|
||||
}
|
||||
},
|
||||
async loadDatabaseHealth() {
|
||||
this.databaseHealth = await api.getDatabaseHealth()
|
||||
return this.databaseHealth
|
||||
try {
|
||||
this.databaseHealth = await api.getDatabaseHealth()
|
||||
this.apiMode = 'api'
|
||||
return this.databaseHealth
|
||||
} catch (error) {
|
||||
this.databaseHealth = createEmptyDatabaseHealth()
|
||||
this.apiMode = 'fallback'
|
||||
throw error
|
||||
}
|
||||
},
|
||||
async loadAdminSeasonDetail(seasonId: number) {
|
||||
try {
|
||||
this.adminSelectedSeasonId = seasonId
|
||||
this.adminSeasonDetail = normalizeSeasonDetail(await api.getAdminSeasonDetail(seasonId))
|
||||
const [sponsors, showacts] = await Promise.all([
|
||||
const [seasonDetail, sponsors, showacts, adminWorkflowRules] = await Promise.all([
|
||||
api.getAdminSeasonDetail(seasonId),
|
||||
api.getAdminSponsors(seasonId),
|
||||
api.getAdminShowactApplications(seasonId),
|
||||
api.getAdminWorkflowRules(seasonId),
|
||||
])
|
||||
this.adminSeasonDetail = normalizeSeasonDetail(seasonDetail)
|
||||
this.adminSponsors = sponsors
|
||||
this.adminShowactApplications = showacts
|
||||
this.adminWorkflowRules = adminWorkflowRules
|
||||
this.apiMode = 'api'
|
||||
} catch {
|
||||
this.apiMode = 'fallback'
|
||||
this.adminSeasonDetail = createEmptyAdminSeasonDetail()
|
||||
this.adminSponsors = []
|
||||
this.adminShowactApplications = []
|
||||
this.adminWorkflowRules = createEmptyAdminWorkflowRulesResponse()
|
||||
}
|
||||
},
|
||||
async initializeAdminWorkspace() {
|
||||
@@ -325,6 +352,26 @@ export const useAwardsStore = defineStore('awards', {
|
||||
await this.refreshAdminSeasonWorkspace(seasonId, { reloadHome: true })
|
||||
return result
|
||||
},
|
||||
async updateAdminSeasonSubcategoryTemplates(seasonId: number, payload: UpdateSeasonSubcategoryTemplatesPayload) {
|
||||
const result = await api.updateAdminSeasonSubcategoryTemplates(seasonId, payload)
|
||||
await this.refreshAdminSeasonWorkspace(seasonId, { reloadHome: true })
|
||||
return result
|
||||
},
|
||||
async createAdminCategoryGroup(seasonId: number, payload: UpsertCategoryGroupPayload) {
|
||||
const result = await api.createAdminCategoryGroup(seasonId, payload)
|
||||
await this.refreshAdminSeasonWorkspace(seasonId, { reloadHome: true })
|
||||
return result
|
||||
},
|
||||
async updateAdminCategoryGroup(seasonId: number, groupName: string, payload: UpsertCategoryGroupPayload) {
|
||||
const result = await api.updateAdminCategoryGroup(seasonId, groupName, payload)
|
||||
await this.refreshAdminSeasonWorkspace(seasonId, { reloadHome: true })
|
||||
return result
|
||||
},
|
||||
async deleteAdminCategoryGroup(seasonId: number, groupName: string) {
|
||||
const result = await api.deleteAdminCategoryGroup(seasonId, groupName)
|
||||
await this.refreshAdminSeasonWorkspace(seasonId, { reloadHome: true })
|
||||
return result
|
||||
},
|
||||
async createAdminCandidate(seasonId: number, payload: UpsertCandidatePayload) {
|
||||
const result = await api.createAdminCandidate(seasonId, payload)
|
||||
await this.refreshAdminSeasonWorkspace(seasonId, { reloadHome: true })
|
||||
@@ -379,6 +426,11 @@ export const useAwardsStore = defineStore('awards', {
|
||||
await this.refreshAdminSeasonWorkspace(seasonId, { reloadAdmin: true })
|
||||
return result
|
||||
},
|
||||
async reopenRejectedAdminNomination(nominationId: number, seasonId: number, payload: ReopenRejectedNominationPayload = {}) {
|
||||
const result = await api.reopenRejectedAdminNomination(nominationId, payload)
|
||||
await this.refreshAdminSeasonWorkspace(seasonId, { reloadAdmin: true })
|
||||
return result
|
||||
},
|
||||
async bulkRejectAdminNominations(nominationIds: number[], seasonId: number, reviewNote?: string) {
|
||||
await Promise.allSettled(nominationIds.map((id) => api.rejectAdminNomination(id, { reviewNote })))
|
||||
await this.refreshAdminSeasonWorkspace(seasonId, { reloadAdmin: true })
|
||||
@@ -438,13 +490,52 @@ export const useAwardsStore = defineStore('awards', {
|
||||
return this.adminOptionalFeatureSettings
|
||||
},
|
||||
async loadAdminWorkflowRules() {
|
||||
this.adminWorkflowRules = await api.getAdminWorkflowRules()
|
||||
if (!this.adminSelectedSeasonId) {
|
||||
this.adminWorkflowRules = createEmptyAdminWorkflowRulesResponse()
|
||||
return this.adminWorkflowRules
|
||||
}
|
||||
|
||||
this.adminWorkflowRules = await api.getAdminWorkflowRules(this.adminSelectedSeasonId)
|
||||
return this.adminWorkflowRules
|
||||
},
|
||||
async updateAdminWorkflowRules(payload: UpdateWorkflowRulesPayload) {
|
||||
this.adminWorkflowRules = await api.updateAdminWorkflowRules(payload)
|
||||
if (!this.adminSelectedSeasonId) {
|
||||
throw new Error('Kein Award-Jahr fuer Workflow-Regeln ausgewaehlt.')
|
||||
}
|
||||
|
||||
this.adminWorkflowRules = await api.updateAdminWorkflowRules(this.adminSelectedSeasonId, payload)
|
||||
return this.adminWorkflowRules
|
||||
},
|
||||
async loadAdminTrackingRules() {
|
||||
this.adminTrackingRules = await api.getAdminTrackingRules()
|
||||
return this.adminTrackingRules
|
||||
},
|
||||
async updateAdminTrackingRules(payload: UpdateTrackingRulesPayload) {
|
||||
this.adminTrackingRules = await api.updateAdminTrackingRules(payload)
|
||||
if (this.adminSelectedSeasonId) {
|
||||
await this.loadAdminSeasonDetail(this.adminSelectedSeasonId)
|
||||
}
|
||||
return this.adminTrackingRules
|
||||
},
|
||||
async updateAdminTrackingSource(payload: UpdateTrackingSourcePayload) {
|
||||
this.adminTrackingRules = await api.updateAdminTrackingSource(payload)
|
||||
if (this.adminSelectedSeasonId) {
|
||||
await this.loadAdminSeasonDetail(this.adminSelectedSeasonId)
|
||||
}
|
||||
return this.adminTrackingRules
|
||||
},
|
||||
async updateAdminTrackingReviewNotes(payload: UpdateTrackingReviewNotesPayload) {
|
||||
this.adminTrackingRules = await api.updateAdminTrackingReviewNotes(payload)
|
||||
if (this.adminSelectedSeasonId) {
|
||||
await this.loadAdminSeasonDetail(this.adminSelectedSeasonId)
|
||||
}
|
||||
return this.adminTrackingRules
|
||||
},
|
||||
async updateNominationTrackingReview(nominationId: number, seasonId: number, payload: UpdateNominationTrackingReviewPayload) {
|
||||
const result = await api.updateNominationTrackingReview(nominationId, payload)
|
||||
await this.refreshAdminSeasonWorkspace(seasonId, { reloadAdmin: true })
|
||||
return result
|
||||
},
|
||||
async updateAdminSiteSettings(payload: UpdateSiteSettingsPayload) {
|
||||
const result = await api.updateAdminSiteSettings(payload)
|
||||
this.adminSiteSettings = await api.getAdminSiteSettings()
|
||||
@@ -454,4 +545,12 @@ export const useAwardsStore = defineStore('awards', {
|
||||
},
|
||||
})
|
||||
|
||||
function canExposeCurrentSeasonWinners(currentPhase: string) {
|
||||
const normalized = currentPhase.trim().toLowerCase()
|
||||
return normalized.includes('abgeschlossen')
|
||||
|| normalized.includes('archiv')
|
||||
|| normalized.includes('complete')
|
||||
|| normalized.includes('ended')
|
||||
}
|
||||
|
||||
export type AwardsStore = ReturnType<typeof useAwardsStore>
|
||||
|
||||
@@ -9,6 +9,7 @@ import type {
|
||||
AdminShowactApplicationItem,
|
||||
AdminSiteSettingsResponse,
|
||||
AdminSponsorItem,
|
||||
AdminTrackingRulesResponse,
|
||||
AdminWorkflowRulesResponse,
|
||||
DatabaseHealthResponse,
|
||||
OverviewResponse,
|
||||
@@ -39,6 +40,8 @@ export function createEmptyOverview(): OverviewResponse {
|
||||
hostDisplayName: '',
|
||||
hostTagline: '',
|
||||
newsletterUrl: '',
|
||||
shareXUrl: '',
|
||||
shareDiscordUrl: '',
|
||||
privacyEmail: '',
|
||||
privacyPolicyContent: '',
|
||||
socialLinks: [],
|
||||
@@ -50,8 +53,11 @@ export function createEmptyOverview(): OverviewResponse {
|
||||
clipAdminMenuVisible: true,
|
||||
clipSubmissionDisabledMessage: 'Clip-Einreichungen sind aktuell geschlossen.',
|
||||
showactApplicationsEnabled: false,
|
||||
showactApplicationStartsAt: null,
|
||||
showactApplicationEndsAt: null,
|
||||
showactApplicationDisabledMessage: 'Showact-Bewerbungen sind aktuell geschlossen.',
|
||||
sponsorsVisible: true,
|
||||
showactFormSchemaJson: '[]',
|
||||
},
|
||||
faq: [],
|
||||
}
|
||||
@@ -108,6 +114,22 @@ export function createEmptyAdminWorkflowRulesResponse(): AdminWorkflowRulesRespo
|
||||
}
|
||||
}
|
||||
|
||||
export function createEmptyAdminTrackingRulesResponse(): AdminTrackingRulesResponse {
|
||||
return {
|
||||
source: {
|
||||
providerKey: 'twitchtracker',
|
||||
providerLabel: 'TwitchTracker Basic API',
|
||||
baseUrl: 'https://twitchtracker.com/api',
|
||||
notesSummary: '',
|
||||
showManualReviewNotesInReview: true,
|
||||
},
|
||||
importantMetrics: [],
|
||||
optionalMetrics: [],
|
||||
flags: [],
|
||||
manualReviewNotes: '',
|
||||
}
|
||||
}
|
||||
|
||||
export function createEmptyAdminSeasonDetail(): AdminSeasonDetailResponse {
|
||||
return {
|
||||
id: 0,
|
||||
@@ -125,10 +147,14 @@ export function createEmptyAdminSeasonDetail(): AdminSeasonDetailResponse {
|
||||
reviewEndsAt: '',
|
||||
showDate: '',
|
||||
showStartsAt: '20:00:00',
|
||||
subcategoryTemplates: [],
|
||||
categories: [],
|
||||
candidates: [],
|
||||
pendingNominations: [],
|
||||
pendingNominationGroups: [],
|
||||
reviewedNominations: [],
|
||||
trackingReviewNotes: '',
|
||||
showTrackingReviewNotes: true,
|
||||
results: [],
|
||||
clipSubmissions: [],
|
||||
}
|
||||
@@ -139,6 +165,8 @@ export function createEmptyAdminSiteSettings(): AdminSiteSettingsResponse {
|
||||
hostDisplayName: '',
|
||||
hostTagline: '',
|
||||
newsletterUrl: '',
|
||||
shareXUrl: '',
|
||||
shareDiscordUrl: '',
|
||||
privacyEmail: '',
|
||||
privacyPolicyContent: '',
|
||||
privacyPolicyUpdatedBy: null,
|
||||
@@ -153,6 +181,7 @@ export function createEmptyAdminSiteSettings(): AdminSiteSettingsResponse {
|
||||
showactsContent: '',
|
||||
socialLinks: [],
|
||||
faq: [],
|
||||
showactFormSchemaJson: '[]',
|
||||
}
|
||||
}
|
||||
|
||||
@@ -163,6 +192,9 @@ export function createEmptyAdminOptionalFeatureSettings(): AdminOptionalFeatureS
|
||||
clipAdminMenuVisible: true,
|
||||
clipSubmissionDisabledMessage: 'Clip-Einreichungen sind aktuell geschlossen.',
|
||||
showactApplicationsEnabled: false,
|
||||
showactApplicationStartsAt: null,
|
||||
showactApplicationEndsAt: null,
|
||||
showactApplicationsOpenNow: false,
|
||||
showactApplicationDisabledMessage: 'Showact-Bewerbungen sind aktuell geschlossen.',
|
||||
sponsorsVisible: true,
|
||||
}
|
||||
@@ -191,6 +223,7 @@ export function createAwardsState() {
|
||||
adminSiteSettings: createEmptyAdminSiteSettings(),
|
||||
adminOptionalFeatureSettings: createEmptyAdminOptionalFeatureSettings(),
|
||||
adminWorkflowRules: createEmptyAdminWorkflowRulesResponse(),
|
||||
adminTrackingRules: createEmptyAdminTrackingRulesResponse(),
|
||||
adminSponsors: [] as AdminSponsorItem[],
|
||||
adminShowactApplications: [] as AdminShowactApplicationItem[],
|
||||
adminRiskHistory: [] as AdminRiskFlag[],
|
||||
@@ -212,6 +245,7 @@ export function createAwardsState() {
|
||||
export function normalizeSeasonDetail(detail: AdminSeasonDetailResponse): AdminSeasonDetailResponse {
|
||||
return {
|
||||
...detail,
|
||||
subcategoryTemplates: detail.subcategoryTemplates ?? [],
|
||||
categories: detail.categories ?? [],
|
||||
candidates: (detail.candidates ?? []).map((candidate) => ({
|
||||
...candidate,
|
||||
@@ -221,9 +255,13 @@ export function normalizeSeasonDetail(detail: AdminSeasonDetailResponse): AdminS
|
||||
clipCompilationTitle: candidate.clipCompilationTitle ?? null,
|
||||
clipCompilationPlatform: candidate.clipCompilationPlatform ?? null,
|
||||
clipEmbedStatus: candidate.clipEmbedStatus ?? 'unchecked',
|
||||
streamerIdentityId: candidate.streamerIdentityId ?? null,
|
||||
nominationTally: candidate.nominationTally ?? 0,
|
||||
})),
|
||||
pendingNominations: detail.pendingNominations ?? [],
|
||||
pendingNominationGroups: detail.pendingNominationGroups ?? [],
|
||||
reviewedNominations: detail.reviewedNominations ?? [],
|
||||
trackingReviewNotes: detail.trackingReviewNotes ?? '',
|
||||
results: detail.results ?? [],
|
||||
clipSubmissions: detail.clipSubmissions ?? [],
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user