604 lines
21 KiB
Vue
604 lines
21 KiB
Vue
<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> |