Files
secu-frontend/src/views/AdminDashboardView.vue

305 lines
10 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { api } from '@/api'
import { useRouter } from 'vue-router'
const router = useRouter()
const loading = ref(true)
const error = ref('')
const stats = ref({
org_count: 0,
user_count: 0,
order_count: 0,
timesheet_count: 0
})
const organizations = ref<any[]>([])
const selectedOrg = ref<any>(null)
const orgDetails = ref<any>(null)
const showOrgModal = ref(false)
// Broadcast
const broadcastMessage = ref('')
const broadcastType = ref('info')
const broadcasting = ref(false)
onMounted(async () => {
await loadDashboard()
})
async function loadDashboard() {
loading.value = true
error.value = ''
try {
const [dashRes, orgsRes] = await Promise.all([
api.get<any>('/admin/dashboard'),
api.get<any>('/admin/organizations')
])
stats.value = dashRes.data.stats
organizations.value = orgsRes.data.organizations
} catch (e) {
error.value = e instanceof Error ? e.message : 'Laden fehlgeschlagen'
if (error.value.includes('Super-Admin')) {
router.push('/')
}
} finally {
loading.value = false
}
}
async function viewOrg(org: any) {
selectedOrg.value = org
showOrgModal.value = true
try {
const res = await api.get<any>(`/admin/organizations/${org.id}`)
orgDetails.value = res.data
} catch (e) {
console.error('Load org details failed:', e)
}
}
async function impersonate(userId: string) {
if (!confirm('Als dieser User einloggen?')) return
try {
const res = await api.post<any>(`/admin/impersonate/${userId}`)
// Store token and redirect
localStorage.setItem('accessToken', res.data.token)
localStorage.setItem('impersonating', 'true')
window.location.href = '/'
} catch (e) {
alert('Impersonation fehlgeschlagen: ' + (e instanceof Error ? e.message : 'Unbekannter Fehler'))
}
}
async function sendBroadcast() {
if (!broadcastMessage.value.trim()) return
if (!confirm('Nachricht an alle Organisationen senden?')) return
broadcasting.value = true
try {
await api.post('/admin/broadcast', {
message: broadcastMessage.value,
type: broadcastType.value
})
alert('Broadcast gesendet!')
broadcastMessage.value = ''
} catch (e) {
alert('Fehler: ' + (e instanceof Error ? e.message : 'Unbekannt'))
} finally {
broadcasting.value = false
}
}
function formatDate(date: string) {
return new Date(date).toLocaleDateString('de-DE', {
day: '2-digit',
month: '2-digit',
year: 'numeric'
})
}
function getPlanBadge(plan: string) {
const plans: Record<string, string> = {
free: 'badge bg-gray-100 text-gray-800',
starter: 'badge bg-blue-100 text-blue-800',
business: 'badge bg-purple-100 text-purple-800',
enterprise: 'badge bg-yellow-100 text-yellow-800'
}
return plans[plan] || plans.free
}
</script>
<template>
<div class="min-h-screen bg-gray-100 dark:bg-gray-900">
<!-- Header -->
<header class="bg-white dark:bg-gray-800 shadow">
<div class="max-w-7xl mx-auto px-4 py-4 flex items-center justify-between">
<div class="flex items-center gap-3">
<span class="text-2xl">🛡</span>
<h1 class="text-xl font-bold text-gray-900 dark:text-white">
SeCu Admin Panel
</h1>
</div>
<router-link to="/" class="btn btn-secondary text-sm">
Zurück zur App
</router-link>
</div>
</header>
<main class="max-w-7xl mx-auto px-4 py-8">
<!-- Loading -->
<div v-if="loading" class="text-center py-12">
<p class="text-gray-500">Lädt...</p>
</div>
<!-- Error -->
<div v-else-if="error" class="card bg-red-50 text-red-700 text-center">
{{ error }}
</div>
<!-- Content -->
<div v-else class="space-y-6">
<!-- Stats -->
<div class="grid grid-cols-1 md:grid-cols-4 gap-4">
<div class="card">
<div class="text-3xl font-bold text-primary-600">{{ stats.org_count }}</div>
<div class="text-sm text-gray-500">Organisationen</div>
</div>
<div class="card">
<div class="text-3xl font-bold text-green-600">{{ stats.user_count }}</div>
<div class="text-sm text-gray-500">Benutzer</div>
</div>
<div class="card">
<div class="text-3xl font-bold text-blue-600">{{ stats.order_count }}</div>
<div class="text-sm text-gray-500">Aufträge</div>
</div>
<div class="card">
<div class="text-3xl font-bold text-purple-600">{{ stats.timesheet_count }}</div>
<div class="text-sm text-gray-500">Stundenzettel</div>
</div>
</div>
<!-- Broadcast -->
<div class="card">
<h2 class="text-lg font-semibold text-gray-900 dark:text-white mb-4">
📢 Broadcast an alle Kunden
</h2>
<div class="flex gap-4">
<select v-model="broadcastType" class="input w-32">
<option value="info">Info</option>
<option value="warning">Warnung</option>
<option value="success">Erfolg</option>
</select>
<input
v-model="broadcastMessage"
type="text"
class="input flex-1"
placeholder="Nachricht eingeben..."
/>
<button
@click="sendBroadcast"
:disabled="broadcasting || !broadcastMessage.trim()"
class="btn btn-primary"
>
{{ broadcasting ? 'Sendet...' : 'Senden' }}
</button>
</div>
</div>
<!-- Organizations Table -->
<div class="card">
<h2 class="text-lg font-semibold text-gray-900 dark:text-white mb-4">
🏢 Organisationen
</h2>
<div class="overflow-x-auto">
<table class="w-full">
<thead class="text-left text-sm text-gray-500 border-b dark:border-gray-700">
<tr>
<th class="pb-3 font-medium">Organisation</th>
<th class="pb-3 font-medium">Slug</th>
<th class="pb-3 font-medium">Plan</th>
<th class="pb-3 font-medium">Users</th>
<th class="pb-3 font-medium">Aufträge</th>
<th class="pb-3 font-medium">Erstellt</th>
<th class="pb-3 font-medium"></th>
</tr>
</thead>
<tbody class="divide-y dark:divide-gray-700">
<tr v-for="org in organizations" :key="org.id" class="hover:bg-gray-50 dark:hover:bg-gray-800">
<td class="py-3 font-medium text-gray-900 dark:text-white">
{{ org.name }}
</td>
<td class="py-3 text-gray-500">{{ org.slug }}</td>
<td class="py-3">
<span :class="getPlanBadge(org.settings?.plan || 'free')">
{{ org.settings?.plan || 'free' }}
</span>
</td>
<td class="py-3 text-gray-500">{{ org.user_count }}</td>
<td class="py-3 text-gray-500">{{ org.order_count }}</td>
<td class="py-3 text-gray-500">{{ formatDate(org.created_at) }}</td>
<td class="py-3">
<button @click="viewOrg(org)" class="text-primary-600 hover:text-primary-800 text-sm">
Details
</button>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</main>
<!-- Org Details Modal -->
<div v-if="showOrgModal" class="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-xl max-w-2xl w-full max-h-[80vh] overflow-y-auto">
<div class="p-6">
<div class="flex items-center justify-between mb-6">
<h2 class="text-xl font-bold text-gray-900 dark:text-white">
{{ selectedOrg?.name }}
</h2>
<button @click="showOrgModal = false" class="text-gray-400 hover:text-gray-600">
</button>
</div>
<div v-if="orgDetails" class="space-y-6">
<!-- Stats -->
<div class="grid grid-cols-3 gap-4">
<div class="bg-gray-50 dark:bg-gray-700 rounded-lg p-4 text-center">
<div class="text-2xl font-bold">{{ orgDetails.users?.length || 0 }}</div>
<div class="text-sm text-gray-500">Benutzer</div>
</div>
<div class="bg-gray-50 dark:bg-gray-700 rounded-lg p-4 text-center">
<div class="text-2xl font-bold">{{ orgDetails.stats?.order_count || 0 }}</div>
<div class="text-sm text-gray-500">Aufträge</div>
</div>
<div class="bg-gray-50 dark:bg-gray-700 rounded-lg p-4 text-center">
<div class="text-2xl font-bold">{{ orgDetails.stats?.timesheet_count || 0 }}</div>
<div class="text-sm text-gray-500">Stundenzettel</div>
</div>
</div>
<!-- Users -->
<div>
<h3 class="font-semibold text-gray-900 dark:text-white mb-3">Benutzer</h3>
<div class="space-y-2">
<div
v-for="user in orgDetails.users"
:key="user.id"
class="flex items-center justify-between p-3 bg-gray-50 dark:bg-gray-700 rounded-lg"
>
<div>
<div class="font-medium text-gray-900 dark:text-white">
{{ user.first_name }} {{ user.last_name }}
<span v-if="!user.active" class="text-red-500 text-sm">(inaktiv)</span>
</div>
<div class="text-sm text-gray-500">{{ user.email }} {{ user.role }}</div>
</div>
<button
@click="impersonate(user.id)"
class="text-sm text-primary-600 hover:text-primary-800"
>
Als User einloggen
</button>
</div>
</div>
</div>
</div>
<div v-else class="text-center py-8 text-gray-500">
Lädt...
</div>
</div>
</div>
</div>
</div>
</template>