feat: Add 5 Platform Admin views for super-admin dashboard
- AdminDashboard: KPI cards (licenses, users, MRR, servers, signups) + quick links - AdminLicenses: Searchable paginated table with detail panel, CSV export, license generation - AdminSubscriptions: MRR summary cards, per-module breakdown, subscriber table - AdminUsers: Paginated user table with super admin toggle and account disable actions - AdminServers: Filterable server table with connection type badges, status dots, relative heartbeat times Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
116
frontend/src/views/platform-admin/AdminDashboard.vue
Normal file
116
frontend/src/views/platform-admin/AdminDashboard.vue
Normal file
@@ -0,0 +1,116 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useApi } from '@/composables/useApi'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { Key, KeyRound, Users, DollarSign, Server, UserPlus, ArrowRight, ScrollText, CreditCard, MonitorCog } from 'lucide-vue-next'
|
||||
|
||||
const api = useApi()
|
||||
const router = useRouter()
|
||||
|
||||
interface PlatformStats {
|
||||
total_licenses: number
|
||||
active_licenses: number
|
||||
total_users: number
|
||||
module_mrr: number
|
||||
servers_online: number
|
||||
new_signups_this_week: number
|
||||
}
|
||||
|
||||
const stats = ref<PlatformStats | null>(null)
|
||||
const isLoading = ref(false)
|
||||
|
||||
const kpiCards = [
|
||||
{ key: 'total_licenses' as const, label: 'Total Licenses', icon: Key, format: 'number' },
|
||||
{ key: 'active_licenses' as const, label: 'Active Licenses', icon: KeyRound, format: 'number' },
|
||||
{ key: 'total_users' as const, label: 'Total Users', icon: Users, format: 'number' },
|
||||
{ key: 'module_mrr' as const, label: 'Module MRR', icon: DollarSign, format: 'currency' },
|
||||
{ key: 'servers_online' as const, label: 'Servers Online', icon: Server, format: 'number' },
|
||||
{ key: 'new_signups_this_week' as const, label: 'New Signups This Week', icon: UserPlus, format: 'number' },
|
||||
]
|
||||
|
||||
const quickLinks = [
|
||||
{ label: 'Licenses', description: 'Manage license keys and activations', icon: Key, route: '/platform-admin/licenses' },
|
||||
{ label: 'Subscriptions', description: 'View module subscriptions and MRR', icon: CreditCard, route: '/platform-admin/subscriptions' },
|
||||
{ label: 'Users', description: 'Manage platform users and permissions', icon: Users, route: '/platform-admin/users' },
|
||||
{ label: 'Servers', description: 'Monitor connected game servers', icon: MonitorCog, route: '/platform-admin/servers' },
|
||||
]
|
||||
|
||||
function formatValue(value: number, format: string): string {
|
||||
if (format === 'currency') {
|
||||
return `$${value.toFixed(2)}`
|
||||
}
|
||||
return value.toLocaleString()
|
||||
}
|
||||
|
||||
async function fetchStats() {
|
||||
isLoading.value = true
|
||||
try {
|
||||
stats.value = await api.get<PlatformStats>('/admin/stats')
|
||||
} catch {
|
||||
// API not wired yet
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchStats()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="p-6 space-y-8 bg-neutral-950 min-h-screen">
|
||||
<!-- Header -->
|
||||
<div>
|
||||
<div class="flex items-center gap-3 mb-1">
|
||||
<ScrollText class="w-6 h-6 text-oxide-500" />
|
||||
<h1 class="text-2xl font-bold text-neutral-100">Platform Admin</h1>
|
||||
</div>
|
||||
<p class="text-sm text-neutral-400 ml-9">Overview of all platform activity and key metrics.</p>
|
||||
</div>
|
||||
|
||||
<!-- KPI Cards -->
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
<div
|
||||
v-for="card in kpiCards"
|
||||
:key="card.key"
|
||||
class="bg-neutral-900 border border-neutral-800 rounded-lg p-5"
|
||||
>
|
||||
<div class="flex items-center gap-3 mb-3">
|
||||
<div class="p-2 rounded-lg bg-oxide-500/10">
|
||||
<component :is="card.icon" class="w-4 h-4 text-oxide-400" />
|
||||
</div>
|
||||
<p class="text-sm text-neutral-400">{{ card.label }}</p>
|
||||
</div>
|
||||
<!-- Loading state -->
|
||||
<div v-if="isLoading" class="h-8 w-24 bg-neutral-800 rounded animate-pulse" />
|
||||
<!-- Value -->
|
||||
<p v-else class="text-3xl font-bold text-neutral-100">
|
||||
{{ stats ? formatValue(stats[card.key], card.format) : '\u2014' }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Quick Links -->
|
||||
<div>
|
||||
<h2 class="text-lg font-semibold text-neutral-200 mb-4">Quick Links</h2>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
<button
|
||||
v-for="link in quickLinks"
|
||||
:key="link.route"
|
||||
@click="router.push(link.route)"
|
||||
class="bg-neutral-900 border border-neutral-800 rounded-lg p-5 text-left hover:border-oxide-500/40 hover:bg-neutral-800/50 transition-all group"
|
||||
>
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<div class="p-2 rounded-lg bg-oxide-500/10">
|
||||
<component :is="link.icon" class="w-4 h-4 text-oxide-400" />
|
||||
</div>
|
||||
<ArrowRight class="w-4 h-4 text-neutral-600 group-hover:text-oxide-400 transition-colors" />
|
||||
</div>
|
||||
<p class="text-sm font-semibold text-neutral-100 mt-3">{{ link.label }}</p>
|
||||
<p class="text-xs text-neutral-500 mt-1">{{ link.description }}</p>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
Reference in New Issue
Block a user