Refactor app architecture and clean local artifacts
This commit is contained in:
@@ -0,0 +1,181 @@
|
||||
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 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,
|
||||
note: `${configuredFooterLinks.value} von 4 Link-Zielen 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,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user