412 lines
14 KiB
Vue
412 lines
14 KiB
Vue
<template>
|
|
<div class="admin-dashboard">
|
|
<!-- Page Header -->
|
|
<div class="mb-8">
|
|
<h1 class="text-3xl font-bold mb-2">Admin Dashboard</h1>
|
|
<p class="text-gray-400">Monitor system health, manage translations, and oversee platform operations.</p>
|
|
</div>
|
|
|
|
<!-- Stats Cards -->
|
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
|
|
<div class="bg-gray-800/50 rounded-xl border border-gray-700 p-6">
|
|
<div class="flex items-center justify-between">
|
|
<div>
|
|
<div class="text-sm text-gray-400 mb-1">Total Users</div>
|
|
<div class="text-3xl font-bold">{{ stats.totalUsers }}</div>
|
|
</div>
|
|
<div class="w-12 h-12 rounded-lg bg-blue-900/30 flex items-center justify-center">
|
|
<span class="text-2xl">👥</span>
|
|
</div>
|
|
</div>
|
|
<div class="mt-4 text-sm text-gray-400">
|
|
<span class="text-green-400">+{{ stats.newUsersToday }} today</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="bg-gray-800/50 rounded-xl border border-gray-700 p-6">
|
|
<div class="flex items-center justify-between">
|
|
<div>
|
|
<div class="text-sm text-gray-400 mb-1">Active Sessions</div>
|
|
<div class="text-3xl font-bold">{{ stats.activeSessions }}</div>
|
|
</div>
|
|
<div class="w-12 h-12 rounded-lg bg-green-900/30 flex items-center justify-center">
|
|
<span class="text-2xl">🔌</span>
|
|
</div>
|
|
</div>
|
|
<div class="mt-4 text-sm text-gray-400">
|
|
<span class="text-green-400">{{ stats.sessionGrowth }}% growth</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="bg-gray-800/50 rounded-xl border border-gray-700 p-6">
|
|
<div class="flex items-center justify-between">
|
|
<div>
|
|
<div class="text-sm text-gray-400 mb-1">Vehicles Catalog</div>
|
|
<div class="text-3xl font-bold">{{ stats.vehicleCount }}</div>
|
|
</div>
|
|
<div class="w-12 h-12 rounded-lg bg-purple-900/30 flex items-center justify-center">
|
|
<span class="text-2xl">🚗</span>
|
|
</div>
|
|
</div>
|
|
<div class="mt-4 text-sm text-gray-400">
|
|
<span class="text-blue-400">{{ stats.pendingVerification }} pending</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="bg-gray-800/50 rounded-xl border border-gray-700 p-6">
|
|
<div class="flex items-center justify-between">
|
|
<div>
|
|
<div class="text-sm text-gray-400 mb-1">API Health</div>
|
|
<div class="text-3xl font-bold">{{ stats.apiLatency }}ms</div>
|
|
</div>
|
|
<div class="w-12 h-12 rounded-lg bg-yellow-900/30 flex items-center justify-center">
|
|
<span class="text-2xl">⚡</span>
|
|
</div>
|
|
</div>
|
|
<div class="mt-4 text-sm text-gray-400">
|
|
<span :class="stats.apiStatus === 'healthy' ? 'text-green-400' : 'text-red-400'">
|
|
{{ stats.apiStatus }}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Two Column Layout -->
|
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
|
<!-- i18n Translations Manager -->
|
|
<div class="bg-gray-800/50 rounded-xl border border-gray-700 p-6">
|
|
<div class="flex items-center justify-between mb-6">
|
|
<h2 class="text-xl font-bold">i18n Translation Manager</h2>
|
|
<div class="flex space-x-2">
|
|
<button
|
|
class="px-4 py-2 bg-gray-700 hover:bg-gray-600 rounded-lg transition-colors"
|
|
@click="refreshTranslations"
|
|
>
|
|
Refresh
|
|
</button>
|
|
<button
|
|
class="px-4 py-2 bg-blue-600 hover:bg-blue-700 rounded-lg transition-colors"
|
|
@click="showAddModal = true"
|
|
>
|
|
Add Key
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Language Selector -->
|
|
<div class="mb-6">
|
|
<label class="block text-sm font-medium text-gray-400 mb-2">Select Language</label>
|
|
<div class="flex space-x-2">
|
|
<button
|
|
v-for="lang in languages"
|
|
:key="lang.code"
|
|
class="px-4 py-2 rounded-lg transition-colors"
|
|
:class="selectedLanguage === lang.code
|
|
? 'bg-blue-600 text-white'
|
|
: 'bg-gray-700 text-gray-300 hover:bg-gray-600'"
|
|
@click="selectedLanguage = lang.code"
|
|
>
|
|
{{ lang.name }}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Translations Table -->
|
|
<div class="overflow-x-auto">
|
|
<table class="w-full">
|
|
<thead>
|
|
<tr class="border-b border-gray-700">
|
|
<th class="text-left py-3 px-4 text-sm font-medium text-gray-400">Key</th>
|
|
<th class="text-left py-3 px-4 text-sm font-medium text-gray-400">Translation</th>
|
|
<th class="text-left py-3 px-4 text-sm font-medium text-gray-400">Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr
|
|
v-for="(translation, key) in filteredTranslations"
|
|
:key="key"
|
|
class="border-b border-gray-800 hover:bg-gray-800/30 transition-colors"
|
|
>
|
|
<td class="py-3 px-4 font-mono text-sm">{{ key }}</td>
|
|
<td class="py-3 px-4">
|
|
<input
|
|
v-model="translationEdits[key]"
|
|
class="w-full bg-gray-900 border border-gray-700 rounded px-3 py-2 text-sm"
|
|
:placeholder="translation"
|
|
@change="saveTranslation(key, translationEdits[key])"
|
|
/>
|
|
</td>
|
|
<td class="py-3 px-4">
|
|
<button
|
|
class="px-3 py-1 bg-red-900/30 hover:bg-red-900/50 text-red-400 rounded text-sm"
|
|
@click="deleteTranslation(key)"
|
|
>
|
|
Delete
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<div v-if="Object.keys(filteredTranslations).length === 0" class="text-center py-8 text-gray-500">
|
|
No translations found for {{ selectedLanguage.toUpperCase() }}.
|
|
</div>
|
|
</div>
|
|
|
|
<!-- System Activity & Quick Actions -->
|
|
<div class="space-y-8">
|
|
<!-- Recent Activity -->
|
|
<div class="bg-gray-800/50 rounded-xl border border-gray-700 p-6">
|
|
<h2 class="text-xl font-bold mb-6">Recent System Activity</h2>
|
|
<div class="space-y-4">
|
|
<div
|
|
v-for="activity in recentActivity"
|
|
:key="activity.id"
|
|
class="flex items-start space-x-4 p-3 rounded-lg hover:bg-gray-800/30 transition-colors"
|
|
>
|
|
<div class="w-10 h-10 rounded-full bg-gray-700 flex items-center justify-center">
|
|
<span class="text-lg">{{ activity.icon }}</span>
|
|
</div>
|
|
<div class="flex-1">
|
|
<div class="font-medium">{{ activity.title }}</div>
|
|
<div class="text-sm text-gray-400">{{ activity.description }}</div>
|
|
<div class="text-xs text-gray-500 mt-1">{{ activity.time }}</div>
|
|
</div>
|
|
<div :class="`px-3 py-1 rounded-full text-xs ${activity.statusClass}`">
|
|
{{ activity.status }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Quick Actions -->
|
|
<div class="bg-gray-800/50 rounded-xl border border-gray-700 p-6">
|
|
<h2 class="text-xl font-bold mb-6">Quick Actions</h2>
|
|
<div class="grid grid-cols-2 gap-4">
|
|
<button
|
|
class="p-4 bg-gray-700 hover:bg-gray-600 rounded-xl transition-colors text-left"
|
|
@click="runSystemAudit"
|
|
>
|
|
<div class="text-2xl mb-2">🔍</div>
|
|
<div class="font-medium">Run System Audit</div>
|
|
<div class="text-sm text-gray-400">Check for inconsistencies</div>
|
|
</button>
|
|
<button
|
|
class="p-4 bg-gray-700 hover:bg-gray-600 rounded-xl transition-colors text-left"
|
|
@click="clearCache"
|
|
>
|
|
<div class="text-2xl mb-2">🧹</div>
|
|
<div class="font-medium">Clear Cache</div>
|
|
<div class="text-sm text-gray-400">Refresh all caches</div>
|
|
</button>
|
|
<button
|
|
class="p-4 bg-gray-700 hover:bg-gray-600 rounded-xl transition-colors text-left"
|
|
@click="exportData"
|
|
>
|
|
<div class="text-2xl mb-2">📤</div>
|
|
<div class="font-medium">Export Data</div>
|
|
<div class="text-sm text-gray-400">Backup database</div>
|
|
</button>
|
|
<button
|
|
class="p-4 bg-gray-700 hover:bg-gray-600 rounded-xl transition-colors text-left"
|
|
@click="showUserImpersonation = true"
|
|
>
|
|
<div class="text-2xl mb-2">👤</div>
|
|
<div class="font-medium">Impersonate User</div>
|
|
<div class="text-sm text-gray-400">Test as another user</div>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Add Translation Modal -->
|
|
<div v-if="showAddModal" class="fixed inset-0 bg-black/70 flex items-center justify-center z-50">
|
|
<div class="bg-gray-800 rounded-xl border border-gray-700 p-6 w-full max-w-md">
|
|
<h3 class="text-xl font-bold mb-4">Add Translation Key</h3>
|
|
<div class="space-y-4">
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-400 mb-2">Key</label>
|
|
<input
|
|
v-model="newKey"
|
|
class="w-full bg-gray-900 border border-gray-700 rounded px-3 py-2"
|
|
placeholder="e.g., dashboard.welcome"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-400 mb-2">Value</label>
|
|
<textarea
|
|
v-model="newValue"
|
|
class="w-full bg-gray-900 border border-gray-700 rounded px-3 py-2"
|
|
rows="3"
|
|
placeholder="Translation text"
|
|
/>
|
|
</div>
|
|
<div class="flex justify-end space-x-3">
|
|
<button
|
|
class="px-4 py-2 bg-gray-700 hover:bg-gray-600 rounded-lg"
|
|
@click="showAddModal = false"
|
|
>
|
|
Cancel
|
|
</button>
|
|
<button
|
|
class="px-4 py-2 bg-blue-600 hover:bg-blue-700 rounded-lg"
|
|
@click="addTranslation"
|
|
>
|
|
Add Translation
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { ref, computed, onMounted } from 'vue'
|
|
|
|
// Stats data
|
|
const stats = ref({
|
|
totalUsers: 1247,
|
|
newUsersToday: 12,
|
|
activeSessions: 89,
|
|
sessionGrowth: 4.2,
|
|
vehicleCount: 8563,
|
|
pendingVerification: 42,
|
|
apiLatency: 42,
|
|
apiStatus: 'healthy' as 'healthy' | 'degraded' | 'down'
|
|
})
|
|
|
|
// i18n translations
|
|
const languages = ref([
|
|
{ code: 'hu', name: 'Hungarian' },
|
|
{ code: 'en', name: 'English' },
|
|
{ code: 'de', name: 'German' },
|
|
{ code: 'fr', name: 'French' }
|
|
])
|
|
|
|
const selectedLanguage = ref('hu')
|
|
const translations = ref<Record<string, string>>({
|
|
'dashboard.welcome': 'Üdvözöljük az admin felületen',
|
|
'dashboard.stats': 'Statisztikák',
|
|
'users.total': 'Összes felhasználó',
|
|
'vehicles.catalog': 'Jármű katalógus',
|
|
'system.health': 'Rendszer állapot',
|
|
'navigation.home': 'Kezdőlap',
|
|
'navigation.admin': 'Adminisztráció',
|
|
'button.save': 'Mentés',
|
|
'button.cancel': 'Mégse',
|
|
'error.not_found': 'Nem található',
|
|
'success.updated': 'Sikeresen frissítve'
|
|
})
|
|
|
|
const translationEdits = ref<Record<string, string>>({})
|
|
|
|
// Filter translations for selected language (simulated)
|
|
const filteredTranslations = computed(() => {
|
|
// In a real app, this would fetch from backend API
|
|
// For now, we'll just return all translations
|
|
return translations.value
|
|
})
|
|
|
|
// Recent activity
|
|
const recentActivity = ref([
|
|
{
|
|
id: 1,
|
|
icon: '👤',
|
|
title: 'New user registration',
|
|
description: 'John Doe registered via email',
|
|
time: '5 minutes ago',
|
|
status: 'Completed',
|
|
statusClass: 'bg-green-900/30 text-green-400'
|
|
},
|
|
{
|
|
id: 2,
|
|
icon: '🚗',
|
|
title: 'Vehicle catalog update',
|
|
description: 'Robot-1-GB added 12 new vehicle models',
|
|
time: '15 minutes ago',
|
|
status: 'Processing',
|
|
statusClass: 'bg-blue-900/30 text-blue-400'
|
|
},
|
|
{
|
|
id: 3,
|
|
icon: '🔧',
|
|
title: 'System maintenance',
|
|
description: 'Database backup completed',
|
|
time: '1 hour ago',
|
|
status: 'Completed',
|
|
statusClass: 'bg-green-900/30 text-green-400'
|
|
},
|
|
{
|
|
id: 4,
|
|
icon: '⚠️',
|
|
title: 'API rate limit warning',
|
|
description: 'DVLA API approaching daily limit',
|
|
time: '2 hours ago',
|
|
status: 'Warning',
|
|
statusClass: 'bg-yellow-900/30 text-yellow-400'
|
|
}
|
|
])
|
|
|
|
// Modal state
|
|
const showAddModal = ref(false)
|
|
const newKey = ref('')
|
|
const newValue = ref('')
|
|
const showUserImpersonation = ref(false)
|
|
|
|
// Methods
|
|
function refreshTranslations() {
|
|
// Simulate API call
|
|
console.log('Refreshing translations for', selectedLanguage.value)
|
|
// In real app: fetch from /api/v1/translations/hu
|
|
}
|
|
|
|
function saveTranslation(key: string, value: string) {
|
|
translations.value[key] = value
|
|
console.log('Saving translation:', key, value)
|
|
// In real app: POST to backend
|
|
}
|
|
|
|
function deleteTranslation(key: string) {
|
|
if (confirm(`Delete translation key "${key}"?`)) {
|
|
delete translations.value[key]
|
|
console.log('Deleted translation:', key)
|
|
}
|
|
}
|
|
|
|
function addTranslation() {
|
|
if (newKey.value && newValue.value) {
|
|
translations.value[newKey.value] = newValue.value
|
|
newKey.value = ''
|
|
newValue.value = ''
|
|
showAddModal.value = false
|
|
}
|
|
}
|
|
|
|
function runSystemAudit() {
|
|
alert('System audit started. Check logs for results.')
|
|
}
|
|
|
|
function clearCache() {
|
|
alert('Cache cleared successfully.')
|
|
}
|
|
|
|
function exportData() {
|
|
alert('Data export initiated. Download will start shortly.')
|
|
}
|
|
|
|
onMounted(() => {
|
|
// Initialize translation edits
|
|
Object.keys(translations.value).forEach(key => {
|
|
translationEdits.value[key] = translations.value[key]
|
|
})
|
|
})
|
|
</script>
|
|
|
|
<style scoped>
|
|
.admin-dashboard {
|
|
font-family: 'Inter', sans-serif;
|
|
}
|
|
</style> |