Refactor app architecture and clean local artifacts
This commit is contained in:
@@ -0,0 +1,171 @@
|
||||
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,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user