feat: Complete Admin Panel UI

- 3 tabs: Overview, Organizations, Module Prices
- Subscription management: Free, Pause, Activate, Extend Trial
- Module management per organization with toggles
- Module pricing display (inklusive vs premium)
- Search & filter organizations
- Status badges and visual indicators
This commit is contained in:
2026-03-13 06:06:57 +00:00
parent 582ad86921
commit 9d8b00d9c9

View File

@@ -1,27 +1,74 @@
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { ref, computed, onMounted } from 'vue'
import { api } from '@/api'
import { useRouter } from 'vue-router'
const router = useRouter()
// State
const loading = ref(true)
const error = ref('')
const activeTab = ref<'overview' | 'organizations' | 'modules'>('overview')
// Stats
const stats = ref({
org_count: 0,
user_count: 0,
order_count: 0,
timesheet_count: 0
})
// Organizations
const organizations = ref<any[]>([])
const selectedOrg = ref<any>(null)
const orgDetails = ref<any>(null)
const orgModules = ref<any[]>([])
const showOrgModal = ref(false)
const orgAction = ref<'details' | 'subscription' | 'modules'>('details')
const actionLoading = ref(false)
const actionMessage = ref('')
// Broadcast
const broadcastMessage = ref('')
const broadcastType = ref('info')
const broadcasting = ref(false)
// Filter
const statusFilter = ref('all')
const searchQuery = ref('')
// Module definitions with pricing
const moduleDefinitions = [
{ id: 'core', name: 'Basis', description: 'Grundfunktionen', icon: '🏠', price: 0, included: true },
{ id: 'orders', name: 'Aufträge', description: 'Auftragsverwaltung', icon: '📋', price: 0, included: true },
{ id: 'availability', name: 'Verfügbarkeit', description: 'Einsatzplanung', icon: '🗓️', price: 0, included: true },
{ id: 'timesheets', name: 'Stundenzettel', description: 'Zeiterfassung', icon: '⏱️', price: 0, included: true },
{ id: 'users', name: 'Benutzer', description: 'Mitarbeiterverwaltung', icon: '👥', price: 0, included: true },
{ id: 'qualifications', name: 'Qualifikationen', description: '§34a Tracking', icon: '🎓', price: 19, included: false },
{ id: 'objects', name: 'Objekte', description: 'Standortverwaltung', icon: '🏢', price: 0, included: true },
{ id: 'shifts', name: 'Schichten', description: 'Dienstplanung', icon: '📅', price: 29, included: false },
{ id: 'patrols', name: 'Rundgänge', description: 'Kontrollgänge', icon: '📍', price: 29, included: false },
{ id: 'incidents', name: 'Vorfälle', description: 'Vorfallserfassung', icon: '🚨', price: 19, included: false },
{ id: 'vehicles', name: 'Fahrzeuge', description: 'Fuhrparkverwaltung', icon: '🚗', price: 19, included: false },
{ id: 'customers', name: 'Kunden', description: 'Kundenstamm', icon: '🤝', price: 0, included: true },
{ id: 'billing', name: 'Abrechnung', description: 'Rechnungserstellung', icon: '💰', price: 49, included: false },
{ id: 'documents', name: 'Dokumente', description: 'Dateiverwaltung', icon: '📁', price: 9, included: false },
{ id: 'partnerships', name: 'Partnerschaften', description: 'Subunternehmer', icon: '🤝', price: 39, included: false },
]
// Filtered organizations
const filteredOrganizations = computed(() => {
let filtered = organizations.value
if (statusFilter.value !== 'all') {
filtered = filtered.filter(org => org.subscription_status === statusFilter.value)
}
if (searchQuery.value) {
const query = searchQuery.value.toLowerCase()
filtered = filtered.filter(org =>
org.name?.toLowerCase().includes(query) ||
org.slug?.toLowerCase().includes(query)
)
}
return filtered
})
onMounted(async () => {
await loadDashboard()
@@ -49,54 +96,145 @@ async function loadDashboard() {
}
}
async function viewOrg(org: any) {
async function openOrgModal(org: any, action: 'details' | 'subscription' | 'modules' = 'details') {
selectedOrg.value = org
orgAction.value = action
showOrgModal.value = true
actionMessage.value = ''
try {
const res = await api.get<any>(`/admin/organizations/${org.id}`)
orgDetails.value = res.data
const [detailsRes, modulesRes] = await Promise.all([
api.get<any>(`/admin/organizations/${org.id}`),
api.get<any>(`/admin/organizations/${org.id}/modules`)
])
orgDetails.value = detailsRes.data
orgModules.value = modulesRes.data.modules
} catch (e) {
console.error('Load org details failed:', e)
}
}
async function impersonate(userId: string) {
if (!confirm('Als dieser User einloggen?')) return
function closeOrgModal() {
showOrgModal.value = false
selectedOrg.value = null
orgDetails.value = null
orgModules.value = []
}
// Subscription Actions
async function setOrgFree(reason: string = '') {
if (!selectedOrg.value) return
actionLoading.value = true
try {
const res = await api.post<any>(`/admin/impersonate/${userId}`)
await api.post(`/admin/organizations/${selectedOrg.value.id}/set-free`, { reason: reason || 'Freigestellt' })
actionMessage.value = '✅ Organisation freigestellt'
selectedOrg.value.subscription_status = 'free'
await loadDashboard()
} catch (e) {
actionMessage.value = '❌ Fehler: ' + (e instanceof Error ? e.message : 'Unbekannt')
} finally {
actionLoading.value = false
}
}
async function pauseOrg(reason: string = '') {
if (!selectedOrg.value) return
actionLoading.value = true
try {
await api.post(`/admin/organizations/${selectedOrg.value.id}/pause`, { reason: reason || 'Zahlung ausstehend' })
actionMessage.value = '✅ Organisation pausiert'
selectedOrg.value.subscription_status = 'paused'
await loadDashboard()
} catch (e) {
actionMessage.value = '❌ Fehler: ' + (e instanceof Error ? e.message : 'Unbekannt')
} finally {
actionLoading.value = false
}
}
async function activateOrg(days: number = 30, plan: string = 'business') {
if (!selectedOrg.value) return
actionLoading.value = true
try {
await api.post(`/admin/organizations/${selectedOrg.value.id}/activate`, { days, plan })
actionMessage.value = `✅ Organisation aktiviert (${days} Tage ${plan})`
selectedOrg.value.subscription_status = 'active'
await loadDashboard()
} catch (e) {
actionMessage.value = '❌ Fehler: ' + (e instanceof Error ? e.message : 'Unbekannt')
} finally {
actionLoading.value = false
}
}
async function extendTrial(days: number = 14) {
if (!selectedOrg.value) return
actionLoading.value = true
try {
await api.post(`/admin/organizations/${selectedOrg.value.id}/extend-trial`, { days })
actionMessage.value = `✅ Trial um ${days} Tage verlängert`
selectedOrg.value.subscription_status = 'trial'
await loadDashboard()
} catch (e) {
actionMessage.value = '❌ Fehler: ' + (e instanceof Error ? e.message : 'Unbekannt')
} finally {
actionLoading.value = false
}
}
async function removeFree() {
if (!selectedOrg.value) return
actionLoading.value = true
try {
await api.post(`/admin/organizations/${selectedOrg.value.id}/remove-free`, { set_trial_days: 14 })
actionMessage.value = '✅ Freistellung aufgehoben'
selectedOrg.value.subscription_status = 'trial'
await loadDashboard()
} catch (e) {
actionMessage.value = '❌ Fehler: ' + (e instanceof Error ? e.message : 'Unbekannt')
} finally {
actionLoading.value = false
}
}
// Module Actions
async function toggleModule(moduleId: string, enabled: boolean) {
if (!selectedOrg.value) return
try {
await api.put(`/admin/organizations/${selectedOrg.value.id}/modules/${moduleId}`, { enabled })
// Store token and redirect
// Update local state
const mod = orgModules.value.find(m => m.id === moduleId)
if (mod) mod.enabled = enabled
actionMessage.value = enabled ? `${moduleId} aktiviert` : `${moduleId} deaktiviert`
} catch (e) {
actionMessage.value = '❌ Fehler: ' + (e instanceof Error ? e.message : 'Unbekannt')
}
}
async function impersonate(orgId: string) {
if (!confirm('Als Chef dieser Organisation einloggen?')) return
try {
const res = await api.post<any>(`/admin/impersonate/${orgId}`)
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
alert('Impersonation fehlgeschlagen')
}
}
// Helpers
function formatDate(date: string) {
if (!date) return '-'
return new Date(date).toLocaleDateString('de-DE', {
day: '2-digit',
month: '2-digit',
@@ -104,14 +242,41 @@ function formatDate(date: string) {
})
}
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'
function daysUntil(date: string) {
if (!date) return null
const diff = new Date(date).getTime() - Date.now()
return Math.ceil(diff / (1000 * 60 * 60 * 24))
}
function getStatusBadge(status: string) {
const badges: Record<string, string> = {
free: 'bg-green-100 text-green-800',
active: 'bg-blue-100 text-blue-800',
trial: 'bg-yellow-100 text-yellow-800',
paused: 'bg-red-100 text-red-800',
expired: 'bg-gray-100 text-gray-800',
}
return plans[plan] || plans.free
return badges[status] || badges.expired
}
function getStatusLabel(status: string) {
const labels: Record<string, string> = {
free: '🎁 Freigestellt',
active: '✅ Aktiv',
trial: '🧪 Trial',
paused: '⏸️ Pausiert',
expired: '❌ Abgelaufen',
}
return labels[status] || status
}
function getPlanBadge(plan: string) {
const badges: Record<string, string> = {
starter: 'bg-gray-100 text-gray-700',
business: 'bg-purple-100 text-purple-700',
enterprise: 'bg-yellow-100 text-yellow-700',
}
return badges[plan] || badges.starter
}
</script>
@@ -123,7 +288,7 @@ function getPlanBadge(plan: string) {
<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
SeCu Super-Admin Panel
</h1>
</div>
<router-link to="/" class="btn btn-secondary text-sm">
@@ -145,115 +310,279 @@ function getPlanBadge(plan: string) {
<!-- 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">
<!-- Tabs -->
<div class="flex gap-2 border-b dark:border-gray-700 pb-4">
<button
@click="activeTab = 'overview'"
:class="['px-4 py-2 rounded-lg font-medium transition',
activeTab === 'overview' ? 'bg-purple-600 text-white' : 'bg-white dark:bg-gray-800 hover:bg-gray-100']"
>
📊 Übersicht
</button>
<button
@click="activeTab = 'organizations'"
:class="['px-4 py-2 rounded-lg font-medium transition',
activeTab === 'organizations' ? 'bg-purple-600 text-white' : 'bg-white dark:bg-gray-800 hover:bg-gray-100']"
>
🏢 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>
</button>
<button
@click="activeTab = 'modules'"
:class="['px-4 py-2 rounded-lg font-medium transition',
activeTab === 'modules' ? 'bg-purple-600 text-white' : 'bg-white dark:bg-gray-800 hover:bg-gray-100']"
>
🧩 Modul-Preise
</button>
</div>
<!-- Overview Tab -->
<div v-if="activeTab === 'overview'" class="space-y-6">
<!-- Stats -->
<div class="grid grid-cols-2 md:grid-cols-4 gap-4">
<div class="card text-center">
<div class="text-3xl font-bold text-purple-600">{{ stats.org_count }}</div>
<div class="text-sm text-gray-500">Organisationen</div>
</div>
<div class="card text-center">
<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 text-center">
<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 text-center">
<div class="text-3xl font-bold text-orange-600">{{ stats.timesheet_count }}</div>
<div class="text-sm text-gray-500">Stundenzettel</div>
</div>
</div>
<!-- Status Overview -->
<div class="card">
<h2 class="text-lg font-semibold mb-4">Subscription Status</h2>
<div class="grid grid-cols-2 md:grid-cols-5 gap-4">
<div class="p-4 rounded-lg bg-green-50 text-center">
<div class="text-2xl font-bold text-green-600">
{{ organizations.filter(o => o.subscription_status === 'free').length }}
</div>
<div class="text-sm text-green-700">Freigestellt</div>
</div>
<div class="p-4 rounded-lg bg-blue-50 text-center">
<div class="text-2xl font-bold text-blue-600">
{{ organizations.filter(o => o.subscription_status === 'active').length }}
</div>
<div class="text-sm text-blue-700">Aktiv</div>
</div>
<div class="p-4 rounded-lg bg-yellow-50 text-center">
<div class="text-2xl font-bold text-yellow-600">
{{ organizations.filter(o => o.subscription_status === 'trial').length }}
</div>
<div class="text-sm text-yellow-700">Trial</div>
</div>
<div class="p-4 rounded-lg bg-red-50 text-center">
<div class="text-2xl font-bold text-red-600">
{{ organizations.filter(o => o.subscription_status === 'paused').length }}
</div>
<div class="text-sm text-red-700">Pausiert</div>
</div>
<div class="p-4 rounded-lg bg-gray-100 text-center">
<div class="text-2xl font-bold text-gray-600">
{{ organizations.filter(o => o.subscription_status === 'expired' || (!o.subscription_status)).length }}
</div>
<div class="text-sm text-gray-700">Abgelaufen</div>
</div>
</div>
</div>
</div>
<!-- Organizations Tab -->
<div v-if="activeTab === 'organizations'" class="space-y-4">
<!-- Filters -->
<div class="card">
<div class="flex flex-wrap gap-4">
<div class="flex-1 min-w-[200px]">
<input
v-model="searchQuery"
type="text"
placeholder="🔍 Organisation suchen..."
class="input w-full"
/>
</div>
<select v-model="statusFilter" class="input w-40">
<option value="all">Alle Status</option>
<option value="free">🎁 Freigestellt</option>
<option value="active"> Aktiv</option>
<option value="trial">🧪 Trial</option>
<option value="paused"> Pausiert</option>
</select>
</div>
</div>
<!-- Organizations Table -->
<div class="card overflow-hidden">
<div class="overflow-x-auto">
<table class="w-full">
<thead class="text-left text-sm text-gray-500 border-b dark:border-gray-700 bg-gray-50 dark:bg-gray-800">
<tr>
<th class="px-4 py-3 font-medium">Organisation</th>
<th class="px-4 py-3 font-medium">Status</th>
<th class="px-4 py-3 font-medium">Plan</th>
<th class="px-4 py-3 font-medium">Läuft ab</th>
<th class="px-4 py-3 font-medium">Users</th>
<th class="px-4 py-3 font-medium">Aktionen</th>
</tr>
</thead>
<tbody class="divide-y dark:divide-gray-700">
<tr
v-for="org in filteredOrganizations"
:key="org.id"
class="hover:bg-gray-50 dark:hover:bg-gray-800"
>
<td class="px-4 py-3">
<div class="font-medium text-gray-900 dark:text-white">{{ org.name }}</div>
<div class="text-sm text-gray-500">{{ org.slug }}</div>
</td>
<td class="px-4 py-3">
<span :class="['px-2 py-1 rounded-full text-xs font-medium', getStatusBadge(org.subscription_status)]">
{{ getStatusLabel(org.subscription_status) }}
</span>
</td>
<td class="px-4 py-3">
<span :class="['px-2 py-1 rounded text-xs font-medium', getPlanBadge(org.subscription_plan)]">
{{ org.subscription_plan || 'starter' }}
</span>
</td>
<td class="px-4 py-3 text-sm">
<template v-if="org.subscription_status === 'free'">
<span class="text-green-600"> Unbegrenzt</span>
</template>
<template v-else-if="org.subscription_status === 'trial'">
<span :class="daysUntil(org.trial_ends_at) <= 3 ? 'text-red-600' : 'text-yellow-600'">
{{ daysUntil(org.trial_ends_at) }} Tage
</span>
</template>
<template v-else-if="org.subscription_ends_at">
{{ formatDate(org.subscription_ends_at) }}
</template>
<template v-else>
-
</template>
</td>
<td class="px-4 py-3 text-sm text-gray-500">
{{ org.user_count }}
</td>
<td class="px-4 py-3">
<div class="flex gap-2">
<button
@click="openOrgModal(org, 'subscription')"
class="text-xs px-2 py-1 bg-purple-100 text-purple-700 rounded hover:bg-purple-200"
>
💳 Abo
</button>
<button
@click="openOrgModal(org, 'modules')"
class="text-xs px-2 py-1 bg-blue-100 text-blue-700 rounded hover:bg-blue-200"
>
🧩 Module
</button>
<button
@click="openOrgModal(org, 'details')"
class="text-xs px-2 py-1 bg-gray-100 text-gray-700 rounded hover:bg-gray-200"
>
👁
</button>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- Modules Tab -->
<div v-if="activeTab === 'modules'" class="space-y-4">
<div class="card">
<h2 class="text-lg font-semibold mb-4">🧩 Standard-Module & Preise</h2>
<p class="text-sm text-gray-500 mb-6">
Diese Module können pro Organisation aktiviert/deaktiviert werden. Premium-Module kosten extra.
</p>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<div
v-for="mod in moduleDefinitions"
:key="mod.id"
:class="['p-4 rounded-lg border-2', mod.included ? 'border-green-200 bg-green-50' : 'border-purple-200 bg-purple-50']"
>
<div class="flex items-start justify-between">
<div>
<span class="text-2xl">{{ mod.icon }}</span>
<h3 class="font-semibold mt-1">{{ mod.name }}</h3>
<p class="text-sm text-gray-600">{{ mod.description }}</p>
</div>
<div class="text-right">
<div v-if="mod.included" class="text-green-600 font-bold">
Inklusive
</div>
<div v-else class="text-purple-600 font-bold">
+{{ mod.price }}/Mo
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</main>
<!-- Org Details Modal -->
<!-- Organization 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="bg-white dark:bg-gray-800 rounded-xl shadow-xl max-w-3xl w-full max-h-[90vh] overflow-y-auto">
<div class="p-6">
<!-- Header -->
<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">
<div>
<h2 class="text-xl font-bold text-gray-900 dark:text-white">
{{ selectedOrg?.name }}
</h2>
<p class="text-sm text-gray-500">{{ selectedOrg?.slug }}</p>
</div>
<button @click="closeOrgModal" class="text-gray-400 hover:text-gray-600 text-2xl">
</button>
</div>
<div v-if="orgDetails" class="space-y-6">
<!-- Stats -->
<div class="grid grid-cols-3 gap-4">
<!-- Modal Tabs -->
<div class="flex gap-2 mb-6 border-b pb-4">
<button
@click="orgAction = 'details'"
:class="['px-3 py-1.5 rounded text-sm font-medium', orgAction === 'details' ? 'bg-gray-900 text-white' : 'bg-gray-100']"
>
👁 Details
</button>
<button
@click="orgAction = 'subscription'"
:class="['px-3 py-1.5 rounded text-sm font-medium', orgAction === 'subscription' ? 'bg-gray-900 text-white' : 'bg-gray-100']"
>
💳 Subscription
</button>
<button
@click="orgAction = 'modules'"
:class="['px-3 py-1.5 rounded text-sm font-medium', orgAction === 'modules' ? 'bg-gray-900 text-white' : 'bg-gray-100']"
>
🧩 Module
</button>
</div>
<!-- Action Message -->
<div v-if="actionMessage" class="mb-4 p-3 rounded-lg bg-gray-100 text-sm">
{{ actionMessage }}
</div>
<!-- Details View -->
<div v-if="orgAction === 'details' && orgDetails">
<div class="grid grid-cols-3 gap-4 mb-6">
<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>
@@ -263,42 +592,180 @@ function getPlanBadge(plan: string) {
<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 class="text-2xl font-bold">{{ orgDetails.stats?.enabled_modules || 0 }}</div>
<div class="text-sm text-gray-500">Module</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>
<h3 class="font-semibold mb-3">Benutzer</h3>
<div class="space-y-2 max-h-60 overflow-y-auto">
<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">{{ user.first_name }} {{ user.last_name }}</div>
<div class="text-sm text-gray-500">{{ user.email }} {{ user.role }}</div>
</div>
</div>
</div>
<div class="mt-6 pt-4 border-t">
<button
@click="impersonate(selectedOrg.id)"
class="btn btn-secondary text-sm"
>
🎭 Als Chef einloggen
</button>
</div>
</div>
<div v-else class="text-center py-8 text-gray-500">
Lädt...
<!-- Subscription View -->
<div v-if="orgAction === 'subscription'">
<div class="mb-6 p-4 rounded-lg bg-gray-50">
<div class="grid grid-cols-2 gap-4 text-sm">
<div>
<span class="text-gray-500">Status:</span>
<span :class="['ml-2 px-2 py-0.5 rounded text-xs font-medium', getStatusBadge(selectedOrg.subscription_status)]">
{{ getStatusLabel(selectedOrg.subscription_status) }}
</span>
</div>
<div>
<span class="text-gray-500">Plan:</span>
<span class="ml-2 font-medium">{{ selectedOrg.subscription_plan || 'starter' }}</span>
</div>
<div v-if="selectedOrg.trial_ends_at">
<span class="text-gray-500">Trial endet:</span>
<span class="ml-2">{{ formatDate(selectedOrg.trial_ends_at) }}</span>
</div>
<div v-if="selectedOrg.subscription_ends_at">
<span class="text-gray-500">Abo endet:</span>
<span class="ml-2">{{ formatDate(selectedOrg.subscription_ends_at) }}</span>
</div>
</div>
</div>
<h3 class="font-semibold mb-4">Aktionen</h3>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3">
<!-- Set Free -->
<button
@click="setOrgFree('Partner')"
:disabled="actionLoading || selectedOrg.subscription_status === 'free'"
class="p-4 rounded-lg border-2 border-green-200 bg-green-50 hover:bg-green-100 text-left disabled:opacity-50"
>
<div class="font-semibold text-green-700">🎁 Freistellen</div>
<div class="text-sm text-green-600">Kostenlos, kein Ablauf</div>
</button>
<!-- Remove Free -->
<button
@click="removeFree()"
:disabled="actionLoading || selectedOrg.subscription_status !== 'free'"
class="p-4 rounded-lg border-2 border-gray-200 bg-gray-50 hover:bg-gray-100 text-left disabled:opacity-50"
>
<div class="font-semibold text-gray-700"> Freistellung aufheben</div>
<div class="text-sm text-gray-600">Zurück zu Trial</div>
</button>
<!-- Activate -->
<button
@click="activateOrg(30, 'business')"
:disabled="actionLoading"
class="p-4 rounded-lg border-2 border-blue-200 bg-blue-50 hover:bg-blue-100 text-left disabled:opacity-50"
>
<div class="font-semibold text-blue-700"> Aktivieren (30 Tage)</div>
<div class="text-sm text-blue-600">Business Plan</div>
</button>
<!-- Activate 1 Year -->
<button
@click="activateOrg(365, 'enterprise')"
:disabled="actionLoading"
class="p-4 rounded-lg border-2 border-purple-200 bg-purple-50 hover:bg-purple-100 text-left disabled:opacity-50"
>
<div class="font-semibold text-purple-700"> Aktivieren (1 Jahr)</div>
<div class="text-sm text-purple-600">Enterprise Plan</div>
</button>
<!-- Extend Trial -->
<button
@click="extendTrial(14)"
:disabled="actionLoading"
class="p-4 rounded-lg border-2 border-yellow-200 bg-yellow-50 hover:bg-yellow-100 text-left disabled:opacity-50"
>
<div class="font-semibold text-yellow-700"> Trial +14 Tage</div>
<div class="text-sm text-yellow-600">Verlängern</div>
</button>
<!-- Pause -->
<button
@click="pauseOrg('Zahlung ausstehend')"
:disabled="actionLoading || selectedOrg.subscription_status === 'paused'"
class="p-4 rounded-lg border-2 border-red-200 bg-red-50 hover:bg-red-100 text-left disabled:opacity-50"
>
<div class="font-semibold text-red-700"> Pausieren</div>
<div class="text-sm text-red-600">Zugang sperren</div>
</button>
</div>
</div>
<!-- Modules View -->
<div v-if="orgAction === 'modules'">
<div class="space-y-3">
<div
v-for="mod in moduleDefinitions"
:key="mod.id"
class="flex items-center justify-between p-4 rounded-lg border bg-white dark:bg-gray-700"
>
<div class="flex items-center gap-3">
<span class="text-2xl">{{ mod.icon }}</span>
<div>
<div class="font-medium">{{ mod.name }}</div>
<div class="text-sm text-gray-500">{{ mod.description }}</div>
</div>
</div>
<div class="flex items-center gap-4">
<span v-if="mod.price > 0" class="text-sm text-purple-600 font-medium">
+{{ mod.price }}
</span>
<span v-else class="text-sm text-green-600">
Inklusive
</span>
<label class="relative inline-flex items-center cursor-pointer">
<input
type="checkbox"
:checked="orgModules.find(m => m.name === mod.id)?.enabled ?? mod.included"
@change="toggleModule(mod.id, ($event.target as HTMLInputElement).checked)"
class="sr-only peer"
>
<div class="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-purple-300 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-purple-600"></div>
</label>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<style scoped>
.card {
@apply bg-white dark:bg-gray-800 rounded-xl shadow-sm p-6;
}
.btn {
@apply px-4 py-2 rounded-lg font-medium transition;
}
.btn-primary {
@apply bg-purple-600 text-white hover:bg-purple-700;
}
.btn-secondary {
@apply bg-gray-200 text-gray-700 hover:bg-gray-300;
}
.input {
@apply border border-gray-300 dark:border-gray-600 rounded-lg px-4 py-2 focus:ring-2 focus:ring-purple-500 focus:border-transparent bg-white dark:bg-gray-700;
}
</style>