add user privileges & add translations

This commit is contained in:
2026-04-20 19:12:27 +03:00
parent f16a830eb2
commit fc96a95335
17 changed files with 1073 additions and 426 deletions

View File

@@ -2,68 +2,71 @@
<AppLayout>
<!-- Stats Grid -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
<div class="card animate-fade-in">
<div class="card hover:shadow-md transition-shadow">
<div class="flex items-center justify-between">
<div>
<p class="text-sm font-medium text-gray-600">Total Users</p>
<p class="text-3xl font-bold text-gray-900 mt-2">{{ stats.totalUsers }}</p>
</div>
<div class="w-12 h-12 bg-primary-100 rounded-lg flex items-center justify-center">
<div class="w-12 h-12 bg-primary-100 rounded-xl flex items-center justify-center">
<svg class="w-6 h-6 text-primary-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z" />
</svg>
</div>
</div>
<div class="mt-4 flex items-center text-sm">
<span class="text-green-600 font-medium"> 12%</span>
<span class="text-gray-500 ml-2">from last month</span>
<span class="text-green-600 font-medium"> {{ userGrowth }}%</span>
<span class="text-gray-500 ml-2">vs last month</span>
</div>
</div>
<div class="card animate-fade-in" style="animation-delay: 0.1s">
<div class="card hover:shadow-md transition-shadow">
<div class="flex items-center justify-between">
<div>
<p class="text-sm font-medium text-gray-600">Active Sessions</p>
<p class="text-3xl font-bold text-gray-900 mt-2">{{ stats.activeSessions }}</p>
</div>
<div class="w-12 h-12 bg-green-100 rounded-lg flex items-center justify-center">
<div class="w-12 h-12 bg-green-100 rounded-xl flex items-center justify-center">
<svg class="w-6 h-6 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
</svg>
</div>
</div>
<div class="mt-4 flex items-center text-sm">
<span class="text-green-600 font-medium"> 5%</span>
<span class="text-green-600 font-medium"> {{ sessionGrowth }}%</span>
<span class="text-gray-500 ml-2">from last hour</span>
</div>
</div>
<div class="card animate-fade-in" style="animation-delay: 0.2s">
<div class="card hover:shadow-md transition-shadow">
<div class="flex items-center justify-between">
<div>
<p class="text-sm font-medium text-gray-600">System Health</p>
<p class="text-3xl font-bold text-gray-900 mt-2">{{ stats.systemHealth }}%</p>
</div>
<div class="w-12 h-12 bg-blue-100 rounded-lg flex items-center justify-center">
<div class="w-12 h-12 bg-blue-100 rounded-xl flex items-center justify-center">
<svg class="w-6 h-6 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
</svg>
</div>
</div>
<div class="mt-4">
<div class="h-2 bg-gray-200 rounded-full">
<div class="h-2 bg-blue-600 rounded-full" :style="{ width: `${stats.systemHealth}%` }"></div>
<div class="h-2 bg-gray-200 rounded-full overflow-hidden">
<div
class="h-full bg-blue-600 rounded-full transition-all duration-500"
:style="{ width: `${stats.systemHealth}%` }"
></div>
</div>
</div>
</div>
<div class="card animate-fade-in" style="animation-delay: 0.3s">
<div class="card hover:shadow-md transition-shadow">
<div class="flex items-center justify-between">
<div>
<p class="text-sm font-medium text-gray-600">Uptime</p>
<p class="text-3xl font-bold text-gray-900 mt-2">{{ stats.uptime }}</p>
</div>
<div class="w-12 h-12 bg-purple-100 rounded-lg flex items-center justify-center">
<div class="w-12 h-12 bg-purple-100 rounded-xl flex items-center justify-center">
<svg class="w-6 h-6 text-purple-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
@@ -78,10 +81,52 @@
</div>
</div>
<!-- Recent Activity -->
<!-- Charts & Activity -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-8">
<!-- User Activity Chart -->
<div class="card">
<div class="flex justify-between items-center mb-4">
<h3 class="text-lg font-semibold text-gray-900">User Activity (Last 7 days)</h3>
<div class="flex items-center space-x-2">
<button @click="activityPeriod = 'week'" class="text-xs px-2 py-1 rounded" :class="activityPeriod === 'week' ? 'bg-primary-100 text-primary-700' : 'text-gray-500'">Week</button>
<button @click="activityPeriod = 'month'" class="text-xs px-2 py-1 rounded" :class="activityPeriod === 'month' ? 'bg-primary-100 text-primary-700' : 'text-gray-500'">Month</button>
</div>
</div>
<div class="flex items-end space-x-2 h-48">
<div v-for="(value, index) in activityData" :key="index" class="flex-1 flex flex-col items-center">
<div class="w-full bg-primary-100 rounded-t-lg transition-all duration-500" :style="{ height: `${value}%`, minHeight: '4px' }"></div>
<span class="text-xs text-gray-500 mt-2">{{ activityLabels[index] }}</span>
</div>
</div>
</div>
<!-- System Services Status -->
<div class="card">
<h3 class="text-lg font-semibold text-gray-900 mb-4">System Services</h3>
<div class="space-y-4">
<div v-for="service in systemServices" :key="service.name" class="flex items-center justify-between p-3 bg-gray-50 rounded-lg">
<div class="flex items-center space-x-3">
<div :class="['w-2 h-2 rounded-full', service.status === 'up' ? 'bg-green-500' : 'bg-red-500']"></div>
<span class="text-gray-700 font-medium">{{ service.name }}</span>
</div>
<div class="flex items-center space-x-4">
<span class="text-sm text-gray-500">{{ service.latency }}ms</span>
<span class="text-xs px-2 py-1 rounded-full" :class="service.status === 'up' ? 'bg-green-100 text-green-700' : 'bg-red-100 text-red-700'">
{{ service.status === 'up' ? 'Operational' : 'Down' }}
</span>
</div>
</div>
</div>
</div>
</div>
<!-- Recent Users & Restaurants -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<div class="card">
<h3 class="text-lg font-semibold text-gray-900 mb-4">Recent Users</h3>
<div class="flex justify-between items-center mb-4">
<h3 class="text-lg font-semibold text-gray-900">Recent Users</h3>
<router-link to="/users" class="text-sm text-primary-600 hover:text-primary-700">View all </router-link>
</div>
<div class="space-y-3">
<div v-for="user in recentUsers" :key="user.id" class="flex items-center justify-between p-3 hover:bg-gray-50 rounded-lg transition-colors">
<div class="flex items-center space-x-3">
@@ -95,19 +140,33 @@
</div>
<span class="px-2 py-1 text-xs font-medium bg-green-100 text-green-700 rounded-full">New</span>
</div>
<div v-if="recentUsers.length === 0" class="text-center text-gray-500 py-8">No users yet</div>
</div>
</div>
<div class="card">
<h3 class="text-lg font-semibold text-gray-900 mb-4">System Status</h3>
<div class="space-y-4">
<div v-for="service in systemServices" :key="service.name" class="flex items-center justify-between">
<div class="flex justify-between items-center mb-4">
<h3 class="text-lg font-semibold text-gray-900">Recent Restaurants</h3>
<router-link to="/restaurants" class="text-sm text-primary-600 hover:text-primary-700">View all </router-link>
</div>
<div class="space-y-3">
<div v-for="rest in recentRestaurants" :key="rest.id" class="flex items-center justify-between p-3 hover:bg-gray-50 rounded-lg transition-colors">
<div class="flex items-center space-x-3">
<div :class="['w-2 h-2 rounded-full', service.status === 'up' ? 'bg-green-500' : 'bg-red-500']"></div>
<span class="text-gray-700">{{ service.name }}</span>
<div class="w-10 h-10 bg-orange-100 rounded-full flex items-center justify-center">
<svg class="w-5 h-5 text-orange-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4" />
</svg>
</div>
<div>
<p class="font-medium text-gray-900">{{ rest.name }}</p>
<p class="text-sm text-gray-500">{{ rest.host }}</p>
</div>
</div>
<div class="flex items-center space-x-2">
<span class="text-xs text-gray-500">{{ formatDate(rest.created) }}</span>
</div>
<span class="text-sm text-gray-500">{{ service.latency }}ms</span>
</div>
<div v-if="recentRestaurants.length === 0" class="text-center text-gray-500 py-8">No restaurants yet</div>
</div>
</div>
</div>
@@ -115,88 +174,73 @@
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue'
import AppLayout from '../components/Layout/AppLayout.vue'
import { ref, onMounted, onUnmounted } from 'vue';
import AppLayout from '../components/Layout/AppLayout.vue';
const stats = ref({ totalUsers: 0, activeSessions: 0, systemHealth: 100, uptime: '99.9%' })
const stats = ref({ totalUsers: 0, activeSessions: 0, systemHealth: 100, uptime: '99.9%' });
const userGrowth = ref(12);
const sessionGrowth = ref(5);
const recentUsers = ref([]);
const recentRestaurants = ref([]);
const systemServices = ref([]);
const activityPeriod = ref('week');
const activityData = ref([65, 78, 82, 71, 88, 94, 72]);
const activityLabels = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
async function loadStats() {
let interval: number;
async function loadDashboardData() {
try {
const [usersRes, sessionsRes, healthRes] = await Promise.all([
const [usersRes, sessionsRes, healthRes, restaurantsRes] = await Promise.all([
fetch('/api/admin/users'),
fetch('/api/admin/active-sessions'),
fetch('/api/health')
])
const users = await usersRes.json()
const sessions = await sessionsRes.json()
const health = await healthRes.json()
fetch('/api/health'),
fetch('/api/admin/restaurants')
]);
stats.value.totalUsers = users.length
stats.value.activeSessions = sessions.count || 0
const users = await usersRes.json();
const sessions = await sessionsRes.json();
const health = await healthRes.json();
const restaurants = await restaurantsRes.json();
const upCount = health.checks?.filter(c => c.status === 'UP').length || 0
const total = health.checks?.length || 1
stats.value.systemHealth = Math.round((upCount / total) * 100)
} catch (e) { console.error(e) }
}
stats.value.totalUsers = users.length;
stats.value.activeSessions = sessions.count || 0;
recentUsers.value = users.slice(-5).reverse();
recentRestaurants.value = restaurants.slice(-5).reverse();
onMounted(() => {
loadStats()
const interval = setInterval(loadStats, 5000)
onUnmounted(() => clearInterval(interval))
})
const upCount = health.checks?.filter((c: any) => c.status === 'UP').length || 0;
const total = health.checks?.length || 1;
stats.value.systemHealth = Math.round((upCount / total) * 100);
const recentUsers = ref([])
const systemServices = ref([])
async function loadHealth() {
try {
const res = await fetch('/api/health')
const data = await res.json()
if (data.checks) {
systemServices.value = data.checks.map(check => ({
if (health.checks) {
systemServices.value = health.checks.map((check: any) => ({
name: check.data?.name || check.id,
status: check.status.toLowerCase(),
latency: check.data?.latency_ms || 0
}))
}));
}
} catch (e) {
console.error('Health check failed', e)
}
}
let interval: number
onMounted(async () => {
await loadData()
await loadHealth()
interval = window.setInterval(loadHealth, 5000)
})
onUnmounted(() => {
if (interval) clearInterval(interval)
})
onMounted(async () => {
await loadData()
})
async function loadData() {
try {
const res = await fetch('/api/admin/users')
const users = await res.json()
stats.value.totalUsers = users.length
recentUsers.value = users.slice(-5).reverse()
} catch (e) {
console.error('Failed to load data', e)
console.error('Failed to load dashboard data', e);
}
}
function formatDate(dateStr: string) {
return new Date(dateStr).toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
})
if (!dateStr) return '-';
const date = new Date(dateStr);
const now = new Date();
const diffDays = Math.floor((now.getTime() - date.getTime()) / (1000 * 60 * 60 * 24));
if (diffDays === 0) return 'Today';
if (diffDays === 1) return 'Yesterday';
if (diffDays < 7) return `${diffDays} days ago`;
return date.toLocaleDateString();
}
onMounted(() => {
loadDashboardData();
interval = window.setInterval(loadDashboardData, 30000);
});
onUnmounted(() => {
if (interval) clearInterval(interval);
});
</script>