admin firs step

This commit is contained in:
Roo
2026-03-23 21:43:40 +00:00
parent 309a72cc0b
commit cddcd34ba9
47 changed files with 22698 additions and 19 deletions

View File

@@ -0,0 +1,604 @@
<template>
<v-app>
<!-- App Bar -->
<v-app-bar color="primary" prominent>
<v-app-bar-nav-icon @click="drawer = !drawer"></v-app-bar-nav-icon>
<v-toolbar-title class="text-h5 font-weight-bold">
<v-icon icon="mdi-rocket-launch" class="mr-2"></v-icon>
{{ t('dashboard.title') }}
<v-chip class="ml-2" :color="roleColor" size="small">
{{ userRole }} {{ scopeLevel }}
</v-chip>
</v-toolbar-title>
<v-spacer></v-spacer>
<!-- Language Switcher -->
<v-menu>
<template v-slot:activator="{ props }">
<v-btn icon v-bind="props" class="mr-2">
<v-icon icon="mdi-translate"></v-icon>
</v-btn>
</template>
<v-list>
<v-list-item @click="locale = 'hu'">
<v-list-item-title :class="{ 'font-weight-bold': locale === 'hu' }">
🇭🇺 Magyar
</v-list-item-title>
</v-list-item>
<v-list-item @click="locale = 'en'">
<v-list-item-title :class="{ 'font-weight-bold': locale === 'en' }">
🇬🇧 English
</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
<!-- User Menu -->
<v-menu>
<template v-slot:activator="{ props }">
<v-btn icon v-bind="props">
<v-avatar size="40" color="secondary">
<v-icon icon="mdi-account"></v-icon>
</v-avatar>
</v-btn>
</template>
<v-list>
<v-list-item>
<v-list-item-title class="font-weight-bold">
{{ userEmail }}
</v-list-item-title>
<v-list-item-subtitle>
Rank: {{ userRank }} Scope ID: {{ scopeId }}
</v-list-item-subtitle>
</v-list-item>
<v-divider></v-divider>
<v-list-item @click="navigateTo('/profile')">
<v-list-item-title>
<v-icon icon="mdi-account-cog" class="mr-2"></v-icon>
{{ t('general.settings') }}
</v-list-item-title>
</v-list-item>
<v-list-item @click="logout">
<v-list-item-title class="text-error">
<v-icon icon="mdi-logout" class="mr-2"></v-icon>
{{ t('navigation.logout') }}
</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
</v-app-bar>
<!-- Navigation Drawer -->
<v-navigation-drawer v-model="drawer" temporary>
<v-list>
<v-list-item prepend-icon="mdi-view-dashboard" :title="t('navigation.dashboard')" value="dashboard" @click="navigateTo('/dashboard')"></v-list-item>
<v-list-item prepend-icon="mdi-cog" :title="t('navigation.settings')" value="settings" @click="navigateTo('/settings')"></v-list-item>
<v-list-item prepend-icon="mdi-shield-account" :title="t('navigation.role_management')" value="roles" @click="navigateTo('/roles')"></v-list-item>
<v-list-item prepend-icon="mdi-map" :title="t('navigation.geographical_scopes')" value="scopes" @click="navigateTo('/scopes')"></v-list-item>
</v-list>
<template v-slot:append>
<v-list>
<v-list-item>
<v-list-item-title class="text-caption text-disabled">
Service Finder Admin v{{ appVersion }}
</v-list-item-title>
</v-list-item>
</v-list>
</template>
</v-navigation-drawer>
<!-- Main Content -->
<v-main>
<v-container fluid class="pa-6">
<!-- Welcome Header -->
<v-row class="mb-6">
<v-col cols="12">
<v-card color="primary" variant="tonal" class="pa-4">
<v-card-title class="text-h4 font-weight-bold">
<v-icon icon="mdi-rocket" class="mr-2"></v-icon>
{{ t('dashboard.welcome_title') }}
</v-card-title>
<v-card-subtitle class="text-h6">
{{ t('dashboard.welcome_subtitle', { scopeLevel }) }}
</v-card-subtitle>
<v-card-text>
<v-chip class="mr-2" color="success">
<v-icon icon="mdi-check-circle" class="mr-1"></v-icon>
Authenticated as {{ userRole }}
</v-chip>
<v-chip class="mr-2" color="info">
<v-icon icon="mdi-map-marker" class="mr-1"></v-icon>
Scope: {{ regionCode || 'Global' }}
</v-chip>
<v-chip color="warning">
<v-icon icon="mdi-shield-star" class="mr-1"></v-icon>
Rank: {{ userRank }}
</v-chip>
<!-- Layout Controls -->
<v-btn
v-if="tileStore.isLayoutModified"
class="ml-2"
color="warning"
size="small"
variant="outlined"
@click="restoreDefaultLayout"
:loading="isRestoringLayout"
>
<v-icon icon="mdi-restore" class="mr-1"></v-icon>
Restore Default Layout
</v-btn>
<v-tooltip v-else location="bottom">
<template v-slot:activator="{ props }">
<v-chip
v-bind="props"
class="ml-2"
color="success"
size="small"
variant="outlined"
>
<v-icon icon="mdi-check-all" class="mr-1"></v-icon>
Default Layout Active
</v-chip>
</template>
<span>Your dashboard layout matches the default configuration</span>
</v-tooltip>
</v-card-text>
</v-card>
</v-col>
</v-row>
<!-- Launchpad Section -->
<v-row class="mb-4">
<v-col cols="12">
<div class="d-flex align-center justify-space-between">
<v-card-title class="text-h5 font-weight-bold pa-0">
<v-icon icon="mdi-view-grid" class="mr-2"></v-icon>
Launchpad
</v-card-title>
<v-btn variant="tonal" color="primary" prepend-icon="mdi-cog">
Customize Tiles
</v-btn>
</div>
<v-card-subtitle class="pa-0">
Role-based dashboard with {{ filteredTiles.length }} accessible tiles
</v-card-subtitle>
</v-col>
</v-row>
<!-- Dynamic Tiles Grid with Drag & Drop -->
<Draggable
v-model="draggableTiles"
tag="v-row"
item-key="id"
class="drag-container"
@end="onDragEnd"
:component-data="{ class: 'drag-row' }"
:animation="200"
:ghost-class="'ghost-tile'"
:chosen-class="'chosen-tile'"
>
<template #item="{ element: tile }">
<v-col
cols="12"
sm="6"
md="4"
lg="3"
class="drag-col"
>
<TileWrapper :tile="tile" @click="handleTileClick">
<template #default>
<p class="text-body-2">{{ tile.description }}</p>
<!-- Requirements Badges -->
<div class="mt-2">
<v-chip
v-for="role in tile.requiredRole"
:key="role"
size="x-small"
class="mr-1 mb-1"
variant="outlined"
>
{{ role }}
</v-chip>
<v-chip
v-if="tile.minRank"
size="x-small"
class="mr-1 mb-1"
color="warning"
variant="outlined"
>
Rank {{ tile.minRank }}+
</v-chip>
</div>
<!-- Scope Level Indicator -->
<div v-if="tile.scopeLevel && tile.scopeLevel.length > 0" class="mt-2">
<v-icon icon="mdi-map-marker" size="small" class="mr-1"></v-icon>
<span class="text-caption">
{{ tile.scopeLevel.join(', ') }}
</span>
</div>
</template>
</TileWrapper>
</v-col>
</template>
<!-- Empty State -->
<template #footer v-if="draggableTiles.length === 0">
<v-col cols="12">
<v-card class="pa-8 text-center">
<v-icon icon="mdi-lock" size="64" class="mb-4 text-disabled"></v-icon>
<v-card-title class="text-h5">
No Tiles Available
</v-card-title>
<v-card-text>
Your current role ({{ userRole }}) doesn't have access to any dashboard tiles.
Contact your administrator for additional permissions.
</v-card-text>
</v-card>
</v-col>
</template>
</Draggable>
<!-- Quick Stats -->
<v-row class="mt-8">
<v-col cols="12">
<v-card-title class="text-h5 font-weight-bold pa-0">
<v-icon icon="mdi-chart-line" class="mr-2"></v-icon>
System Health Dashboard
<v-btn
icon="mdi-refresh"
size="small"
variant="text"
class="ml-2"
@click="healthMonitor.refreshAll"
:loading="healthMonitor.loading"
></v-btn>
</v-card-title>
<v-card-subtitle class="pa-0">
Real-time system metrics from health-monitor API
<v-chip
v-if="healthMonitor.metrics"
:color="healthMonitor.systemStatusColor"
size="small"
class="ml-2"
>
<v-icon :icon="healthMonitor.systemStatusIcon" size="small" class="mr-1"></v-icon>
{{ healthMonitor.metrics?.system_status?.toUpperCase() || 'LOADING' }}
</v-chip>
</v-card-subtitle>
</v-col>
<!-- Total Assets -->
<v-col cols="12" md="3">
<v-card class="pa-4">
<v-card-title class="text-h6 d-flex align-center">
<v-icon icon="mdi-database" class="mr-2"></v-icon>
Total Assets
<v-spacer></v-spacer>
<v-progress-circular
v-if="healthMonitor.loading && !healthMonitor.metrics"
indeterminate
size="20"
width="2"
></v-progress-circular>
</v-card-title>
<v-card-text class="text-h4 font-weight-bold text-primary">
{{ healthMonitor.metrics?.total_assets?.toLocaleString() || '--' }}
</v-card-text>
<v-card-subtitle>Vehicles, services, and organizations</v-card-subtitle>
</v-card>
</v-col>
<!-- Total Organizations -->
<v-col cols="12" md="3">
<v-card class="pa-4">
<v-card-title class="text-h6 d-flex align-center">
<v-icon icon="mdi-office-building" class="mr-2"></v-icon>
Organizations
<v-spacer></v-spacer>
<v-progress-circular
v-if="healthMonitor.loading && !healthMonitor.metrics"
indeterminate
size="20"
width="2"
></v-progress-circular>
</v-card-title>
<v-card-text class="text-h4 font-weight-bold text-success">
{{ healthMonitor.metrics?.total_organizations?.toLocaleString() || '--' }}
</v-card-text>
<v-card-subtitle>Registered business entities</v-card-subtitle>
</v-card>
</v-col>
<!-- Critical Alerts -->
<v-col cols="12" md="3">
<v-card class="pa-4">
<v-card-title class="text-h6 d-flex align-center">
<v-icon icon="mdi-alert" class="mr-2"></v-icon>
Critical Alerts (24h)
<v-spacer></v-spacer>
<v-progress-circular
v-if="healthMonitor.loading && !healthMonitor.metrics"
indeterminate
size="20"
width="2"
></v-progress-circular>
</v-card-title>
<v-card-text class="text-h4 font-weight-bold" :class="healthMonitor.metrics?.critical_alerts_24h ? 'text-error' : 'text-info'">
{{ healthMonitor.metrics?.critical_alerts_24h || 0 }}
</v-card-text>
<v-card-subtitle>
<span v-if="healthMonitor.metrics?.critical_alerts_24h">Requires immediate attention</span>
<span v-else>No critical issues</span>
</v-card-subtitle>
</v-card>
</v-col>
<!-- System Uptime -->
<v-col cols="12" md="3">
<v-card class="pa-4">
<v-card-title class="text-h6 d-flex align-center">
<v-icon icon="mdi-heart-pulse" class="mr-2"></v-icon>
System Uptime
<v-spacer></v-spacer>
<v-progress-circular
v-if="healthMonitor.loading && !healthMonitor.metrics"
indeterminate
size="20"
width="2"
></v-progress-circular>
</v-card-title>
<v-card-text class="text-h4 font-weight-bold text-warning">
{{ healthMonitor.formattedUptime }}
</v-card-text>
<v-card-subtitle>
Response: {{ healthMonitor.formattedResponseTime }}
<v-icon
v-if="healthMonitor.metrics?.response_time_ms < 100"
icon="mdi-check"
color="success"
size="small"
class="ml-1"
></v-icon>
<v-icon
v-else-if="healthMonitor.metrics?.response_time_ms < 300"
icon="mdi-alert"
color="warning"
size="small"
class="ml-1"
></v-icon>
<v-icon
v-else
icon="mdi-alert-circle"
color="error"
size="small"
class="ml-1"
></v-icon>
</v-card-subtitle>
</v-card>
</v-col>
</v-row>
<!-- Additional Metrics Row -->
<v-row class="mt-2">
<v-col cols="12" md="4">
<v-card class="pa-4">
<v-card-title class="text-h6">
<v-icon icon="mdi-account-group" class="mr-2"></v-icon>
Active Users
</v-card-title>
<v-card-text class="text-h3 font-weight-bold text-primary">
{{ healthMonitor.metrics?.active_users?.toLocaleString() || '--' }}
</v-card-text>
<v-card-subtitle>Currently logged in users</v-card-subtitle>
</v-card>
</v-col>
<v-col cols="12" md="4">
<v-card class="pa-4">
<v-card-title class="text-h6">
<v-icon icon="mdi-database-export" class="mr-2"></v-icon>
DB Connections
</v-card-title>
<v-card-text class="text-h3 font-weight-bold" :class="getDbConnectionClass(healthMonitor.metrics?.database_connections)">
{{ healthMonitor.metrics?.database_connections || '--' }}
</v-card-text>
<v-card-subtitle>
<span v-if="healthMonitor.metrics?.database_connections > 40" class="text-error">High load</span>
<span v-else-if="healthMonitor.metrics?.database_connections > 20" class="text-warning">Moderate load</span>
<span v-else class="text-success">Normal load</span>
</v-card-subtitle>
</v-card>
</v-col>
<v-col cols="12" md="4">
<v-card class="pa-4">
<v-card-title class="text-h6">
<v-icon icon="mdi-update" class="mr-2"></v-icon>
Last Updated
</v-card-title>
<v-card-text class="text-h5 font-weight-bold text-grey">
{{ healthMonitor.lastUpdated ? formatTime(healthMonitor.lastUpdated) : 'Never' }}
</v-card-text>
<v-card-subtitle>
<v-icon icon="mdi-clock-outline" size="small" class="mr-1"></v-icon>
Auto-refresh every 30s
</v-card-subtitle>
</v-card>
</v-col>
</v-row>
</v-container>
</v-main>
<!-- Footer -->
<v-footer app color="surface" class="px-4">
<v-spacer></v-spacer>
<div class="text-caption text-disabled">
Geographical Scope: {{ regionCode || 'Global' }} •
Last sync: {{ new Date().toLocaleTimeString() }} •
<v-icon icon="mdi-circle-small" class="mx-1" color="success"></v-icon>
All systems operational
</div>
</v-footer>
</v-app>
</template>
<script setup lang="ts">
import { ref, computed, onMounted, watch } from 'vue'
import { useAuthStore } from '~/stores/auth'
import { useRBAC } from '~/composables/useRBAC'
import { useHealthMonitor } from '~/composables/useHealthMonitor'
import { useTileStore } from '~/stores/tiles'
import TileCard from '~/components/TileCard.vue'
import Draggable from 'vuedraggable'
import { useI18n } from 'vue-i18n'
// i18n
const { t, locale } = useI18n()
// State
const drawer = ref(false)
const appVersion = '1.0.0'
const tileStore = useTileStore()
const isRestoringLayout = ref(false)
const draggableTiles = ref<any[]>([])
// Stores and composables
const authStore = useAuthStore()
const rbac = useRBAC()
const healthMonitor = useHealthMonitor()
// Computed properties
const userEmail = computed(() => authStore.user?.email || '')
const userRole = computed(() => authStore.getUserRole || '')
const userRank = computed(() => authStore.getUserRank || 0)
const scopeLevel = computed(() => authStore.getScopeLevel || '')
const regionCode = computed(() => authStore.getRegionCode || '')
const scopeId = computed(() => authStore.getScopeId || 0)
const roleColor = computed(() => rbac.getRoleColor())
const filteredTiles = computed(() => tileStore.visibleTiles)
// Watch for changes to filteredTiles and update draggableTiles
watch(filteredTiles, (newTiles) => {
draggableTiles.value = [...newTiles]
}, { immediate: true })
// Methods
function logout() {
authStore.logout()
navigateTo('/login')
}
// Drag & Drop handling
function onDragEnd() {
const tileIds = draggableTiles.value.map(tile => tile.id)
tileStore.updateTilePositions(tileIds)
}
// Tile click handling
function handleTileClick(tile: any) {
const routes: Record<string, string> = {
'ai-logs': '/ai-logs',
'financial-dashboard': '/finance',
'salesperson-hub': '/sales',
'user-management': '/users',
'service-moderation-map': '/moderation-map',
'gamification-control': '/gamification',
'system-health': '/system'
}
const route = routes[tile.id]
if (route) {
navigateTo(route)
} else {
console.warn(`No route defined for tile: ${tile.id}`)
}
}
// Restore default layout
async function restoreDefaultLayout() {
isRestoringLayout.value = true
try {
tileStore.resetPreferences()
// Show success message
console.log('Layout restored to default')
} catch (error) {
console.error('Failed to restore layout:', error)
} finally {
isRestoringLayout.value = false
}
}
// Helper functions
const getDbConnectionClass = (connections: number | undefined) => {
if (!connections) return 'text-grey'
if (connections > 40) return 'text-error'
if (connections > 20) return 'text-warning'
return 'text-success'
}
const formatTime = (value: any) => {
if (!value) return 'N/A';
try {
const d = new Date(value);
// Check if it's a valid date
if (isNaN(d.getTime())) return String(value);
return d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' });
} catch (e) {
return 'Invalid Time';
}
}
// Lifecycle
onMounted(() => {
console.log('Dashboard mounted for user:', userEmail.value)
// Initialize health monitor data
healthMonitor.initialize()
// Load tile preferences
tileStore.loadPreferences()
})
</script>
<style scoped>
.v-main {
background-color: #f5f5f5;
}
.v-card {
transition: transform 0.2s ease-in-out;
}
.v-card:hover {
transform: translateY(-4px);
}
/* Drag & Drop Styles */
.drag-container {
min-height: 200px;
}
.drag-row {
display: flex;
flex-wrap: wrap;
margin: -12px;
}
.drag-col {
padding: 12px;
}
.ghost-tile {
opacity: 0.5;
background-color: #e0e0e0;
border-radius: 8px;
}
.chosen-tile {
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.2);
transform: scale(1.02);
z-index: 10;
}
</style>