187 lines
7.3 KiB
TypeScript
187 lines
7.3 KiB
TypeScript
import { computed, onMounted, ref } from 'vue'
|
|
import { CheckCircle2, Database, FileText, Link2, ShieldCheck, Tags, UserRound } from '@lucide/vue'
|
|
|
|
import { getRiskMetricValue } from '../../lib/adminMetrics'
|
|
import { useAwardsStore } from '../../stores/awards'
|
|
import type { AdminSettingsCheckItem, AdminSettingsGateItem } from './adminSettingsTypes'
|
|
|
|
export function useAdminSettingsOverview() {
|
|
const store = useAwardsStore()
|
|
const healthLoading = ref(false)
|
|
const healthError = ref('')
|
|
const healthLoadedAt = ref<Date | null>(null)
|
|
const seasonDetail = computed(() => store.adminSeasonDetail)
|
|
const databaseHealth = computed(() => store.databaseHealth)
|
|
const hasVotingPhase = computed(() => seasonDetail.value.currentPhase.toLowerCase().includes('voting'))
|
|
const categoriesWithoutCandidates = computed(() =>
|
|
seasonDetail.value.categories.filter((category) =>
|
|
!seasonDetail.value.candidates.some((candidate) => candidate.categoryId === category.id),
|
|
),
|
|
)
|
|
const pendingClips = computed(() => seasonDetail.value.clipSubmissions.filter((clip) => clip.status === 'pending').length)
|
|
const pendingMigrationCount = computed(() => databaseHealth.value.pendingMigrations.length)
|
|
const openRiskCount = computed(() => getRiskMetricValue(store.admin.metrics))
|
|
const healthLoadedLabel = computed(() =>
|
|
healthLoadedAt.value ? healthLoadedAt.value.toLocaleString('de-DE', { dateStyle: 'short', timeStyle: 'short' }) : 'Noch nicht aktualisiert',
|
|
)
|
|
const siteSettings = computed(() => store.adminSiteSettings)
|
|
const configuredFooterLinks = computed(() => [
|
|
siteSettings.value.imprintUrl,
|
|
siteSettings.value.contactUrl,
|
|
siteSettings.value.sponsorsUrl,
|
|
siteSettings.value.newsletterUrl,
|
|
].filter((url) => url.trim()).length)
|
|
const configuredFooterPages = computed(() => [
|
|
siteSettings.value.imprintContent,
|
|
siteSettings.value.contactContent,
|
|
siteSettings.value.sponsorsContent,
|
|
].filter((content) => content.trim()).length)
|
|
const contentChecks = computed<AdminSettingsCheckItem[]>(() => [
|
|
{
|
|
label: 'Host',
|
|
value: Boolean(siteSettings.value.hostDisplayName.trim() && siteSettings.value.hostTagline.trim()),
|
|
note: siteSettings.value.hostDisplayName.trim()
|
|
? siteSettings.value.hostTagline.trim() ? siteSettings.value.hostDisplayName.trim() : 'Host-Tagline fehlt'
|
|
: 'Kein Host-Name hinterlegt',
|
|
icon: UserRound,
|
|
to: '/admin/content',
|
|
},
|
|
{
|
|
label: 'Social Links',
|
|
value: siteSettings.value.socialLinks.length > 0,
|
|
note: `${siteSettings.value.socialLinks.length} Links gespeichert`,
|
|
icon: Link2,
|
|
to: '/admin/content',
|
|
},
|
|
{
|
|
label: 'FAQ',
|
|
value: siteSettings.value.faq.length > 0,
|
|
note: `${siteSettings.value.faq.length} Fragen gespeichert`,
|
|
icon: FileText,
|
|
to: '/admin/content',
|
|
},
|
|
{
|
|
label: 'Footer Links',
|
|
value: configuredFooterLinks.value === 4 && configuredFooterPages.value === 3,
|
|
note: `${configuredFooterLinks.value} von 4 Link-Zielen, ${configuredFooterPages.value} von 3 Footer-Seiten gepflegt`,
|
|
icon: Link2,
|
|
to: '/admin/content',
|
|
},
|
|
{
|
|
label: 'Datenschutz',
|
|
value: Boolean(siteSettings.value.privacyPolicyContent.trim() && siteSettings.value.privacyEmail.trim()),
|
|
note: siteSettings.value.privacyPolicyUpdatedAt
|
|
? `Zuletzt aktualisiert: ${new Date(siteSettings.value.privacyPolicyUpdatedAt).toLocaleString('de-DE')}`
|
|
: 'Datenschutztext oder Datenschutz-Mail fehlt',
|
|
icon: ShieldCheck,
|
|
to: '/admin/content',
|
|
},
|
|
])
|
|
const contentCompletion = computed(() => contentChecks.value.filter((check) => check.value).length)
|
|
|
|
const checks = computed<AdminSettingsCheckItem[]>(() => [
|
|
{
|
|
label: 'Backend verbunden',
|
|
value: store.apiMode === 'api',
|
|
note: store.apiMode === 'api' ? 'Admin-Daten kommen aus der API.' : 'Fallback-Daten aktiv oder API nicht erreichbar.',
|
|
icon: Database,
|
|
to: null,
|
|
},
|
|
{
|
|
label: 'Postgres verbunden',
|
|
value: databaseHealth.value.canConnect,
|
|
note: databaseHealth.value.canConnect
|
|
? `${databaseHealth.value.provider} erreichbar · Quelle: ${databaseHealth.value.configuredConnection.source}.`
|
|
: databaseHealth.value.error || 'Datenbank ist nicht erreichbar.',
|
|
icon: Database,
|
|
to: null,
|
|
},
|
|
{
|
|
label: 'Migrationen aktuell',
|
|
value: databaseHealth.value.canConnect && pendingMigrationCount.value === 0,
|
|
note: pendingMigrationCount.value === 0
|
|
? 'Keine ausstehenden Migrationen.'
|
|
: `${pendingMigrationCount.value} Migrationen warten auf Anwendung.`,
|
|
icon: CheckCircle2,
|
|
to: null,
|
|
},
|
|
{
|
|
label: 'Public-Jahr gesetzt',
|
|
value: seasonDetail.value.isCurrent,
|
|
note: seasonDetail.value.isCurrent ? `${seasonDetail.value.year} ist öffentlich markiert.` : 'Das gewählte Jahr ist aktuell intern.',
|
|
icon: CheckCircle2,
|
|
to: '/admin/years',
|
|
},
|
|
{
|
|
label: 'Voting-Basis vollständig',
|
|
value: categoriesWithoutCandidates.value.length === 0 && seasonDetail.value.categories.length > 0,
|
|
note: categoriesWithoutCandidates.value.length === 0 ? 'Alle Kategorien haben Kandidaten.' : `${categoriesWithoutCandidates.value.length} Kategorien brauchen Kandidaten.`,
|
|
icon: Tags,
|
|
to: '/admin/categories',
|
|
},
|
|
{
|
|
label: 'Risiko-Queue leer',
|
|
value: openRiskCount.value === 0,
|
|
note: `${openRiskCount.value} offene Risikohinweise im Admin-Kontext.`,
|
|
icon: ShieldCheck,
|
|
to: '/admin/risk',
|
|
},
|
|
])
|
|
const gates = computed<AdminSettingsGateItem[]>(() => [
|
|
{
|
|
label: 'Nominierungsfenster',
|
|
state: seasonDetail.value.currentPhase.toLowerCase().includes('nomin'),
|
|
note: seasonDetail.value.currentPhase.toLowerCase().includes('nomin')
|
|
? 'Öffentliche Nominierung ist aktiv.'
|
|
: 'Die aktuelle Phase erlaubt keine neuen Nominierungen.',
|
|
to: '/admin/years',
|
|
},
|
|
{
|
|
label: 'Voting freigeschaltet',
|
|
state: hasVotingPhase.value && categoriesWithoutCandidates.value.length === 0,
|
|
note: hasVotingPhase.value ? 'Phase ist Voting; Kategorie-Readiness entscheidet.' : 'Phase ist nicht Voting.',
|
|
to: '/admin/categories',
|
|
},
|
|
{
|
|
label: 'Clip-Reviews offen',
|
|
state: pendingClips.value > 0,
|
|
note: pendingClips.value > 0 ? `${pendingClips.value} Clip-Einreichungen offen.` : 'Keine offenen Clip-Einreichungen.',
|
|
to: '/admin/clips',
|
|
},
|
|
{
|
|
label: 'Freitext-Reviews geklärt',
|
|
state: seasonDetail.value.pendingNominations.length === 0,
|
|
note: `${seasonDetail.value.pendingNominations.length} offene Freitext-Reviews.`,
|
|
to: '/admin/nominations?review=1',
|
|
},
|
|
])
|
|
|
|
async function refreshDatabaseHealth() {
|
|
healthLoading.value = true
|
|
healthError.value = ''
|
|
try {
|
|
await store.loadDatabaseHealth()
|
|
healthLoadedAt.value = new Date()
|
|
} catch (error) {
|
|
healthError.value = error instanceof Error ? error.message : 'Datenbank-Healthcheck konnte nicht geladen werden.'
|
|
} finally {
|
|
healthLoading.value = false
|
|
}
|
|
}
|
|
|
|
onMounted(refreshDatabaseHealth)
|
|
|
|
return {
|
|
healthLoading,
|
|
healthError,
|
|
databaseHealth,
|
|
pendingMigrationCount,
|
|
healthLoadedLabel,
|
|
contentChecks,
|
|
contentCompletion,
|
|
checks,
|
|
gates,
|
|
refreshDatabaseHealth,
|
|
}
|
|
}
|