191 lines
6.8 KiB
TypeScript
191 lines
6.8 KiB
TypeScript
import { computed, reactive, ref, watch } from 'vue'
|
|
|
|
import { useAwardsStore } from '../../stores/awards'
|
|
import type { AdminCategoryItem } from '../../types/awards'
|
|
|
|
type CategoryStatusFilter = 'all' | 'empty' | 'reviews' | 'thin'
|
|
|
|
interface CategoryForm {
|
|
groupName: string
|
|
name: string
|
|
slug: string
|
|
description: string
|
|
sortOrder: number
|
|
maxNomineesPerUser: number
|
|
}
|
|
|
|
export function useAdminCategoryManager() {
|
|
const store = useAwardsStore()
|
|
const selectedSeasonId = computed(() => store.adminSelectedSeasonId)
|
|
const seasonDetail = computed(() => store.adminSeasonDetail)
|
|
const query = ref('')
|
|
const statusFilter = ref<CategoryStatusFilter>('all')
|
|
const selectedCategoryId = ref<number | null>(null)
|
|
const saving = ref<number | 'new' | null>(null)
|
|
const adminMessage = ref('')
|
|
const adminError = ref('')
|
|
const categoryToDelete = ref<AdminCategoryItem | null>(null)
|
|
const deleting = ref(false)
|
|
|
|
const editForms = reactive<Record<number, CategoryForm>>({})
|
|
const newCategoryForm = reactive(createEmptyCategoryForm())
|
|
|
|
const categoriesWithState = computed(() =>
|
|
seasonDetail.value.categories
|
|
.map((category) => ({
|
|
...category,
|
|
pending: seasonDetail.value.pendingNominations.filter((nomination) => nomination.categoryId === category.id).length,
|
|
candidates: seasonDetail.value.candidates.filter((candidate) => candidate.categoryId === category.id).length,
|
|
}))
|
|
.sort((a, b) => a.sortOrder - b.sortOrder),
|
|
)
|
|
const filteredCategories = computed(() => {
|
|
const search = query.value.trim().toLowerCase()
|
|
return categoriesWithState.value.filter((category) => {
|
|
const matchesStatus =
|
|
statusFilter.value === 'all' ||
|
|
(statusFilter.value === 'empty' && category.candidates === 0) ||
|
|
(statusFilter.value === 'reviews' && category.pending > 0) ||
|
|
(statusFilter.value === 'thin' && category.candidates > 0 && category.candidates < Math.max(2, category.maxNomineesPerUser))
|
|
const matchesSearch = !search || [category.groupName, category.name, category.slug, category.description]
|
|
.join(' ')
|
|
.toLowerCase()
|
|
.includes(search)
|
|
return matchesStatus && matchesSearch
|
|
})
|
|
})
|
|
const selectedCategory = computed(() =>
|
|
filteredCategories.value.find((category) => category.id === selectedCategoryId.value) ?? filteredCategories.value[0] ?? null,
|
|
)
|
|
const categoryStats = computed(() => [
|
|
{ label: 'Kategorien', value: seasonDetail.value.categories.length },
|
|
{ label: 'Kandidaten', value: seasonDetail.value.candidates.length },
|
|
{ label: 'Reviews', value: seasonDetail.value.pendingNominations.length },
|
|
])
|
|
const statusFilters = computed(() => [
|
|
{ key: 'all' as const, label: 'Alle', count: categoriesWithState.value.length },
|
|
{ key: 'empty' as const, label: 'Ohne Kandidaten', count: categoriesWithState.value.filter((category) => category.candidates === 0).length },
|
|
{ key: 'reviews' as const, label: 'Mit Reviews', count: categoriesWithState.value.filter((category) => category.pending > 0).length },
|
|
{ key: 'thin' as const, label: 'Dünn besetzt', count: categoriesWithState.value.filter((category) => category.candidates > 0 && category.candidates < Math.max(2, category.maxNomineesPerUser)).length },
|
|
])
|
|
|
|
watch(
|
|
seasonDetail,
|
|
(detail) => {
|
|
for (const category of detail.categories) {
|
|
editForms[category.id] = createCategoryForm(category)
|
|
}
|
|
newCategoryForm.sortOrder = detail.categories.length + 1
|
|
if (!detail.categories.some((category) => category.id === selectedCategoryId.value)) {
|
|
selectedCategoryId.value = detail.categories[0]?.id ?? null
|
|
}
|
|
},
|
|
{ immediate: true },
|
|
)
|
|
|
|
watch(filteredCategories, (categories) => {
|
|
if (!categories.some((category) => category.id === selectedCategoryId.value)) {
|
|
selectedCategoryId.value = categories[0]?.id ?? null
|
|
}
|
|
})
|
|
|
|
async function saveCategory(categoryId: number) {
|
|
if (!selectedSeasonId.value) return
|
|
saving.value = categoryId
|
|
adminMessage.value = ''
|
|
adminError.value = ''
|
|
try {
|
|
await store.updateAdminCategory(categoryId, selectedSeasonId.value, editForms[categoryId])
|
|
adminMessage.value = 'Kategorie gespeichert.'
|
|
} catch (error) {
|
|
adminError.value = error instanceof Error ? error.message : 'Kategorie konnte nicht gespeichert werden.'
|
|
} finally {
|
|
saving.value = null
|
|
}
|
|
}
|
|
|
|
async function createCategory() {
|
|
if (!selectedSeasonId.value) return
|
|
saving.value = 'new'
|
|
adminMessage.value = ''
|
|
adminError.value = ''
|
|
try {
|
|
await store.createAdminCategory(selectedSeasonId.value, newCategoryForm)
|
|
adminMessage.value = 'Kategorie angelegt.'
|
|
Object.assign(newCategoryForm, createEmptyCategoryForm(seasonDetail.value.categories.length + 1))
|
|
} catch (error) {
|
|
adminError.value = error instanceof Error ? error.message : 'Kategorie konnte nicht angelegt werden.'
|
|
} finally {
|
|
saving.value = null
|
|
}
|
|
}
|
|
|
|
function fillNewSlug() {
|
|
newCategoryForm.slug = slugify(newCategoryForm.name)
|
|
}
|
|
|
|
async function confirmDeleteCategory() {
|
|
if (!categoryToDelete.value || !selectedSeasonId.value) return
|
|
deleting.value = true
|
|
adminError.value = ''
|
|
try {
|
|
await store.deleteAdminCategory(categoryToDelete.value.id, selectedSeasonId.value)
|
|
adminMessage.value = `Kategorie „${categoryToDelete.value.name}" wurde gelöscht.`
|
|
categoryToDelete.value = null
|
|
} catch (error) {
|
|
adminError.value = error instanceof Error ? error.message : 'Löschen fehlgeschlagen.'
|
|
} finally {
|
|
deleting.value = false
|
|
}
|
|
}
|
|
|
|
return {
|
|
selectedSeasonId,
|
|
query,
|
|
statusFilter,
|
|
selectedCategoryId,
|
|
saving,
|
|
adminMessage,
|
|
adminError,
|
|
editForms,
|
|
newCategoryForm,
|
|
filteredCategories,
|
|
selectedCategory,
|
|
categoryStats,
|
|
statusFilters,
|
|
categoryToDelete,
|
|
deleting,
|
|
saveCategory,
|
|
createCategory,
|
|
fillNewSlug,
|
|
confirmDeleteCategory,
|
|
}
|
|
}
|
|
|
|
function createCategoryForm(category: AdminCategoryItem): CategoryForm
|
|
function createCategoryForm(category?: AdminCategoryItem, sortOrder?: number): CategoryForm
|
|
function createCategoryForm(category?: AdminCategoryItem, sortOrder = 1): CategoryForm {
|
|
return {
|
|
groupName: category?.groupName ?? '',
|
|
name: category?.name ?? '',
|
|
slug: category?.slug ?? '',
|
|
description: category?.description ?? '',
|
|
sortOrder: category?.sortOrder ?? sortOrder,
|
|
maxNomineesPerUser: category?.maxNomineesPerUser ?? 3,
|
|
}
|
|
}
|
|
|
|
function createEmptyCategoryForm(sortOrder = 1): CategoryForm {
|
|
return createCategoryForm(undefined, sortOrder)
|
|
}
|
|
|
|
function slugify(value: string) {
|
|
return value
|
|
.trim()
|
|
.toLowerCase()
|
|
.normalize('NFD')
|
|
.replace(/[\u0300-\u036f]/g, '')
|
|
.replace(/[^a-z0-9]+/g, '-')
|
|
.replace(/^-+|-+$/g, '')
|
|
}
|