import { computed } from 'vue' import { AlertTriangle, CheckCircle2, Clock3, Sparkles, Tags, Trophy, Users, Vote } from '@lucide/vue' import { getVoteMetricValue } from '../../lib/adminMetrics' import { useAwardsStore } from '../../stores/awards' export function useAdminAnalyticsManager() { const store = useAwardsStore() const seasonDetail = computed(() => store.adminSeasonDetail) const topCategories = computed(() => store.admin.topCategories) const totalVotes = computed(() => getVoteMetricValue(store.admin.metrics)) const totalNominations = computed(() => store.admin.metrics.find((metric) => metric.label === 'Nominierungen')?.value ?? 0) const maxVotes = computed(() => Math.max(...topCategories.value.map((category) => category.votes), 1)) const resultMap = computed(() => new Map(seasonDetail.value.results.map((result) => [result.categoryId, result]))) const voteMap = computed(() => new Map(topCategories.value.map((category) => [category.category, category.votes]))) const categoryHealth = computed(() => seasonDetail.value.categories .map((category) => { const candidates = seasonDetail.value.candidates.filter((candidate) => candidate.categoryId === category.id).length const reviews = seasonDetail.value.pendingNominations.filter((nomination) => nomination.categoryId === category.id).length const hasWinner = resultMap.value.has(category.id) const votes = voteMap.value.get(category.name) ?? 0 const status = candidates === 0 ? 'Leer' : reviews > 0 ? 'Review offen' : hasWinner ? 'Gewinner gesetzt' : 'Bereit' return { id: category.id, name: category.name, groupName: category.groupName, candidates, reviews, hasWinner, votes, votePct: totalVotes.value > 0 ? Math.round((votes / totalVotes.value) * 100) : 0, status, statusClass: candidates === 0 ? 'border-rose-100 bg-rose-50 text-rose-700' : reviews > 0 ? 'border-amber-100 bg-amber-50 text-amber-700' : hasWinner ? 'border-emerald-100 bg-emerald-50 text-emerald-700' : 'border-sky-100 bg-sky-50 text-sky-700', statusDot: candidates === 0 ? 'bg-rose-400' : reviews > 0 ? 'bg-amber-400' : hasWinner ? 'bg-emerald-400' : 'bg-sky-400', } }) .sort((a, b) => b.reviews - a.reviews || a.candidates - b.candidates || b.votes - a.votes), ) const categoryGroups = computed(() => { const groups = new Map() for (const cat of categoryHealth.value) { const key = cat.groupName || 'Ohne Gruppe' if (!groups.has(key)) groups.set(key, []) groups.get(key)!.push(cat) } return [...groups.entries()].map(([name, cats]) => ({ name, cats })) }) const emptyCategories = computed(() => categoryHealth.value.filter((category) => category.candidates === 0)) const categoriesWithReviews = computed(() => categoryHealth.value.filter((category) => category.reviews > 0)) const categoriesWithoutWinner = computed(() => categoryHealth.value.filter((category) => !category.hasWinner)) const categoriesReady = computed(() => categoryHealth.value.filter((c) => c.status === 'Bereit' || c.status === 'Gewinner gesetzt')) const pendingClipCount = computed(() => seasonDetail.value.clipSubmissions.filter((clip) => clip.status === 'pending').length) const winnerCoveragePct = computed(() => { const categoryCount = seasonDetail.value.categories.length if (categoryCount === 0) return 0 return Math.round((seasonDetail.value.results.length / categoryCount) * 100) }) const readinessPct = computed(() => { const total = categoryHealth.value.length if (total === 0) return 0 return Math.round((categoriesReady.value.length / total) * 100) }) const metricCards = computed(() => [ { label: 'Nominierungen', value: totalNominations.value, note: 'eingereicht im Jahr', icon: Sparkles, tone: 'text-fuchsia-700 bg-fuchsia-50 border-fuchsia-100' }, { label: 'Stimmen', value: totalVotes.value, note: 'gezählte Votes', icon: Vote, tone: 'text-violet-700 bg-violet-50 border-violet-100' }, { label: 'Kandidaten', value: seasonDetail.value.candidates.length, note: 'freigegeben in Kategorien', icon: Users, tone: 'text-cyan-700 bg-cyan-50 border-cyan-100' }, { label: 'Review-Backlog', value: seasonDetail.value.pendingNominations.length, note: 'offene Entscheidungen', icon: Clock3, tone: 'text-amber-700 bg-amber-50 border-amber-100' }, ]) const readinessCards = computed(() => [ { label: 'Kategorie-Abdeckung', value: `${seasonDetail.value.categories.length - emptyCategories.value.length}/${seasonDetail.value.categories.length}`, note: emptyCategories.value.length === 0 ? 'Alle Kategorien haben Kandidaten.' : `${emptyCategories.value.length} Kategorien sind noch leer.`, icon: Tags, to: emptyCategories.value.length > 0 ? `/admin/categories?group=${encodeURIComponent(emptyCategories.value[0]!.groupName)}` : '/admin/categories', }, { label: 'Gewinnerstatus', value: `${winnerCoveragePct.value}%`, note: `${seasonDetail.value.results.length} von ${seasonDetail.value.categories.length} Kategorien final gesetzt.`, icon: Trophy, to: categoriesWithoutWinner.value.length > 0 ? `/admin/winners?status=open&categoryId=${categoriesWithoutWinner.value[0]!.id}` : '/admin/winners?status=set', }, { label: 'Freigabe-Risiko', value: String(categoriesWithReviews.value.length + pendingClipCount.value), note: `${categoriesWithReviews.value.length} Kategorien mit Reviews, ${pendingClipCount.value} Clips offen.`, icon: AlertTriangle, to: categoriesWithReviews.value.length > 0 ? `/admin/nominations?review=1&categoryId=${categoriesWithReviews.value[0]!.id}&status=heavy` : '/admin/clips?status=pending', }, ]) const insightCards = computed(() => { const votesPerCandidate = seasonDetail.value.candidates.length === 0 ? 0 : Math.round(totalVotes.value / seasonDetail.value.candidates.length) const busiestReviewCategory = categoriesWithReviews.value[0] const strongestCategory = topCategories.value[0] return [ { label: 'Votes pro Kandidat', value: votesPerCandidate.toLocaleString('de-DE'), note: 'Zeigt, ob die Kandidatenbasis breit genug fuer die aktuelle Vote-Menge ist.', icon: Vote, }, { label: 'Staerkste Kategorie', value: strongestCategory?.votes.toLocaleString('de-DE') ?? '0', note: strongestCategory ? strongestCategory.category : 'Noch keine Vote-Verteilung vorhanden.', icon: CheckCircle2, }, { label: 'Review-Hotspot', value: String(busiestReviewCategory?.reviews ?? 0), note: busiestReviewCategory ? busiestReviewCategory.name : 'Keine offenen Review-Hotspots.', icon: Clock3, to: busiestReviewCategory ? `/admin/nominations?review=1&categoryId=${busiestReviewCategory.id}&status=heavy` : '/admin/nominations?review=1', }, ] }) const attentionItems = computed(() => [ { key: 'empty-categories', label: 'Leere Kategorien', value: emptyCategories.value.length, note: emptyCategories.value.length === 0 ? 'Keine Luecke in der Kandidatenbasis.' : emptyCategories.value.slice(0, 3).map((category) => category.name).join(', '), to: emptyCategories.value.length > 0 ? `/admin/categories?group=${encodeURIComponent(emptyCategories.value[0]!.groupName)}` : '/admin/categories', tone: emptyCategories.value.length > 0 ? 'border-rose-100 bg-rose-50 text-rose-800' : 'border-emerald-100 bg-emerald-50 text-emerald-800', }, { key: 'pending-reviews', label: 'Offene Reviews', value: seasonDetail.value.pendingNominations.length, note: categoriesWithReviews.value.length === 0 ? 'Review-Queue ist leer.' : `${categoriesWithReviews.value.length} Kategorien betroffen.`, to: categoriesWithReviews.value.length > 0 ? `/admin/nominations?review=1&categoryId=${categoriesWithReviews.value[0]!.id}&status=heavy` : '/admin/nominations?review=1', tone: seasonDetail.value.pendingNominations.length > 0 ? 'border-amber-100 bg-amber-50 text-amber-800' : 'border-emerald-100 bg-emerald-50 text-emerald-800', }, { key: 'missing-winners', label: 'Gewinner fehlen', value: categoriesWithoutWinner.value.length, note: categoriesWithoutWinner.value.length === 0 ? 'Alle Gewinner sind gesetzt.' : 'Finalisierung auf der Gewinner-Seite abschliessen.', to: categoriesWithoutWinner.value.length > 0 ? `/admin/winners?status=open&categoryId=${categoriesWithoutWinner.value[0]!.id}` : '/admin/winners?status=set', tone: categoriesWithoutWinner.value.length > 0 ? 'border-sky-100 bg-sky-50 text-sky-800' : 'border-emerald-100 bg-emerald-50 text-emerald-800', }, ]) // Top categories enriched with vote share % const topCategoriesEnriched = computed(() => topCategories.value.map((cat) => ({ ...cat, pct: totalVotes.value > 0 ? Math.round((cat.votes / totalVotes.value) * 100) : 0, barWidth: totalVotes.value > 0 ? Math.max(2, Math.round((cat.votes / maxVotes.value) * 100)) : 2, })), ) // Health counts for status summary const healthSummary = computed(() => ({ leer: emptyCategories.value.length, reviewOffen: categoriesWithReviews.value.length, bereit: categoryHealth.value.filter((c) => c.status === 'Bereit').length, gewinner: categoryHealth.value.filter((c) => c.status === 'Gewinner gesetzt').length, total: categoryHealth.value.length, })) return { categoryHealth, categoryGroups, metricCards, readinessCards, insightCards, attentionItems, topCategories, topCategoriesEnriched, maxVotes, winnerCoveragePct, readinessPct, healthSummary, totalVotes, } }