admin_users_personels
This commit is contained in:
@@ -2,114 +2,182 @@
|
||||
<div>
|
||||
<!-- Page Header -->
|
||||
<div class="mb-8">
|
||||
<h1 class="text-2xl font-bold text-white">Dashboard</h1>
|
||||
<p class="text-slate-400 mt-1">ServiceFinder administration overview</p>
|
||||
<h1 class="text-2xl font-bold text-white">{{ t('dashboard.title') }}</h1>
|
||||
<p class="text-slate-400 mt-1">{{ t('dashboard.subtitle') }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Stat Tiles Grid -->
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-4 gap-6 mb-8">
|
||||
<!-- Active Users -->
|
||||
<div class="bg-slate-800 rounded-xl border border-slate-700 p-6 hover:border-indigo-500/50 transition group">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<div class="p-2 rounded-lg bg-indigo-500/10 text-indigo-400 group-hover:bg-indigo-500/20 transition">
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<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.5-9a2.25 2.25 0 11-4.5 0 2.25 2.25 0 014.5 0z" />
|
||||
</svg>
|
||||
</div>
|
||||
<span class="text-xs font-medium text-emerald-400 bg-emerald-400/10 px-2 py-1 rounded-full">+12%</span>
|
||||
</div>
|
||||
<h3 class="text-sm font-medium text-slate-400 uppercase tracking-wider">Active Users</h3>
|
||||
<p class="text-3xl font-bold text-white mt-2">2,847</p>
|
||||
<p class="text-xs text-slate-500 mt-1">1,240 new this month</p>
|
||||
</div>
|
||||
|
||||
<!-- Total Vehicles -->
|
||||
<div class="bg-slate-800 rounded-xl border border-slate-700 p-6 hover:border-cyan-500/50 transition group">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<div class="p-2 rounded-lg bg-cyan-500/10 text-cyan-400 group-hover:bg-cyan-500/20 transition">
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.5 21v-7.5a.75.75 0 01.75-.75h3a.75.75 0 01.75.75V21m-4.5 0H2.36m11.14 0H18m0 0h3.64m-1.39 0V9.349m-16.5 11.65V9.35m0 0a3.001 3.001 0 003.75-.615A2.993 2.993 0 009.75 9.75c.896 0 1.7-.393 2.25-1.016a2.993 2.993 0 002.25 1.016c.896 0 1.7-.393 2.25-1.016a3.001 3.001 0 003.75.614m-16.5 0a3.004 3.004 0 01-.621-4.72L4.318 3.44A1.5 1.5 0 015.378 3h13.243a1.5 1.5 0 011.06.44l1.19 1.189a3 3 0 01-.621 4.72m-13.5 8.65h3.75a.75.75 0 00.75-.75V13.5a.75.75 0 00-.75-.75H6.75a.75.75 0 00-.75.75v3.75c0 .415.336.75.75.75z" />
|
||||
</svg>
|
||||
</div>
|
||||
<span class="text-xs font-medium text-emerald-400 bg-emerald-400/10 px-2 py-1 rounded-full">+5%</span>
|
||||
</div>
|
||||
<h3 class="text-sm font-medium text-slate-400 uppercase tracking-wider">Total Vehicles</h3>
|
||||
<p class="text-3xl font-bold text-white mt-2">12,493</p>
|
||||
<p class="text-xs text-slate-500 mt-1">342 added this month</p>
|
||||
</div>
|
||||
|
||||
<!-- Monthly Revenue -->
|
||||
<div class="bg-slate-800 rounded-xl border border-slate-700 p-6 hover:border-emerald-500/50 transition group">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<div class="p-2 rounded-lg bg-emerald-500/10 text-emerald-400 group-hover:bg-emerald-500/20 transition">
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v12m-3-2.818l.879.659c1.171.879 3.07.879 4.242 0 1.172-.879 1.172-2.303 0-3.182C13.536 12.219 12.768 12 12 12c-.725 0-1.45-.22-2.003-.659-1.106-.879-1.106-2.303 0-3.182s2.9-.879 4.006 0l.415.33M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
</div>
|
||||
<span class="text-xs font-medium text-emerald-400 bg-emerald-400/10 px-2 py-1 rounded-full">+18%</span>
|
||||
</div>
|
||||
<h3 class="text-sm font-medium text-slate-400 uppercase tracking-wider">Monthly Revenue</h3>
|
||||
<p class="text-3xl font-bold text-white mt-2">€48,250</p>
|
||||
<p class="text-xs text-slate-500 mt-1">€6,200 from premium</p>
|
||||
</div>
|
||||
|
||||
<!-- Error Logs -->
|
||||
<div class="bg-slate-800 rounded-xl border border-slate-700 p-6 hover:border-rose-500/50 transition group">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<div class="p-2 rounded-lg bg-rose-500/10 text-rose-400 group-hover:bg-rose-500/20 transition">
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.007v.008H12v-.008z" />
|
||||
</svg>
|
||||
</div>
|
||||
<span class="text-xs font-medium text-rose-400 bg-rose-400/10 px-2 py-1 rounded-full">+3</span>
|
||||
</div>
|
||||
<h3 class="text-sm font-medium text-slate-400 uppercase tracking-wider">Error Logs</h3>
|
||||
<p class="text-3xl font-bold text-white mt-2">23</p>
|
||||
<p class="text-xs text-slate-500 mt-1">3 critical today</p>
|
||||
<!-- Loading State -->
|
||||
<div v-if="loading" class="flex items-center justify-center py-20">
|
||||
<div class="text-slate-400 flex items-center gap-3">
|
||||
<svg class="w-5 h-5 animate-spin" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
||||
</svg>
|
||||
{{ t('dashboard.loading') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Quick Actions & Recent Activity -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<!-- Quick Actions -->
|
||||
<div class="bg-slate-800 rounded-xl border border-slate-700 p-6">
|
||||
<h2 class="text-lg font-semibold text-white mb-4">Quick Actions</h2>
|
||||
<div class="space-y-3">
|
||||
<button class="w-full flex items-center gap-3 px-4 py-3 bg-slate-700/50 hover:bg-slate-700 rounded-lg text-sm text-slate-300 hover:text-white transition text-left">
|
||||
<svg class="w-5 h-5 text-indigo-400 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<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.5-9a2.25 2.25 0 11-4.5 0 2.25 2.25 0 014.5 0z" />
|
||||
</svg>
|
||||
Invite New Admin User
|
||||
</button>
|
||||
<button class="w-full flex items-center gap-3 px-4 py-3 bg-slate-700/50 hover:bg-slate-700 rounded-lg text-sm text-slate-300 hover:text-white transition text-left">
|
||||
<svg class="w-5 h-5 text-cyan-400 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.5 21v-7.5a.75.75 0 01.75-.75h3a.75.75 0 01.75.75V21m-4.5 0H2.36m11.14 0H18m0 0h3.64m-1.39 0V9.349m-16.5 11.65V9.35m0 0a3.001 3.001 0 003.75-.615A2.993 2.993 0 009.75 9.75c.896 0 1.7-.393 2.25-1.016a2.993 2.993 0 002.25 1.016c.896 0 1.7-.393 2.25-1.016a3.001 3.001 0 003.75.614m-16.5 0a3.004 3.004 0 01-.621-4.72L4.318 3.44A1.5 1.5 0 015.378 3h13.243a1.5 1.5 0 011.06.44l1.19 1.189a3 3 0 01-.621 4.72m-13.5 8.65h3.75a.75.75 0 00.75-.75V13.5a.75.75 0 00-.75-.75H6.75a.75.75 0 00-.75.75v3.75c0 .415.336.75.75.75z" />
|
||||
</svg>
|
||||
Add New Vehicle
|
||||
</button>
|
||||
<button class="w-full flex items-center gap-3 px-4 py-3 bg-slate-700/50 hover:bg-slate-700 rounded-lg text-sm text-slate-300 hover:text-white transition text-left">
|
||||
<svg class="w-5 h-5 text-emerald-400 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h3.75M9 15h3.75M9 18h3.75m3 .75H18a2.25 2.25 0 002.25-2.25V6.108c0-1.135-.845-2.098-1.976-2.192a48.424 48.424 0 00-1.123-.08m-5.801 0c-.065.21-.1.433-.1.664 0 .414.336.75.75.75h4.5a.75.75 0 00.75-.75 2.25 2.25 0 00-.1-.664m-5.8 0A2.251 2.251 0 0113.5 2.25H15a2.25 2.25 0 012.15 1.586m-5.8 0c-.376.023-.75.05-1.124.08C9.095 4.01 8.25 4.973 8.25 6.108V8.25m0 0H4.875c-.621 0-1.125.504-1.125 1.125v11.25c0 .621.504 1.125 1.125 1.125h9.75c.621 0 1.125-.504 1.125-1.125V9.375c0-.621-.504-1.125-1.125-1.125H8.25zM6.75 12h.008v.008H6.75V12zm0 3h.008v.008H6.75V15zm0 3h.008v.008H6.75V18z" />
|
||||
</svg>
|
||||
View Service Reports
|
||||
</button>
|
||||
<!-- Error State -->
|
||||
<div v-else-if="error" class="bg-rose-500/10 border border-rose-500/30 rounded-xl p-6 mb-8">
|
||||
<p class="text-rose-400">{{ t('dashboard.error') }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Dashboard Content -->
|
||||
<template v-else>
|
||||
<!-- Stat Tiles Grid -->
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-4 gap-6 mb-8">
|
||||
<!-- Active Users -->
|
||||
<div class="bg-slate-800 rounded-xl border border-slate-700 p-6 hover:border-indigo-500/50 transition group">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<div class="p-2 rounded-lg bg-indigo-500/10 text-indigo-400 group-hover:bg-indigo-500/20 transition">
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<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.5-9a2.25 2.25 0 11-4.5 0 2.25 2.25 0 014.5 0z" />
|
||||
</svg>
|
||||
</div>
|
||||
<span v-if="stats.new_users_this_month > 0" class="text-xs font-medium text-emerald-400 bg-emerald-400/10 px-2 py-1 rounded-full">+{{ stats.new_users_this_month }}</span>
|
||||
</div>
|
||||
<h3 class="text-sm font-medium text-slate-400 uppercase tracking-wider">{{ t('dashboard.active_users') }}</h3>
|
||||
<p class="text-3xl font-bold text-white mt-2">{{ formatNumber(stats.active_users) }}</p>
|
||||
<p class="text-xs text-slate-500 mt-1">{{ formatNumber(stats.new_users_this_month) }} {{ t('dashboard.new_this_month') }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Total Vehicles -->
|
||||
<div class="bg-slate-800 rounded-xl border border-slate-700 p-6 hover:border-cyan-500/50 transition group">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<div class="p-2 rounded-lg bg-cyan-500/10 text-cyan-400 group-hover:bg-cyan-500/20 transition">
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.5 21v-7.5a.75.75 0 01.75-.75h3a.75.75 0 01.75.75V21m-4.5 0H2.36m11.14 0H18m0 0h3.64m-1.39 0V9.349m-16.5 11.65V9.35m0 0a3.001 3.001 0 003.75-.615A2.993 2.993 0 009.75 9.75c.896 0 1.7-.393 2.25-1.016a2.993 2.993 0 002.25 1.016c.896 0 1.7-.393 2.25-1.016a3.001 3.001 0 003.75.614m-16.5 0a3.004 3.004 0 01-.621-4.72L4.318 3.44A1.5 1.5 0 015.378 3h13.243a1.5 1.5 0 011.06.44l1.19 1.189a3 3 0 01-.621 4.72m-13.5 8.65h3.75a.75.75 0 00.75-.75V13.5a.75.75 0 00-.75-.75H6.75a.75.75 0 00-.75.75v3.75c0 .415.336.75.75.75z" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<h3 class="text-sm font-medium text-slate-400 uppercase tracking-wider">{{ t('dashboard.total_vehicles') }}</h3>
|
||||
<p class="text-3xl font-bold text-white mt-2">{{ formatNumber(stats.total_vehicles) }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Organizations -->
|
||||
<div class="bg-slate-800 rounded-xl border border-slate-700 p-6 hover:border-emerald-500/50 transition group">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<div class="p-2 rounded-lg bg-emerald-500/10 text-emerald-400 group-hover:bg-emerald-500/20 transition">
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.25 21h19.5m-18-18v18m10.5-18v18m6-13.5V21M6.75 6.75h.75m-.75 3h.75m-.75 3h.75m3-6h.75m-.75 3h.75m-.75 3h.75M6.75 21v-3.375c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21M3 3h12m-.75 4.5H21m-3.75 3.75h.008v.008h-.008v-.008zm0 3h.008v.008h-.008v-.008zm0 3h.008v.008h-.008v-.008z" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<h3 class="text-sm font-medium text-slate-400 uppercase tracking-wider">{{ t('dashboard.organizations') }}</h3>
|
||||
<p class="text-3xl font-bold text-white mt-2">{{ formatNumber(stats.active_organizations_count) }}</p>
|
||||
<p class="text-xs text-slate-500 mt-1">{{ formatNumber(stats.total_memberships) }} {{ t('dashboard.memberships') }}</p>
|
||||
</div>
|
||||
|
||||
<!-- New Users (This Month) -->
|
||||
<div class="bg-slate-800 rounded-xl border border-slate-700 p-6 hover:border-violet-500/50 transition group">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<div class="p-2 rounded-lg bg-violet-500/10 text-violet-400 group-hover:bg-violet-500/20 transition">
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M18 18.72a9.094 9.094 0 003.741-.479 3 3 0 00-4.682-2.72m.94 3.198l.001.031c0 .225-.012.447-.037.666A11.944 11.944 0 0112 21c-2.17 0-4.207-.576-5.963-1.584A6.062 6.062 0 016 18.719m12 0a5.971 5.971 0 00-.941-3.197m0 0A5.995 5.995 0 0012 12.75a5.995 5.995 0 00-5.058 2.772m0 0a3 3 0 00-4.681 2.72 8.986 8.986 0 003.74.477m.94-3.197a5.971 5.971 0 00-.94 3.197M15 6.75a3 3 0 11-6 0 3 3 0 016 0zm6 3a2.25 2.25 0 11-4.5 0 2.25 2.25 0 014.5 0zm-13.5 0a2.25 2.25 0 11-4.5 0 2.25 2.25 0 014.5 0z" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<h3 class="text-sm font-medium text-slate-400 uppercase tracking-wider">{{ t('dashboard.new_users') }}</h3>
|
||||
<p class="text-3xl font-bold text-white mt-2">{{ formatNumber(stats.new_users_this_month) }}</p>
|
||||
<p class="text-xs text-slate-500 mt-1">{{ formatNumber(stats.new_users_today) }} {{ t('dashboard.new_today') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Recent Activity -->
|
||||
<div class="bg-slate-800 rounded-xl border border-slate-700 p-6">
|
||||
<h2 class="text-lg font-semibold text-white mb-4">Recent Activity</h2>
|
||||
<div class="space-y-4">
|
||||
<div v-for="(activity, index) in recentActivities" :key="index" class="flex items-start gap-3">
|
||||
<div class="w-2 h-2 mt-2 rounded-full flex-shrink-0" :class="activity.dotColor" />
|
||||
<div class="flex-1 min-w-0">
|
||||
<p class="text-sm text-slate-300">{{ activity.text }}</p>
|
||||
<p class="text-xs text-slate-500 mt-0.5">{{ activity.time }}</p>
|
||||
<!-- Secondary Stats Row -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6 mb-8">
|
||||
<!-- Users by Role -->
|
||||
<div class="bg-slate-800 rounded-xl border border-slate-700 p-6">
|
||||
<h3 class="text-sm font-medium text-slate-400 uppercase tracking-wider mb-4">{{ t('dashboard.users_by_role') }}</h3>
|
||||
<div class="space-y-3">
|
||||
<div v-for="(count, role) in stats.users_by_role" :key="role" class="flex items-center justify-between">
|
||||
<span class="text-sm text-slate-300">{{ role }}</span>
|
||||
<span class="text-sm font-semibold text-white">{{ count }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Users by Plan -->
|
||||
<div class="bg-slate-800 rounded-xl border border-slate-700 p-6">
|
||||
<h3 class="text-sm font-medium text-slate-400 uppercase tracking-wider mb-4">{{ t('dashboard.users_by_plan') }}</h3>
|
||||
<div class="space-y-3">
|
||||
<div v-for="(count, plan) in stats.users_by_plan" :key="plan" class="flex items-center justify-between">
|
||||
<span class="text-sm text-slate-300">{{ plan }}</span>
|
||||
<span class="text-sm font-semibold text-white">{{ count }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Users by Language -->
|
||||
<div class="bg-slate-800 rounded-xl border border-slate-700 p-6">
|
||||
<h3 class="text-sm font-medium text-slate-400 uppercase tracking-wider mb-4">{{ t('dashboard.users_by_language') }}</h3>
|
||||
<div class="space-y-3">
|
||||
<div v-for="(count, lang) in stats.users_by_language" :key="lang" class="flex items-center justify-between">
|
||||
<span class="text-sm text-slate-300">{{ lang }}</span>
|
||||
<span class="text-sm font-semibold text-white">{{ count }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Registration Trend -->
|
||||
<div class="bg-slate-800 rounded-xl border border-slate-700 p-6 mb-8">
|
||||
<h3 class="text-sm font-medium text-slate-400 uppercase tracking-wider mb-4">{{ t('dashboard.registration_trend') }}</h3>
|
||||
<div class="flex items-end gap-1 h-32">
|
||||
<div
|
||||
v-for="(day, idx) in registrationTrend"
|
||||
:key="idx"
|
||||
class="flex-1 bg-indigo-500/60 hover:bg-indigo-400/80 rounded-t transition-all cursor-pointer relative group"
|
||||
:style="{ height: day.height + '%' }"
|
||||
:title="day.date + ': ' + day.count + ' users'"
|
||||
>
|
||||
<div class="absolute -top-8 left-1/2 -translate-x-1/2 bg-slate-700 text-xs text-white px-2 py-1 rounded opacity-0 group-hover:opacity-100 transition whitespace-nowrap">
|
||||
{{ day.date }}: {{ day.count }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Quick Actions & Recent Activity -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<!-- Quick Actions -->
|
||||
<div class="bg-slate-800 rounded-xl border border-slate-700 p-6">
|
||||
<h2 class="text-lg font-semibold text-white mb-4">{{ t('dashboard.quick_actions') }}</h2>
|
||||
<div class="space-y-3">
|
||||
<button class="w-full flex items-center gap-3 px-4 py-3 bg-slate-700/50 hover:bg-slate-700 rounded-lg text-sm text-slate-300 hover:text-white transition text-left">
|
||||
<svg class="w-5 h-5 text-indigo-400 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<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.5-9a2.25 2.25 0 11-4.5 0 2.25 2.25 0 014.5 0z" />
|
||||
</svg>
|
||||
{{ t('dashboard.invite_admin') }}
|
||||
</button>
|
||||
<button class="w-full flex items-center gap-3 px-4 py-3 bg-slate-700/50 hover:bg-slate-700 rounded-lg text-sm text-slate-300 hover:text-white transition text-left">
|
||||
<svg class="w-5 h-5 text-cyan-400 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.5 21v-7.5a.75.75 0 01.75-.75h3a.75.75 0 01.75.75V21m-4.5 0H2.36m11.14 0H18m0 0h3.64m-1.39 0V9.349m-16.5 11.65V9.35m0 0a3.001 3.001 0 003.75-.615A2.993 2.993 0 009.75 9.75c.896 0 1.7-.393 2.25-1.016a2.993 2.993 0 002.25 1.016c.896 0 1.7-.393 2.25-1.016a3.001 3.001 0 003.75.614m-16.5 0a3.004 3.004 0 01-.621-4.72L4.318 3.44A1.5 1.5 0 015.378 3h13.243a1.5 1.5 0 011.06.44l1.19 1.189a3 3 0 01-.621 4.72m-13.5 8.65h3.75a.75.75 0 00.75-.75V13.5a.75.75 0 00-.75-.75H6.75a.75.75 0 00-.75.75v3.75c0 .415.336.75.75.75z" />
|
||||
</svg>
|
||||
{{ t('dashboard.add_vehicle') }}
|
||||
</button>
|
||||
<button class="w-full flex items-center gap-3 px-4 py-3 bg-slate-700/50 hover:bg-slate-700 rounded-lg text-sm text-slate-300 hover:text-white transition text-left">
|
||||
<svg class="w-5 h-5 text-emerald-400 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h3.75M9 15h3.75M9 18h3.75m3 .75H18a2.25 2.25 0 002.25-2.25V6.108c0-1.135-.845-2.098-1.976-2.192a48.424 48.424 0 00-1.123-.08m-5.801 0c-.065.21-.1.433-.1.664 0 .414.336.75.75.75h4.5a.75.75 0 00.75-.75 2.25 2.25 0 00-.1-.664m-5.8 0A2.251 2.251 0 0113.5 2.25H15a2.25 2.25 0 012.15 1.586m-5.8 0c-.376.023-.75.05-1.124.08C9.095 4.01 8.25 4.973 8.25 6.108V8.25m0 0H4.875c-.621 0-1.125.504-1.125 1.125v11.25c0 .621.504 1.125 1.125 1.125h9.75c.621 0 1.125-.504 1.125-1.125V9.375c0-.621-.504-1.125-1.125-1.125H8.25zM6.75 12h.008v.008H6.75V12zm0 3h.008v.008H6.75V15zm0 3h.008v.008H6.75V18z" />
|
||||
</svg>
|
||||
{{ t('dashboard.view_reports') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Recent Activity -->
|
||||
<div class="bg-slate-800 rounded-xl border border-slate-700 p-6">
|
||||
<h2 class="text-lg font-semibold text-white mb-4">{{ t('dashboard.recent_activity') }}</h2>
|
||||
<div class="space-y-4">
|
||||
<div v-for="(activity, index) in recentActivities" :key="index" class="flex items-start gap-3">
|
||||
<div class="w-2 h-2 mt-2 rounded-full flex-shrink-0" :class="activity.dotColor" />
|
||||
<div class="flex-1 min-w-0">
|
||||
<p class="text-sm text-slate-300">{{ activity.text }}</p>
|
||||
<p class="text-xs text-slate-500 mt-0.5">{{ activity.time }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -118,6 +186,63 @@ definePageMeta({
|
||||
middleware: 'auth',
|
||||
})
|
||||
|
||||
interface DashboardStats {
|
||||
total_users: number
|
||||
active_users: number
|
||||
deleted_users: number
|
||||
banned_users: number
|
||||
new_users_today: number
|
||||
new_users_this_week: number
|
||||
new_users_this_month: number
|
||||
users_by_role: Record<string, number>
|
||||
users_by_plan: Record<string, number>
|
||||
users_by_language: Record<string, number>
|
||||
users_with_person: number
|
||||
users_without_person: number
|
||||
total_vehicles: number
|
||||
active_organizations_count: number
|
||||
total_memberships: number
|
||||
registration_trend: { date: string; count: number }[]
|
||||
}
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const loading = ref(true)
|
||||
const error = ref(false)
|
||||
const stats = ref<DashboardStats>({
|
||||
total_users: 0,
|
||||
active_users: 0,
|
||||
deleted_users: 0,
|
||||
banned_users: 0,
|
||||
new_users_today: 0,
|
||||
new_users_this_week: 0,
|
||||
new_users_this_month: 0,
|
||||
users_by_role: {},
|
||||
users_by_plan: {},
|
||||
users_by_language: {},
|
||||
users_with_person: 0,
|
||||
users_without_person: 0,
|
||||
total_vehicles: 0,
|
||||
active_organizations_count: 0,
|
||||
total_memberships: 0,
|
||||
registration_trend: [],
|
||||
})
|
||||
|
||||
const registrationTrend = computed(() => {
|
||||
const trend = stats.value.registration_trend
|
||||
if (!trend.length) return []
|
||||
const maxCount = Math.max(...trend.map(d => d.count))
|
||||
return trend.map(d => ({
|
||||
date: d.date,
|
||||
count: d.count,
|
||||
height: maxCount > 0 ? (d.count / maxCount) * 100 : 0,
|
||||
}))
|
||||
})
|
||||
|
||||
function formatNumber(num: number): string {
|
||||
return new Intl.NumberFormat().format(num)
|
||||
}
|
||||
|
||||
const recentActivities = [
|
||||
{ text: 'New user registered: john.doe@example.com', time: '2 minutes ago', dotColor: 'bg-emerald-400' },
|
||||
{ text: 'Vehicle #V-8842 added to catalog', time: '15 minutes ago', dotColor: 'bg-cyan-400' },
|
||||
@@ -125,4 +250,21 @@ const recentActivities = [
|
||||
{ text: 'System error: DVLA API timeout on request #4421', time: '2 hours ago', dotColor: 'bg-rose-400' },
|
||||
{ text: 'Permission updated for user: superadmin@sf.hu', time: '3 hours ago', dotColor: 'bg-indigo-400' },
|
||||
]
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
// Read the access_token from cookie for authenticated API calls
|
||||
const tokenCookie = useCookie('access_token')
|
||||
const token = tokenCookie.value
|
||||
const data = await $fetch('/api/v1/admin/users/stats', {
|
||||
headers: token ? { Authorization: `Bearer ${token}` } : undefined,
|
||||
})
|
||||
stats.value = data as DashboardStats
|
||||
} catch (e) {
|
||||
console.error('Failed to load dashboard stats:', e)
|
||||
error.value = true
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user