1 line
58 KiB
JSON
1 line
58 KiB
JSON
{"file":"index-D8qGjmnz.js","mappings":"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","names":["_ssrRenderAttrs","_mergeProps","$t","_ssrRenderAttr","_ssrInterpolate","_ssrRenderList","_ssrIncludeBooleanAttr","_push","_parent","_createTextVNode","_toDisplayString","_ssrRenderClass"],"sources":["../../../../components/charts/UserTrendChart.vue","../../../../pages/users/index.vue"],"sourcesContent":["<template>\n <div class=\"bg-slate-800 rounded-xl border border-slate-700 p-5\">\n <!-- Header -->\n <div class=\"flex items-center justify-between mb-4\">\n <div>\n <h3 class=\"text-sm font-semibold text-white\">{{ $t('users.trend_title') }}</h3>\n <p class=\"text-xs text-slate-400 mt-0.5\">{{ $t('users.trend_subtitle') }}</p>\n </div>\n <!-- Period selector -->\n <select\n v-model=\"selectedPeriod\"\n class=\"px-2.5 py-1.5 text-xs bg-slate-700 border border-slate-600 rounded-lg text-slate-300 focus:outline-none focus:ring-2 focus:ring-indigo-500/50 transition\"\n @change=\"updateChart\"\n >\n <option value=\"7\">{{ $t('users.trend_7d') }}</option>\n <option value=\"14\">{{ $t('users.trend_14d') }}</option>\n <option value=\"30\">{{ $t('users.trend_30d') }}</option>\n <option value=\"90\">{{ $t('users.trend_90d') }}</option>\n </select>\n </div>\n\n <!-- Loading State -->\n <div v-if=\"loading\" class=\"flex items-center justify-center py-12\">\n <svg class=\"w-6 h-6 text-indigo-400 animate-spin\" fill=\"none\" viewBox=\"0 0 24 24\">\n <circle class=\"opacity-25\" cx=\"12\" cy=\"12\" r=\"10\" stroke=\"currentColor\" stroke-width=\"4\" />\n <path class=\"opacity-75\" fill=\"currentColor\" d=\"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z\" />\n </svg>\n </div>\n\n <!-- Empty State -->\n <div v-else-if=\"filteredData.length === 0\" class=\"flex flex-col items-center justify-center py-12 text-center\">\n <svg class=\"w-10 h-10 text-slate-600 mb-3\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"1.5\" 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\" />\n </svg>\n <p class=\"text-slate-500 text-xs\">{{ $t('users.trend_no_data') }}</p>\n </div>\n\n <!-- Chart Area -->\n <div v-else class=\"relative\">\n <!-- SVG Bar Chart -->\n <svg\n :viewBox=\"`0 0 ${svgWidth} ${svgHeight}`\"\n class=\"w-full h-auto overflow-visible\"\n preserveAspectRatio=\"xMidYMid meet\"\n >\n <!-- Grid lines -->\n <line\n v-for=\"(y, yi) in gridLines\"\n :key=\"'grid-' + yi\"\n :x1=\"padding.left\"\n :y1=\"y\"\n :x2=\"svgWidth - padding.right\"\n :y2=\"y\"\n stroke=\"currentColor\"\n class=\"text-slate-700/50\"\n stroke-width=\"1\"\n stroke-dasharray=\"4,4\"\n />\n <!-- Y-axis labels -->\n <text\n v-for=\"(label, li) in yLabels\"\n :key=\"'ylabel-' + li\"\n :x=\"padding.left - 8\"\n :y=\"label.y + 4\"\n text-anchor=\"end\"\n class=\"fill-slate-500\"\n font-size=\"10\"\n >\n {{ label.text }}\n </text>\n\n <!-- Bars -->\n <g\n v-for=\"(item, idx) in visibleBars\"\n :key=\"'bar-' + idx\"\n class=\"chart-bar-group\"\n @mouseenter=\"hoveredBar = idx\"\n @mouseleave=\"hoveredBar = null\"\n >\n <rect\n :x=\"item.x\"\n :y=\"item.y\"\n :width=\"barWidth\"\n :height=\"item.height\"\n :rx=\"3\"\n :ry=\"3\"\n :fill=\"item.color\"\n :opacity=\"hoveredBar === idx ? 0.9 : 0.7\"\n class=\"transition-all duration-200 cursor-pointer\"\n />\n <!-- Tooltip -->\n <g v-if=\"hoveredBar === idx\">\n <rect\n :x=\"item.tooltipX\"\n :y=\"item.tooltipY - 28\"\n :width=\"item.tooltipWidth\"\n height=\"22\"\n rx=\"4\"\n fill=\"#1e293b\"\n stroke=\"#475569\"\n stroke-width=\"1\"\n />\n <text\n :x=\"item.tooltipX + item.tooltipWidth / 2\"\n :y=\"item.tooltipY - 13\"\n text-anchor=\"middle\"\n fill=\"#e2e8f0\"\n font-size=\"11\"\n font-weight=\"600\"\n >\n {{ item.tooltipText }}\n </text>\n </g>\n </g>\n\n <!-- X-axis labels (show every Nth label to avoid crowding) -->\n <text\n v-for=\"(item, xi) in xLabels\"\n :key=\"'xlabel-' + xi\"\n :x=\"item.x + barWidth / 2\"\n :y=\"svgHeight - padding.bottom + 16\"\n text-anchor=\"middle\"\n class=\"fill-slate-500\"\n font-size=\"9\"\n >\n {{ item.label }}\n </text>\n </svg>\n\n <!-- Summary Stats Below Chart -->\n <div class=\"flex items-center justify-between mt-4 pt-3 border-t border-slate-700/50\">\n <div class=\"flex items-center gap-4 text-xs\">\n <div class=\"flex items-center gap-1.5\">\n <span class=\"w-2.5 h-2.5 rounded-sm bg-indigo-500/70\"></span>\n <span class=\"text-slate-400\">{{ $t('users.trend_daily') }}</span>\n </div>\n <div>\n <span class=\"text-slate-400\">{{ $t('users.trend_avg') }}:</span>\n <span class=\"text-white font-semibold ml-1\">{{ averageCount }}</span>\n </div>\n <div>\n <span class=\"text-slate-400\">{{ $t('users.trend_total') }}:</span>\n <span class=\"text-white font-semibold ml-1\">{{ totalCount }}</span>\n </div>\n </div>\n <div v-if=\"trendDirection !== 'stable'\" class=\"flex items-center gap-1 text-xs\">\n <svg\n v-if=\"trendDirection === 'up'\"\n class=\"w-3.5 h-3.5 text-emerald-400\"\n fill=\"none\"\n stroke=\"currentColor\"\n viewBox=\"0 0 24 24\"\n >\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M13 7h8m0 0v8m0-8l-8 8-4-4-6 6\" />\n </svg>\n <svg\n v-else\n class=\"w-3.5 h-3.5 text-red-400\"\n fill=\"none\"\n stroke=\"currentColor\"\n viewBox=\"0 0 24 24\"\n >\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M13 17h8m0 0V9m0 8l-8-8-4 4-6-6\" />\n </svg>\n <span :class=\"trendDirection === 'up' ? 'text-emerald-400' : 'text-red-400'\" class=\"font-medium\">\n {{ trendPercent }}%\n </span>\n <span class=\"text-slate-500\">{{ $t('users.trend_vs_prev') }}</span>\n </div>\n </div>\n </div>\n </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { ref, computed, onMounted, watch } from 'vue'\n\ninterface TrendDataPoint {\n date: string\n count: number\n}\n\nconst props = defineProps<{\n data: TrendDataPoint[]\n loading?: boolean\n}>()\n\n// ── State ──────────────────────────────────────────────────────────\n\nconst selectedPeriod = ref('30')\nconst hoveredBar = ref<number | null>(null)\n\n// ── Chart Dimensions ────────────────────────────────────────────────\n\nconst svgWidth = 800\nconst svgHeight = 220\nconst padding = { top: 10, right: 16, bottom: 30, left: 36 }\n\n// ── Computed ───────────────────────────────────────────────────────\n\nconst filteredData = computed(() => {\n const days = parseInt(selectedPeriod.value, 10)\n if (!props.data || props.data.length === 0) return []\n // Sort by date ascending\n const sorted = [...props.data].sort(\n (a, b) => new Date(a.date).getTime() - new Date(b.date).getTime()\n )\n const cutoff = new Date()\n cutoff.setDate(cutoff.getDate() - days)\n return sorted.filter((d) => new Date(d.date) >= cutoff)\n})\n\nconst maxCount = computed(() => {\n if (filteredData.value.length === 0) return 1\n return Math.max(...filteredData.value.map((d) => d.count), 1)\n})\n\nconst chartHeight = svgHeight - padding.top - padding.bottom\n\n// Grid lines (5 horizontal lines)\nconst gridLines = computed(() => {\n const lines = []\n for (let i = 0; i <= 4; i++) {\n lines.push(padding.top + (chartHeight / 4) * i)\n }\n return lines\n})\n\nconst yLabels = computed(() => {\n const labels = []\n for (let i = 0; i <= 4; i++) {\n const y = padding.top + (chartHeight / 4) * i\n const value = Math.round(maxCount.value - (maxCount.value / 4) * i)\n labels.push({ y, text: value.toString() })\n }\n return labels\n})\n\nconst barWidth = computed(() => {\n const count = filteredData.value.length\n if (count === 0) return 0\n const availableWidth = svgWidth - padding.left - padding.right\n const maxBarWidth = 32\n const calculatedWidth = (availableWidth / count) * 0.7\n return Math.min(calculatedWidth, maxBarWidth)\n})\n\nconst visibleBars = computed(() => {\n const count = filteredData.value.length\n if (count === 0) return []\n\n const availableWidth = svgWidth - padding.left - padding.right\n const gap = count > 1 ? (availableWidth - barWidth.value * count) / (count - 1) : 0\n\n return filteredData.value.map((item, idx) => {\n const x = padding.left + idx * (barWidth.value + gap)\n const barHeight = maxCount.value > 0 ? (item.count / maxCount.value) * chartHeight : 0\n const y = svgHeight - padding.bottom - barHeight\n\n // Color gradient based on value\n const ratio = maxCount.value > 0 ? item.count / maxCount.value : 0\n let color: string\n if (ratio > 0.8) color = '#6366f1' // indigo-500\n else if (ratio > 0.5) color = '#818cf8' // indigo-400\n else if (ratio > 0.3) color = '#a5b4fc' // indigo-300\n else color = '#c7d2fe' // indigo-200\n\n // Tooltip\n const tooltipText = `${item.count} ${formatDate(item.date)}`\n const tooltipWidth = tooltipText.length * 7 + 16\n let tooltipX = x + barWidth.value / 2 - tooltipWidth / 2\n if (tooltipX < padding.left) tooltipX = padding.left\n if (tooltipX + tooltipWidth > svgWidth - padding.right) tooltipX = svgWidth - padding.right - tooltipWidth\n const tooltipY = y - 4\n\n return {\n x,\n y,\n height: barHeight,\n color,\n tooltipX,\n tooltipY,\n tooltipWidth,\n tooltipText,\n }\n })\n})\n\n// X-axis labels (show every Nth)\nconst xLabels = computed(() => {\n const count = filteredData.value.length\n if (count === 0) return []\n\n const availableWidth = svgWidth - padding.left - padding.right\n const gap = count > 1 ? (availableWidth - barWidth.value * count) / (count - 1) : 0\n\n // Determine label interval based on data density\n let interval = 1\n if (count > 14) interval = 2\n if (count > 30) interval = 5\n if (count > 60) interval = 10\n\n return filteredData.value\n .map((item, idx) => {\n const x = padding.left + idx * (barWidth.value + gap)\n const d = new Date(item.date)\n const label = `${d.getMonth() + 1}/${d.getDate()}`\n return { x, label, idx }\n })\n .filter((_, i) => i % interval === 0 || i === count - 1)\n})\n\n// Summary stats\nconst averageCount = computed(() => {\n if (filteredData.value.length === 0) return 0\n const sum = filteredData.value.reduce((acc, d) => acc + d.count, 0)\n return Math.round(sum / filteredData.value.length)\n})\n\nconst totalCount = computed(() => {\n return filteredData.value.reduce((acc, d) => acc + d.count, 0)\n})\n\nconst trendDirection = computed(() => {\n const data = filteredData.value\n if (data.length < 4) return 'stable'\n const half = Math.floor(data.length / 2)\n const firstHalf = data.slice(0, half).reduce((acc, d) => acc + d.count, 0) / half\n const secondHalf = data.slice(half).reduce((acc, d) => acc + d.count, 0) / (data.length - half)\n if (secondHalf > firstHalf * 1.05) return 'up'\n if (secondHalf < firstHalf * 0.95) return 'down'\n return 'stable'\n})\n\nconst trendPercent = computed(() => {\n const data = filteredData.value\n if (data.length < 4) return 0\n const half = Math.floor(data.length / 2)\n const firstHalf = data.slice(0, half).reduce((acc, d) => acc + d.count, 0) / half\n if (firstHalf === 0) return 100\n const secondHalf = data.slice(half).reduce((acc, d) => acc + d.count, 0) / (data.length - half)\n return Math.round(Math.abs((secondHalf - firstHalf) / firstHalf) * 100)\n})\n\n// ── Helpers ────────────────────────────────────────────────────────\n\nfunction formatDate(dateStr: string): string {\n try {\n const d = new Date(dateStr)\n return `${d.getMonth() + 1}/${d.getDate()}`\n } catch {\n return dateStr\n }\n}\n\nfunction updateChart() {\n // Trigger reactivity by clearing and resetting hover\n hoveredBar.value = null\n}\n\n// ── Watch for data changes ─────────────────────────────────────────\n\nwatch(() => props.data, () => {\n updateChart()\n})\n</script>\n","<template>\n <div>\n <!-- Page Header -->\n <div class=\"mb-8\">\n <h1 class=\"text-2xl font-bold text-white\">{{ $t('users.title') }}</h1>\n <p class=\"text-slate-400 mt-1\">{{ $t('users.subtitle') }}</p>\n </div>\n\n <!-- Search & Filter Bar -->\n <div class=\"flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4 mb-6\">\n <div class=\"relative flex-1 max-w-md\">\n <svg class=\"absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-500\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z\" />\n </svg>\n <input\n v-model=\"searchQuery\"\n type=\"text\"\n :placeholder=\"$t('users.search_placeholder')\"\n class=\"w-full pl-10 pr-4 py-2.5 bg-slate-800 border border-slate-700 rounded-lg text-sm text-white placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-indigo-500/50 focus:border-indigo-500 transition\"\n />\n </div>\n <div class=\"flex items-center gap-2 flex-wrap\">\n <!-- Status Filter -->\n <select\n v-model=\"statusFilter\"\n class=\"px-3 py-2.5 bg-slate-800 border border-slate-700 rounded-lg text-sm text-slate-300 focus:outline-none focus:ring-2 focus:ring-indigo-500/50 transition\"\n >\n <option value=\"all\">{{ $t('users.all_statuses') }}</option>\n <option value=\"active\">{{ $t('users.status_active') }}</option>\n <option value=\"inactive\">{{ $t('users.status_inactive') }}</option>\n <option value=\"deleted\">{{ $t('users.status_deleted') }}</option>\n <option value=\"banned\">{{ $t('users.status_banned') }}</option>\n </select>\n <!-- Role Filter -->\n <select\n v-model=\"roleFilter\"\n class=\"px-3 py-2.5 bg-slate-800 border border-slate-700 rounded-lg text-sm text-slate-300 focus:outline-none focus:ring-2 focus:ring-indigo-500/50 transition\"\n >\n <option value=\"all\">{{ $t('users.all_roles') }}</option>\n <option value=\"user\">{{ $t('users.role_user') }}</option>\n <option value=\"admin\">{{ $t('users.role_admin') }}</option>\n <option value=\"staff\">{{ $t('users.role_staff') }}</option>\n <option value=\"superadmin\">{{ $t('users.role_superadmin') }}</option>\n </select>\n <!-- Plan Filter -->\n <select\n v-model=\"planFilter\"\n class=\"px-3 py-2.5 bg-slate-800 border border-slate-700 rounded-lg text-sm text-slate-300 focus:outline-none focus:ring-2 focus:ring-indigo-500/50 transition\"\n >\n <option value=\"all\">{{ $t('users.all_plans') }}</option>\n <option value=\"FREE\">{{ $t('users.plan_free') }}</option>\n <option value=\"PREMIUM\">{{ $t('users.plan_premium') }}</option>\n <option value=\"ENTERPRISE\">{{ $t('users.plan_enterprise') }}</option>\n </select>\n </div>\n </div>\n\n <!-- Loading State -->\n <div v-if=\"loading\" class=\"flex items-center justify-center py-20\">\n <div class=\"flex flex-col items-center gap-3\">\n <svg class=\"w-8 h-8 text-indigo-400 animate-spin\" fill=\"none\" viewBox=\"0 0 24 24\">\n <circle class=\"opacity-25\" cx=\"12\" cy=\"12\" r=\"10\" stroke=\"currentColor\" stroke-width=\"4\" />\n <path class=\"opacity-75\" fill=\"currentColor\" d=\"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z\" />\n </svg>\n <p class=\"text-slate-400 text-sm\">{{ $t('users.loading') }}</p>\n </div>\n </div>\n\n <!-- Error State -->\n <div v-else-if=\"error\" class=\"flex flex-col items-center justify-center py-20 text-center\">\n <svg class=\"w-12 h-12 text-red-500 mb-4\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"1.5\" d=\"M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L4.082 16.5c-.77.833.192 2.5 1.732 2.5z\" />\n </svg>\n <p class=\"text-red-400 text-sm mb-2\">{{ error }}</p>\n <button @click=\"fetchUsers\" class=\"px-4 py-2 text-sm bg-indigo-600/20 text-indigo-300 hover:bg-indigo-600/30 rounded-lg transition\">\n {{ $t('users.retry') }}\n </button>\n </div>\n\n <!-- Content -->\n <template v-else>\n <!-- Stats Cards Row -->\n <div class=\"grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-5 gap-4 mb-8\">\n <!-- Total Users -->\n <div class=\"bg-slate-800 rounded-xl border border-slate-700 p-4\">\n <div class=\"flex items-center gap-3\">\n <div class=\"p-2 rounded-lg bg-indigo-500/10 text-indigo-400\">\n <svg class=\"w-5 h-5\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <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\" />\n </svg>\n </div>\n <div>\n <p class=\"text-xs text-slate-400 uppercase tracking-wider\">{{ $t('users.total_users') }}</p>\n <p class=\"text-2xl font-bold text-white\">{{ stats.total_users }}</p>\n </div>\n </div>\n </div>\n <!-- Active Users -->\n <div class=\"bg-slate-800 rounded-xl border border-slate-700 p-4\">\n <div class=\"flex items-center gap-3\">\n <div class=\"p-2 rounded-lg bg-emerald-500/10 text-emerald-400\">\n <svg class=\"w-5 h-5\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z\" />\n </svg>\n </div>\n <div>\n <p class=\"text-xs text-slate-400 uppercase tracking-wider\">{{ $t('users.active_users') }}</p>\n <p class=\"text-2xl font-bold text-emerald-400\">{{ stats.active_users }}</p>\n </div>\n </div>\n </div>\n <!-- Deleted Users -->\n <div class=\"bg-slate-800 rounded-xl border border-slate-700 p-4\">\n <div class=\"flex items-center gap-3\">\n <div class=\"p-2 rounded-lg bg-red-500/10 text-red-400\">\n <svg class=\"w-5 h-5\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16\" />\n </svg>\n </div>\n <div>\n <p class=\"text-xs text-slate-400 uppercase tracking-wider\">{{ $t('users.deleted_users') }}</p>\n <p class=\"text-2xl font-bold text-red-400\">{{ stats.deleted_users }}</p>\n </div>\n </div>\n </div>\n <!-- Banned Users -->\n <div class=\"bg-slate-800 rounded-xl border border-slate-700 p-4\">\n <div class=\"flex items-center gap-3\">\n <div class=\"p-2 rounded-lg bg-amber-500/10 text-amber-400\">\n <svg class=\"w-5 h-5\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M18.364 18.364A9 9 0 005.636 5.636m12.728 12.728A9 9 0 015.636 5.636m12.728 12.728L5.636 5.636\" />\n </svg>\n </div>\n <div>\n <p class=\"text-xs text-slate-400 uppercase tracking-wider\">{{ $t('users.banned_users') }}</p>\n <p class=\"text-2xl font-bold text-amber-400\">{{ stats.banned_users }}</p>\n </div>\n </div>\n </div>\n <!-- New Today -->\n <div class=\"bg-slate-800 rounded-xl border border-slate-700 p-4\">\n <div class=\"flex items-center gap-3\">\n <div class=\"p-2 rounded-lg bg-cyan-500/10 text-cyan-400\">\n <svg class=\"w-5 h-5\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <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\" />\n </svg>\n </div>\n <div>\n <p class=\"text-xs text-slate-400 uppercase tracking-wider\">{{ $t('users.new_today') }}</p>\n <p class=\"text-2xl font-bold text-cyan-400\">{{ stats.new_users_today }}</p>\n </div>\n </div>\n </div>\n </div>\n\n <!-- Registration Trend Chart -->\n <div class=\"mb-8\">\n <UserTrendChart :data=\"stats.registration_trend\" :loading=\"loading\" />\n </div>\n\n <!-- Filtered results indicator -->\n <div v-if=\"isFilterActive\" class=\"mb-4 text-xs text-slate-500\">\n {{ $t('users.filtered_results') }}: <span class=\"text-slate-300 font-semibold\">{{ users.length }}</span> / {{ totalCount }} db\n </div>\n\n <!-- Bulk Actions Bar -->\n <div v-if=\"selectedUserIds.length > 0 && canEditUsers\" class=\"mb-4 px-4 py-3 bg-indigo-900/30 border border-indigo-700/50 rounded-xl flex items-center justify-between\">\n <p class=\"text-sm text-indigo-300\">\n {{ $t('users.selected_count', { count: selectedUserIds.length }) }}\n </p>\n <div class=\"flex items-center gap-2\">\n <button\n @click=\"bulkActivate\"\n class=\"px-3 py-1.5 text-xs font-medium bg-emerald-600/20 text-emerald-300 hover:bg-emerald-600/30 rounded-lg transition\"\n >\n {{ $t('users.bulk_activate') }}\n </button>\n <button\n @click=\"bulkDeactivate\"\n class=\"px-3 py-1.5 text-xs font-medium bg-red-600/20 text-red-300 hover:bg-red-600/30 rounded-lg transition\"\n >\n {{ $t('users.bulk_deactivate') }}\n </button>\n <button\n @click=\"clearSelection\"\n class=\"px-3 py-1.5 text-xs font-medium bg-slate-600/20 text-slate-300 hover:bg-slate-600/30 rounded-lg transition\"\n >\n {{ $t('users.clear_selection') }}\n </button>\n </div>\n </div>\n\n <!-- Data Table -->\n <div class=\"bg-slate-800/50 border border-slate-700 rounded-xl overflow-hidden\">\n <div class=\"overflow-x-auto\">\n <table class=\"w-full text-sm\">\n <thead>\n <tr class=\"border-b border-slate-700 bg-slate-800/80\">\n <th class=\"text-left py-3.5 px-4 w-10\">\n <input\n type=\"checkbox\"\n :checked=\"allSelected\"\n @change=\"toggleSelectAll\"\n class=\"rounded border-slate-600 bg-slate-700 text-indigo-600 focus:ring-indigo-500/50\"\n />\n </th>\n <th class=\"text-left py-3.5 px-4 text-xs font-semibold uppercase tracking-wider text-slate-400\">ID</th>\n <th class=\"text-left py-3.5 px-4 text-xs font-semibold uppercase tracking-wider text-slate-400\">{{ $t('users.email') }}</th>\n <th class=\"text-left py-3.5 px-4 text-xs font-semibold uppercase tracking-wider text-slate-400\">{{ $t('users.name') }}</th>\n <th class=\"text-left py-3.5 px-4 text-xs font-semibold uppercase tracking-wider text-slate-400\">{{ $t('users.role') }}</th>\n <th class=\"text-left py-3.5 px-4 text-xs font-semibold uppercase tracking-wider text-slate-400\">{{ $t('users.status') }}</th>\n <th class=\"text-left py-3.5 px-4 text-xs font-semibold uppercase tracking-wider text-slate-400\">{{ $t('users.package') }}</th>\n <th class=\"text-left py-3.5 px-4 text-xs font-semibold uppercase tracking-wider text-slate-400\">{{ $t('users.registration') }}</th>\n <th class=\"text-left py-3.5 px-4 text-xs font-semibold uppercase tracking-wider text-slate-400\">{{ $t('users.language') }}</th>\n <th class=\"text-left py-3.5 px-4 text-xs font-semibold uppercase tracking-wider text-slate-400\">{{ $t('users.actions') }}</th>\n </tr>\n </thead>\n <tbody>\n <tr\n v-for=\"user in users\"\n :key=\"user.id\"\n class=\"border-b border-slate-700/50 hover:bg-slate-700/30 transition\"\n >\n <!-- Checkbox -->\n <td class=\"py-3.5 px-4\">\n <input\n type=\"checkbox\"\n :checked=\"selectedUserIds.includes(user.id)\"\n @change=\"toggleUserSelection(user.id)\"\n class=\"rounded border-slate-600 bg-slate-700 text-indigo-600 focus:ring-indigo-500/50\"\n />\n </td>\n <!-- ID -->\n <td class=\"py-3.5 px-4 text-slate-400 font-mono text-xs\">{{ user.id }}</td>\n <!-- Email -->\n <td class=\"py-3.5 px-4\">\n <NuxtLink\n :to=\"'/users/' + user.id\"\n class=\"text-sm font-medium text-white hover:underline cursor-pointer hover:text-indigo-400 transition-colors\"\n >\n {{ user.email }}\n </NuxtLink>\n </td>\n <!-- Name -->\n <td class=\"py-3.5 px-4\">\n <span class=\"text-slate-300\">{{ user.person_name || '—' }}</span>\n </td>\n <!-- Role -->\n <td class=\"py-3.5 px-4\">\n <span\n class=\"inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium\"\n :class=\"roleBadgeClass(user.role)\"\n >\n {{ roleLabel(user.role) }}\n </span>\n </td>\n <!-- Status -->\n <td class=\"py-3.5 px-4\">\n <span\n class=\"inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium\"\n :class=\"statusBadgeClass(user)\"\n >\n <span\n class=\"w-1.5 h-1.5 rounded-full mr-1.5\"\n :class=\"statusDotClass(user)\"\n ></span>\n {{ statusLabel(user) }}\n </span>\n </td>\n <!-- Package -->\n <td class=\"py-3.5 px-4\">\n <span\n class=\"px-2.5 py-0.5 rounded-full text-xs font-medium\"\n :class=\"planBadgeClass(user.subscription_plan)\"\n >\n {{ user.subscription_plan || '—' }}\n </span>\n </td>\n <!-- Registration -->\n <td class=\"py-3.5 px-4 text-slate-300 text-xs\">\n {{ formatDate(user.created_at) }}\n </td>\n <!-- Language -->\n <td class=\"py-3.5 px-4\">\n <span class=\"text-slate-400 text-xs uppercase\">{{ user.preferred_language || '—' }}</span>\n </td>\n <!-- Actions -->\n <td class=\"py-3.5 px-4\">\n <div class=\"flex items-center gap-2\">\n <NuxtLink\n :to=\"'/users/' + user.id\"\n class=\"px-3 py-1.5 text-xs font-medium bg-slate-600/20 text-slate-300 hover:bg-slate-600/30 rounded-lg transition\"\n >\n {{ $t('users.view') }}\n </NuxtLink>\n <button\n v-if=\"!user.is_deleted && canEditUsers\"\n @click=\"toggleUserActive(user)\"\n class=\"px-3 py-1.5 text-xs font-medium rounded-lg transition\"\n :class=\"user.is_active\n ? 'bg-red-500/10 text-red-300 hover:bg-red-500/20'\n : 'bg-emerald-500/10 text-emerald-300 hover:bg-emerald-500/20'\"\n >\n {{ user.is_active ? $t('users.deactivate') : $t('users.activate') }}\n </button>\n </div>\n </td>\n </tr>\n </tbody>\n </table>\n </div>\n\n <!-- Empty State -->\n <div\n v-if=\"users.length === 0 && !loading\"\n class=\"flex flex-col items-center justify-center py-16 text-center\"\n >\n <svg class=\"w-12 h-12 text-slate-600 mb-4\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"1.5\" 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\" />\n </svg>\n <p class=\"text-slate-400 text-sm\">{{ $t('users.no_results') }}</p>\n </div>\n </div>\n\n <!-- Pagination -->\n <div v-if=\"totalCount > limit\" class=\"flex items-center justify-between mt-6\">\n <p class=\"text-xs text-slate-500\">\n {{ $t('users.showing') }} {{ skip + 1 }}–{{ Math.min(skip + limit, totalCount) }} / {{ totalCount }}\n </p>\n <div class=\"flex items-center gap-2\">\n <button\n @click=\"prevPage\"\n :disabled=\"skip === 0\"\n class=\"px-3 py-1.5 text-xs font-medium rounded-lg transition disabled:opacity-30 disabled:cursor-not-allowed\"\n :class=\"skip > 0 ? 'bg-slate-700 text-slate-300 hover:bg-slate-600' : 'bg-slate-800 text-slate-600'\"\n >\n {{ $t('users.prev') }}\n </button>\n <button\n @click=\"nextPage\"\n :disabled=\"skip + limit >= totalCount\"\n class=\"px-3 py-1.5 text-xs font-medium rounded-lg transition disabled:opacity-30 disabled:cursor-not-allowed\"\n :class=\"skip + limit < totalCount ? 'bg-slate-700 text-slate-300 hover:bg-slate-600' : 'bg-slate-800 text-slate-600'\"\n >\n {{ $t('users.next') }}\n </button>\n </div>\n </div>\n </template>\n\n <!-- Notification -->\n <div\n v-if=\"notification\"\n class=\"fixed bottom-6 right-6 px-5 py-3 rounded-xl shadow-xl text-sm font-medium z-50\"\n :class=\"notification.type === 'success'\n ? 'bg-emerald-600 text-white'\n : 'bg-red-600 text-white'\"\n >\n {{ notification.message }}\n </div>\n </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { ref, computed, onMounted, onUnmounted, watch } from 'vue'\nimport { useRouter } from 'vue-router'\nimport { useAuthStore } from '~/stores/auth'\nimport UserTrendChart from '~/components/charts/UserTrendChart.vue'\n\nconst router = useRouter()\nconst auth = useAuthStore()\n\ndefinePageMeta({\n middleware: 'auth',\n})\n\n// ── RBAC Permission Check ──────────────────────────────────────────\n\nconst canViewUsers = computed(() => {\n if (auth.isAdmin) return true\n return !!auth.user?.system_capabilities?.['users:view']\n})\n\nconst canEditUsers = computed(() => {\n if (auth.isAdmin) return true\n return !!auth.user?.system_capabilities?.['users:edit']\n})\n\nconst canBanUsers = computed(() => {\n if (auth.isAdmin) return true\n return !!auth.user?.system_capabilities?.['users:ban']\n})\n\nconst canDeleteUsers = computed(() => {\n if (auth.isAdmin) return true\n return !!auth.user?.system_capabilities?.['users:delete']\n})\n\n// Redirect if no view permission\nif (!canViewUsers.value && !auth.isAdmin) {\n router.push('/')\n}\n\n// ── Types ──────────────────────────────────────────────────────────\n\ninterface UserListItem {\n id: number\n email: string\n role: string\n is_active: boolean\n is_deleted: boolean\n is_vip: boolean\n subscription_plan: string\n preferred_language: string\n region_code: string\n created_at: string | null\n person_name: string | null\n person_phone: string | null\n}\n\ninterface UserListResponse {\n total: number\n skip: number\n limit: number\n users: UserListItem[]\n}\n\ninterface UserStats {\n total_users: number\n active_users: number\n deleted_users: number\n banned_users: number\n new_users_today: number\n new_users_this_week: number\n new_users_this_month: number\n users_by_role: Record<string, number>\n users_by_plan: Record<string, number>\n users_by_language: Record<string, number>\n users_with_person: number\n users_without_person: number\n registration_trend: { date: string; count: number }[]\n active_organizations_count: number\n total_memberships: number\n}\n\n// ── State ──────────────────────────────────────────────────────────\n\nconst loading = ref(true)\nconst error = ref<string | null>(null)\nconst users = ref<UserListItem[]>([])\nconst totalCount = ref(0)\nconst skip = ref(0)\nconst limit = ref(50)\n\nconst searchQuery = ref('')\nconst statusFilter = ref('all')\nconst roleFilter = ref('all')\nconst planFilter = ref('all')\n\nconst stats = ref<UserStats>({\n total_users: 0,\n active_users: 0,\n deleted_users: 0,\n banned_users: 0,\n new_users_today: 0,\n new_users_this_week: 0,\n new_users_this_month: 0,\n users_by_role: {},\n users_by_plan: {},\n users_by_language: {},\n users_with_person: 0,\n users_without_person: 0,\n registration_trend: [],\n active_organizations_count: 0,\n total_memberships: 0,\n})\n\n// Bulk selection\nconst selectedUserIds = ref<number[]>([])\n\n// Notification\nconst notification = ref<{ type: 'success' | 'error'; message: string } | null>(null)\n\n// ── Debounced Search Timer ──────────────────────────────────────────\n\nlet searchDebounceTimer: ReturnType<typeof setTimeout> | null = null\n\nfunction debouncedFetchUsers() {\n if (searchDebounceTimer) {\n clearTimeout(searchDebounceTimer)\n }\n searchDebounceTimer = setTimeout(() => {\n skip.value = 0\n fetchUsers()\n }, 400)\n}\n\n// ── Watchers ────────────────────────────────────────────────────────\n\nwatch(searchQuery, () => {\n debouncedFetchUsers()\n})\n\nwatch(statusFilter, () => {\n skip.value = 0\n fetchUsers()\n})\n\nwatch(roleFilter, () => {\n skip.value = 0\n fetchUsers()\n})\n\nwatch(planFilter, () => {\n skip.value = 0\n fetchUsers()\n})\n\n// ── Computed ───────────────────────────────────────────────────────\n\nconst isFilterActive = computed(() => {\n return searchQuery.value !== '' || statusFilter.value !== 'all' || roleFilter.value !== 'all' || planFilter.value !== 'all'\n})\n\nconst allSelected = computed(() => {\n return users.value.length > 0 && selectedUserIds.value.length === users.value.length\n})\n\n// ── Auth Helper ────────────────────────────────────────────────────\n\nfunction getAuthHeaders() {\n const tokenCookie = useCookie('access_token')\n const token = tokenCookie.value\n return token ? { Authorization: `Bearer ${token}` } : {}\n}\n\n// ── Data Fetching ──────────────────────────────────────────────────\n\nasync function fetchUsers() {\n loading.value = true\n error.value = null\n try {\n const params: Record<string, any> = { skip: skip.value, limit: limit.value }\n\n if (searchQuery.value) {\n params.search_term = searchQuery.value\n }\n // Status filter mapping: frontend uses combined status_filter, backend uses is_active/is_deleted\n if (statusFilter.value !== 'all') {\n switch (statusFilter.value) {\n case 'active':\n params.is_active = true\n break\n case 'inactive':\n params.is_active = false\n break\n case 'deleted':\n params.is_deleted = true\n break\n case 'banned':\n params.is_active = false\n break\n }\n }\n if (roleFilter.value !== 'all') {\n params.role = roleFilter.value\n }\n if (planFilter.value !== 'all') {\n params.plan_filter = planFilter.value\n }\n\n const response = await $fetch<UserListResponse>('/api/v1/admin/users', {\n params,\n headers: getAuthHeaders(),\n })\n users.value = response.users\n totalCount.value = response.total\n } catch (err: any) {\n console.error('[Users] Failed to fetch users:', err)\n error.value = err?.data?.detail || err?.message || 'Failed to load users'\n } finally {\n loading.value = false\n }\n}\n\nasync function fetchStats() {\n try {\n const response = await $fetch<UserStats>('/api/v1/admin/users/stats', {\n headers: getAuthHeaders(),\n })\n stats.value = response\n } catch (err: any) {\n console.error('[Users] Failed to fetch stats:', err)\n }\n}\n\n// ── Pagination ─────────────────────────────────────────────────────\n\nfunction nextPage() {\n if (skip.value + limit.value < totalCount.value) {\n skip.value += limit.value\n fetchUsers()\n }\n}\n\nfunction prevPage() {\n if (skip.value > 0) {\n skip.value = Math.max(0, skip.value - limit.value)\n fetchUsers()\n }\n}\n\n// ── Bulk Selection ─────────────────────────────────────────────────\n\nfunction toggleSelectAll() {\n if (allSelected.value) {\n selectedUserIds.value = []\n } else {\n selectedUserIds.value = users.value.map(u => u.id)\n }\n}\n\nfunction toggleUserSelection(userId: number) {\n const idx = selectedUserIds.value.indexOf(userId)\n if (idx === -1) {\n selectedUserIds.value.push(userId)\n } else {\n selectedUserIds.value.splice(idx, 1)\n }\n}\n\nfunction clearSelection() {\n selectedUserIds.value = []\n}\n\nasync function bulkActivate() {\n if (selectedUserIds.value.length === 0) return\n try {\n await $fetch('/api/v1/admin/users/bulk-action', {\n method: 'POST',\n headers: {\n ...getAuthHeaders(),\n 'Content-Type': 'application/json',\n },\n body: {\n user_ids: selectedUserIds.value,\n action: 'activate',\n },\n })\n showNotification('success', `${selectedUserIds.value.length} user(s) activated successfully`)\n selectedUserIds.value = []\n await fetchUsers()\n await fetchStats()\n } catch (err: any) {\n console.error('[Users] Bulk activate failed:', err)\n showNotification('error', err?.data?.detail || err?.message || 'Bulk activate failed')\n }\n}\n\nasync function bulkDeactivate() {\n if (selectedUserIds.value.length === 0) return\n try {\n await $fetch('/api/v1/admin/users/bulk-action', {\n method: 'POST',\n headers: {\n ...getAuthHeaders(),\n 'Content-Type': 'application/json',\n },\n body: {\n user_ids: selectedUserIds.value,\n action: 'deactivate',\n },\n })\n showNotification('success', `${selectedUserIds.value.length} user(s) deactivated successfully`)\n selectedUserIds.value = []\n await fetchUsers()\n await fetchStats()\n } catch (err: any) {\n console.error('[Users] Bulk deactivate failed:', err)\n showNotification('error', err?.data?.detail || err?.message || 'Bulk deactivate failed')\n }\n}\n\n// ── Single User Toggle ─────────────────────────────────────────────\n\nasync function toggleUserActive(user: UserListItem) {\n const newActive = !user.is_active\n try {\n await $fetch(`/api/v1/admin/users/${user.id}`, {\n method: 'PATCH',\n headers: {\n ...getAuthHeaders(),\n 'Content-Type': 'application/json',\n },\n body: { is_active: newActive },\n })\n showNotification('success', `User \"${user.email}\" ${newActive ? 'activated' : 'deactivated'} successfully`)\n await fetchUsers()\n await fetchStats()\n } catch (err: any) {\n console.error('[Users] Toggle status failed:', err)\n showNotification('error', err?.data?.detail || err?.message || 'Failed to toggle user status')\n }\n}\n\n// ── Helpers ────────────────────────────────────────────────────────\n\nfunction roleBadgeClass(role: string): string {\n const r = role.toLowerCase()\n if (r === 'superadmin') return 'bg-purple-500/20 text-purple-300'\n if (r === 'admin') return 'bg-indigo-500/20 text-indigo-300'\n if (r === 'staff') return 'bg-cyan-500/20 text-cyan-300'\n return 'bg-slate-500/20 text-slate-300'\n}\n\nfunction roleLabel(role: string): string {\n const r = role.toLowerCase()\n if (r === 'superadmin') return 'Superadmin'\n if (r === 'admin') return 'Admin'\n if (r === 'staff') return 'Staff'\n return 'User'\n}\n\nfunction statusBadgeClass(user: UserListItem): string {\n if (user.is_deleted) return 'bg-red-500/10 text-red-400'\n if (!user.is_active) return 'bg-amber-500/10 text-amber-400'\n return 'bg-emerald-500/10 text-emerald-400'\n}\n\nfunction statusDotClass(user: UserListItem): string {\n if (user.is_deleted) return 'bg-red-400'\n if (!user.is_active) return 'bg-amber-400'\n return 'bg-emerald-400'\n}\n\nfunction statusLabel(user: UserListItem): string {\n if (user.is_deleted) return 'Deleted'\n if (!user.is_active) return 'Inactive'\n return 'Active'\n}\n\nfunction planBadgeClass(plan: string): string {\n const p = (plan || '').toLowerCase()\n if (p.includes('enterprise') || p.includes('vip')) return 'bg-purple-500/20 text-purple-300'\n if (p.includes('premium') || p.includes('pro')) return 'bg-amber-500/20 text-amber-300'\n return 'bg-slate-500/20 text-slate-300'\n}\n\nfunction formatDate(dateStr: string | null): string {\n if (!dateStr) return '—'\n try {\n const d = new Date(dateStr)\n return d.toLocaleDateString('hu-HU', {\n year: 'numeric',\n month: 'short',\n day: 'numeric',\n })\n } catch {\n return dateStr\n }\n}\n\nfunction showNotification(type: 'success' | 'error', message: string) {\n notification.value = { type, message }\n setTimeout(() => {\n notification.value = null\n }, 4000)\n}\n\n// ── Lifecycle ──────────────────────────────────────────────────────\n\nonMounted(() => {\n fetchUsers()\n fetchStats()\n})\n</script>\n"],"version":3} |