172 lines
6.5 KiB
TypeScript
172 lines
6.5 KiB
TypeScript
import { computed, reactive, ref, watch } from 'vue'
|
|
import { CheckCircle2, Clock3, ListChecks, Trophy } from '@lucide/vue'
|
|
|
|
import { useAwardsStore } from '../../stores/awards'
|
|
|
|
const allFilter = 'all'
|
|
|
|
export function useAdminWinnersManager() {
|
|
const store = useAwardsStore()
|
|
const savingResultForCategory = ref<number | null>(null)
|
|
const deletingResultId = ref<number | null>(null)
|
|
const adminMessage = ref('')
|
|
const adminError = ref('')
|
|
const query = ref('')
|
|
const statusFilter = ref(allFilter)
|
|
const winnerSelections = reactive<Record<number, string>>({})
|
|
|
|
const seasonDetail = computed(() => store.adminSeasonDetail)
|
|
const resultMap = computed(() => new Map(seasonDetail.value.results.map((result) => [result.categoryId, result])))
|
|
|
|
const resultRows = computed(() =>
|
|
seasonDetail.value.categories
|
|
.map((category) => {
|
|
const candidates = seasonDetail.value.candidates
|
|
.filter((candidate) => candidate.categoryId === category.id)
|
|
.sort((a, b) => a.displayName.localeCompare(b.displayName))
|
|
const existing = resultMap.value.get(category.id) ?? null
|
|
const openReviews = seasonDetail.value.pendingNominations.filter((nomination) => nomination.categoryId === category.id).length
|
|
const approvedClips = seasonDetail.value.clipSubmissions.filter((clip) => clip.categoryId === category.id && clip.status === 'approved').length
|
|
|
|
return {
|
|
category,
|
|
candidates,
|
|
existing,
|
|
openReviews,
|
|
approvedClips,
|
|
hasPendingReviews: openReviews > 0,
|
|
isEmpty: candidates.length === 0,
|
|
isComplete: Boolean(existing),
|
|
}
|
|
})
|
|
.sort((a, b) => a.category.sortOrder - b.category.sortOrder),
|
|
)
|
|
|
|
const completedCount = computed(() => resultRows.value.filter((row) => row.isComplete).length)
|
|
const missingCount = computed(() => Math.max(0, resultRows.value.length - completedCount.value))
|
|
const reviewWarningCount = computed(() => resultRows.value.filter((row) => row.hasPendingReviews).length)
|
|
const emptyCount = computed(() => resultRows.value.filter((row) => row.isEmpty).length)
|
|
const completionPct = computed(() => resultRows.value.length === 0 ? 0 : Math.round((completedCount.value / resultRows.value.length) * 100))
|
|
|
|
const statusFilters = computed(() => [
|
|
{ value: allFilter, label: 'Alle', count: resultRows.value.length },
|
|
{ value: 'open', label: 'Offen', count: missingCount.value },
|
|
{ value: 'set', label: 'Gesetzt', count: completedCount.value },
|
|
{ value: 'review', label: 'Reviews offen', count: reviewWarningCount.value },
|
|
{ value: 'empty', label: 'Ohne Kandidaten', count: emptyCount.value },
|
|
])
|
|
|
|
const visibleResultRows = computed(() => {
|
|
const normalizedQuery = query.value.trim().toLowerCase()
|
|
|
|
return resultRows.value.filter((row) => {
|
|
const matchesStatus = statusFilter.value === allFilter
|
|
|| (statusFilter.value === 'open' && !row.isComplete)
|
|
|| (statusFilter.value === 'set' && row.isComplete)
|
|
|| (statusFilter.value === 'review' && row.hasPendingReviews)
|
|
|| (statusFilter.value === 'empty' && row.isEmpty)
|
|
|
|
if (!matchesStatus) return false
|
|
if (!normalizedQuery) return true
|
|
|
|
return [
|
|
row.category.name,
|
|
row.category.groupName,
|
|
row.existing?.candidateDisplayName ?? '',
|
|
...row.candidates.map((candidate) => `${candidate.displayName} ${candidate.channelSlug} ${candidate.platform}`),
|
|
].some((value) => value.toLowerCase().includes(normalizedQuery))
|
|
})
|
|
})
|
|
|
|
const summaryCards = computed(() => [
|
|
{
|
|
label: 'Fortschritt',
|
|
value: `${completionPct.value}%`,
|
|
note: `${completedCount.value} von ${resultRows.value.length} Kategorien final.`,
|
|
icon: Trophy,
|
|
tone: 'border-violet-100 bg-violet-50 text-violet-800',
|
|
},
|
|
{
|
|
label: 'Offen',
|
|
value: String(missingCount.value),
|
|
note: missingCount.value === 0 ? 'Keine Gewinner fehlen.' : 'Diese Kategorien brauchen eine Auswahl.',
|
|
icon: ListChecks,
|
|
tone: missingCount.value > 0 ? 'border-sky-100 bg-sky-50 text-sky-800' : 'border-emerald-100 bg-emerald-50 text-emerald-800',
|
|
},
|
|
{
|
|
label: 'Reviews',
|
|
value: String(reviewWarningCount.value),
|
|
note: reviewWarningCount.value === 0 ? 'Keine Review-Warnungen.' : 'Vor finaler Freigabe pruefen.',
|
|
icon: Clock3,
|
|
tone: reviewWarningCount.value > 0 ? 'border-amber-100 bg-amber-50 text-amber-800' : 'border-emerald-100 bg-emerald-50 text-emerald-800',
|
|
},
|
|
{
|
|
label: 'Gesetzt',
|
|
value: String(completedCount.value),
|
|
note: 'Speist Archiv und Gewinneransicht.',
|
|
icon: CheckCircle2,
|
|
tone: 'border-emerald-100 bg-emerald-50 text-emerald-800',
|
|
},
|
|
])
|
|
|
|
watch(resultRows, (rows) => {
|
|
const knownCategoryIds = new Set(rows.map((row) => row.category.id))
|
|
for (const key of Object.keys(winnerSelections)) {
|
|
if (!knownCategoryIds.has(Number(key))) {
|
|
delete winnerSelections[Number(key)]
|
|
}
|
|
}
|
|
|
|
for (const row of rows) {
|
|
winnerSelections[row.category.id] = row.existing?.candidateId ? String(row.existing.candidateId) : ''
|
|
}
|
|
}, { immediate: true })
|
|
|
|
async function saveWinner(categoryId: number) {
|
|
const candidateId = Number(winnerSelections[categoryId])
|
|
if (!candidateId || !seasonDetail.value.id) return
|
|
savingResultForCategory.value = categoryId
|
|
adminMessage.value = ''
|
|
adminError.value = ''
|
|
try {
|
|
await store.setAdminResult(seasonDetail.value.id, { categoryId, candidateId })
|
|
adminMessage.value = 'Gewinner wurde gespeichert.'
|
|
} catch (error) {
|
|
adminError.value = error instanceof Error ? error.message : 'Gewinner konnte nicht gespeichert werden.'
|
|
} finally {
|
|
savingResultForCategory.value = null
|
|
}
|
|
}
|
|
|
|
async function clearWinner(resultId: number) {
|
|
if (!seasonDetail.value.id) return
|
|
deletingResultId.value = resultId
|
|
adminMessage.value = ''
|
|
adminError.value = ''
|
|
try {
|
|
await store.deleteAdminResult(resultId, seasonDetail.value.id)
|
|
adminMessage.value = 'Gewinner wurde entfernt.'
|
|
} catch (error) {
|
|
adminError.value = error instanceof Error ? error.message : 'Gewinner konnte nicht entfernt werden.'
|
|
} finally {
|
|
deletingResultId.value = null
|
|
}
|
|
}
|
|
|
|
return {
|
|
adminError,
|
|
adminMessage,
|
|
completionPct,
|
|
deletingResultId,
|
|
query,
|
|
savingResultForCategory,
|
|
statusFilter,
|
|
statusFilters,
|
|
summaryCards,
|
|
visibleResultRows,
|
|
winnerSelections,
|
|
clearWinner,
|
|
saveWinner,
|
|
}
|
|
}
|