Refactor app architecture and clean local artifacts

This commit is contained in:
AzuTear
2026-06-24 23:43:14 +02:00
parent 17134b3b82
commit fef1d36fe8
274 changed files with 37724 additions and 6065 deletions
@@ -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,
}
}