Refactor app architecture and clean local artifacts
This commit is contained in:
@@ -1,143 +1,109 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { CheckCircle2, Database, Settings, ShieldCheck, Tags, Vote } from '@lucide/vue'
|
||||
import { RouterLink } from 'vue-router'
|
||||
import { Settings } from '@lucide/vue'
|
||||
import { computed, onBeforeUnmount, onMounted } from 'vue'
|
||||
import { onBeforeRouteLeave } from 'vue-router'
|
||||
|
||||
import AdminOperationalSettingsCard from '../../components/admin/AdminOperationalSettingsCard.vue'
|
||||
import AdminPageHeader from '../../components/admin/AdminPageHeader.vue'
|
||||
import AdminSeasonToolbar from '../../components/admin/AdminSeasonToolbar.vue'
|
||||
import Card from '../../components/ui/Card.vue'
|
||||
import { useAwardsStore } from '../../stores/awards'
|
||||
import AdminSettingsDatabaseCard from '../../components/admin/AdminSettingsDatabaseCard.vue'
|
||||
import AdminSettingsOverviewBoard from '../../components/admin/AdminSettingsOverviewBoard.vue'
|
||||
import { useAuthStore } from '../../stores/auth'
|
||||
import { useAdminOperationalSettings } from '../../components/admin/useAdminOperationalSettings'
|
||||
import { useAdminSettingsOverview } from '../../components/admin/useAdminSettingsOverview'
|
||||
|
||||
const store = useAwardsStore()
|
||||
const seasonDetail = computed(() => store.adminSeasonDetail)
|
||||
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 authStore = useAuthStore()
|
||||
const canManageOperationalSettings = computed(() => authStore.canManageOperationalSettings)
|
||||
|
||||
const checks = computed(() => [
|
||||
{
|
||||
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: '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: store.admin.riskFlags.length === 0,
|
||||
note: `${store.admin.riskFlags.length} offene Risikohinweise im Admin-Kontext.`,
|
||||
icon: ShieldCheck,
|
||||
to: '/admin/risk',
|
||||
},
|
||||
])
|
||||
const gates = computed(() => [
|
||||
{
|
||||
label: 'Nominierungen',
|
||||
state: seasonDetail.value.currentPhase.toLowerCase().includes('nomin'),
|
||||
note: 'Aktiv, wenn die Season-Phase auf Nominierung steht.',
|
||||
to: '/admin/years',
|
||||
},
|
||||
{
|
||||
label: 'Voting',
|
||||
state: hasVotingPhase.value && categoriesWithoutCandidates.value.length === 0,
|
||||
note: hasVotingPhase.value ? 'Phase ist Voting; Kategorie-Readiness entscheidet.' : 'Phase ist nicht Voting.',
|
||||
to: '/admin/voting',
|
||||
},
|
||||
{
|
||||
label: 'Clip-Moderation',
|
||||
state: pendingClips.value > 0,
|
||||
note: pendingClips.value > 0 ? `${pendingClips.value} Clip-Einreichungen offen.` : 'Keine offenen Clip-Einreichungen.',
|
||||
to: '/admin/clips',
|
||||
},
|
||||
{
|
||||
label: 'Review-Freeze',
|
||||
state: seasonDetail.value.pendingNominations.length === 0,
|
||||
note: `${seasonDetail.value.pendingNominations.length} offene Freitext-Reviews.`,
|
||||
to: '/admin/reviews',
|
||||
},
|
||||
])
|
||||
const {
|
||||
healthLoading,
|
||||
healthError,
|
||||
databaseHealth,
|
||||
pendingMigrationCount,
|
||||
healthLoadedLabel,
|
||||
contentChecks,
|
||||
contentCompletion,
|
||||
checks,
|
||||
gates,
|
||||
refreshDatabaseHealth,
|
||||
} = useAdminSettingsOverview()
|
||||
|
||||
const {
|
||||
operationalLoading,
|
||||
operationalSaving,
|
||||
operationalError,
|
||||
operationalSuccess,
|
||||
operationalForm,
|
||||
demoPasswordSet,
|
||||
demoManagedByDatabase,
|
||||
demoPasswordInput,
|
||||
demoPasswordHint,
|
||||
demoCredentialsComplete,
|
||||
operationalSummary,
|
||||
hasUnsavedOperationalChanges,
|
||||
saveOperationalSettings,
|
||||
} = useAdminOperationalSettings()
|
||||
|
||||
function confirmDiscardOperationalChanges() {
|
||||
if (!hasUnsavedOperationalChanges.value) {
|
||||
return true
|
||||
}
|
||||
|
||||
return window.confirm('Du hast ungespeicherte Änderungen in Demo & Wartung. Änderungen verwerfen?')
|
||||
}
|
||||
|
||||
function handleBeforeUnload(event: BeforeUnloadEvent) {
|
||||
if (!hasUnsavedOperationalChanges.value) return
|
||||
event.preventDefault()
|
||||
event.returnValue = ''
|
||||
}
|
||||
|
||||
onBeforeRouteLeave(() => confirmDiscardOperationalChanges())
|
||||
onMounted(() => window.addEventListener('beforeunload', handleBeforeUnload))
|
||||
onBeforeUnmount(() => window.removeEventListener('beforeunload', handleBeforeUnload))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="space-y-6">
|
||||
<AdminPageHeader
|
||||
eyebrow="Einstellungen"
|
||||
title="Systemchecks ohne Doppelpflege"
|
||||
description="Diese Seite speichert keine Season-Daten mehr. Sie zeigt, ob API, Public-Jahr, Voting-Basis, Reviews, Clips und Risiko-Queue für den Award-Betrieb gesund sind."
|
||||
description="Demo-Zugang, Wartungsmodus und Healthchecks."
|
||||
:icon="Settings"
|
||||
/>
|
||||
|
||||
<AdminSeasonToolbar />
|
||||
|
||||
<section class="grid gap-4 md:grid-cols-2 xl:grid-cols-4">
|
||||
<RouterLink
|
||||
v-for="check in checks"
|
||||
:key="check.label"
|
||||
:to="check.to ?? '/admin/settings'"
|
||||
class="rounded-[26px] border bg-white/85 p-5 shadow-[0_18px_46px_rgba(168,145,214,0.09)] transition hover:bg-violet-50/50"
|
||||
:class="check.value ? 'border-emerald-100' : 'border-amber-100'"
|
||||
>
|
||||
<div class="flex items-start justify-between gap-4">
|
||||
<div>
|
||||
<p class="text-xs font-semibold uppercase tracking-[0.18em]" :class="check.value ? 'text-emerald-600' : 'text-amber-600'">{{ check.label }}</p>
|
||||
<p class="mt-3 text-sm leading-6 text-slate-600">{{ check.note }}</p>
|
||||
</div>
|
||||
<div class="grid h-10 w-10 shrink-0 place-items-center rounded-2xl" :class="check.value ? 'bg-emerald-50 text-emerald-700' : 'bg-amber-50 text-amber-700'">
|
||||
<component :is="check.icon" class="h-5 w-5" />
|
||||
</div>
|
||||
</div>
|
||||
</RouterLink>
|
||||
</section>
|
||||
<AdminSettingsOverviewBoard
|
||||
:checks="checks"
|
||||
:gates="gates"
|
||||
:content-checks="contentChecks"
|
||||
:content-completion="contentCompletion"
|
||||
/>
|
||||
|
||||
<Card class="p-6">
|
||||
<div class="flex items-start justify-between gap-4">
|
||||
<div>
|
||||
<p class="text-xs font-semibold uppercase tracking-[0.24em] text-violet-500">Feature Gates</p>
|
||||
<h2 class="mt-2 font-[Cormorant_Garamond] text-4xl text-violet-800">Was ist wirklich aktiv?</h2>
|
||||
<p class="mt-2 max-w-2xl text-sm leading-6 text-slate-500">
|
||||
Die Gates sind aus aktuellen Daten abgeleitet und verlinken zum Ort, an dem der Zustand behoben wird.
|
||||
</p>
|
||||
</div>
|
||||
<Vote class="h-6 w-6 text-violet-500" />
|
||||
</div>
|
||||
<AdminOperationalSettingsCard
|
||||
v-model:demo-password="demoPasswordInput"
|
||||
:form="operationalForm"
|
||||
:loading="operationalLoading"
|
||||
:saving="operationalSaving"
|
||||
:error="operationalError"
|
||||
:success="operationalSuccess"
|
||||
:demo-password-hint="demoPasswordHint"
|
||||
:demo-password-set="demoPasswordSet"
|
||||
:demo-managed-by-database="demoManagedByDatabase"
|
||||
:demo-credentials-complete="demoCredentialsComplete"
|
||||
:summary="operationalSummary"
|
||||
:dirty="hasUnsavedOperationalChanges"
|
||||
:can-manage="canManageOperationalSettings"
|
||||
@save="saveOperationalSettings"
|
||||
/>
|
||||
|
||||
<div class="mt-5 grid gap-3 md:grid-cols-2">
|
||||
<RouterLink
|
||||
v-for="gate in gates"
|
||||
:key="gate.label"
|
||||
:to="gate.to"
|
||||
class="rounded-[22px] border p-4 transition hover:bg-violet-50/50"
|
||||
:class="gate.state ? 'border-emerald-100 bg-emerald-50/40' : 'border-slate-100 bg-slate-50/70'"
|
||||
>
|
||||
<div class="flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<p class="font-semibold text-slate-900">{{ gate.label }}</p>
|
||||
<p class="mt-1 text-sm leading-6 text-slate-500">{{ gate.note }}</p>
|
||||
</div>
|
||||
<span class="rounded-full px-3 py-1 text-xs font-semibold" :class="gate.state ? 'bg-emerald-100 text-emerald-700' : 'bg-slate-200 text-slate-600'">
|
||||
{{ gate.state ? 'aktiv' : 'inaktiv' }}
|
||||
</span>
|
||||
</div>
|
||||
</RouterLink>
|
||||
</div>
|
||||
</Card>
|
||||
<AdminSettingsDatabaseCard
|
||||
:health-loading="healthLoading"
|
||||
:health-error="healthError"
|
||||
:database-health="databaseHealth"
|
||||
:pending-migration-count="pendingMigrationCount"
|
||||
:health-loaded-label="healthLoadedLabel"
|
||||
:refresh-database-health="refreshDatabaseHealth"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user