489 lines
37 KiB
JavaScript
489 lines
37 KiB
JavaScript
import { _ as __nuxt_component_0 } from './nuxt-link-Ci8vU-Yt.mjs';
|
|
import { defineComponent, computed, ref, watch, withCtx, createTextVNode, toDisplayString, mergeProps, useSSRContext } from 'vue';
|
|
import { ssrRenderAttrs, ssrInterpolate, ssrRenderAttr, ssrIncludeBooleanAttr, ssrLooseContain, ssrLooseEqual, ssrRenderComponent, ssrRenderList, ssrRenderClass } from 'vue/server-renderer';
|
|
import { useRouter } from 'vue-router';
|
|
import { c as useAuthStore, d as useCookie } from './server.mjs';
|
|
import '../nitro/nitro.mjs';
|
|
import 'node:http';
|
|
import 'node:https';
|
|
import 'node:events';
|
|
import 'node:buffer';
|
|
import 'node:fs';
|
|
import 'node:path';
|
|
import 'node:crypto';
|
|
import 'node:url';
|
|
import 'pinia';
|
|
import '../routes/renderer.mjs';
|
|
import 'vue-bundle-renderer/runtime';
|
|
import 'unhead/server';
|
|
import 'devalue';
|
|
import 'unhead/utils';
|
|
import 'unhead/plugins';
|
|
|
|
const svgWidth = 800;
|
|
const svgHeight = 220;
|
|
const _sfc_main$1 = /* @__PURE__ */ defineComponent({
|
|
__name: "UserTrendChart",
|
|
__ssrInlineRender: true,
|
|
props: {
|
|
data: {},
|
|
loading: { type: Boolean }
|
|
},
|
|
setup(__props) {
|
|
const props = __props;
|
|
const selectedPeriod = ref("30");
|
|
const hoveredBar = ref(null);
|
|
const padding = { top: 10, right: 16, bottom: 30, left: 36 };
|
|
const filteredData = computed(() => {
|
|
const days = parseInt(selectedPeriod.value, 10);
|
|
if (!props.data || props.data.length === 0) return [];
|
|
const sorted = [...props.data].sort(
|
|
(a, b) => new Date(a.date).getTime() - new Date(b.date).getTime()
|
|
);
|
|
const cutoff = /* @__PURE__ */ new Date();
|
|
cutoff.setDate(cutoff.getDate() - days);
|
|
return sorted.filter((d) => new Date(d.date) >= cutoff);
|
|
});
|
|
const maxCount = computed(() => {
|
|
if (filteredData.value.length === 0) return 1;
|
|
return Math.max(...filteredData.value.map((d) => d.count), 1);
|
|
});
|
|
const chartHeight = svgHeight - padding.top - padding.bottom;
|
|
const gridLines = computed(() => {
|
|
const lines = [];
|
|
for (let i = 0; i <= 4; i++) {
|
|
lines.push(padding.top + chartHeight / 4 * i);
|
|
}
|
|
return lines;
|
|
});
|
|
const yLabels = computed(() => {
|
|
const labels = [];
|
|
for (let i = 0; i <= 4; i++) {
|
|
const y = padding.top + chartHeight / 4 * i;
|
|
const value = Math.round(maxCount.value - maxCount.value / 4 * i);
|
|
labels.push({ y, text: value.toString() });
|
|
}
|
|
return labels;
|
|
});
|
|
const barWidth = computed(() => {
|
|
const count = filteredData.value.length;
|
|
if (count === 0) return 0;
|
|
const availableWidth = svgWidth - padding.left - padding.right;
|
|
const maxBarWidth = 32;
|
|
const calculatedWidth = availableWidth / count * 0.7;
|
|
return Math.min(calculatedWidth, maxBarWidth);
|
|
});
|
|
const visibleBars = computed(() => {
|
|
const count = filteredData.value.length;
|
|
if (count === 0) return [];
|
|
const availableWidth = svgWidth - padding.left - padding.right;
|
|
const gap = count > 1 ? (availableWidth - barWidth.value * count) / (count - 1) : 0;
|
|
return filteredData.value.map((item, idx) => {
|
|
const x = padding.left + idx * (barWidth.value + gap);
|
|
const barHeight = maxCount.value > 0 ? item.count / maxCount.value * chartHeight : 0;
|
|
const y = svgHeight - padding.bottom - barHeight;
|
|
const ratio = maxCount.value > 0 ? item.count / maxCount.value : 0;
|
|
let color;
|
|
if (ratio > 0.8) color = "#6366f1";
|
|
else if (ratio > 0.5) color = "#818cf8";
|
|
else if (ratio > 0.3) color = "#a5b4fc";
|
|
else color = "#c7d2fe";
|
|
const tooltipText = `${item.count} ${formatDate(item.date)}`;
|
|
const tooltipWidth = tooltipText.length * 7 + 16;
|
|
let tooltipX = x + barWidth.value / 2 - tooltipWidth / 2;
|
|
if (tooltipX < padding.left) tooltipX = padding.left;
|
|
if (tooltipX + tooltipWidth > svgWidth - padding.right) tooltipX = svgWidth - padding.right - tooltipWidth;
|
|
const tooltipY = y - 4;
|
|
return {
|
|
x,
|
|
y,
|
|
height: barHeight,
|
|
color,
|
|
tooltipX,
|
|
tooltipY,
|
|
tooltipWidth,
|
|
tooltipText
|
|
};
|
|
});
|
|
});
|
|
const xLabels = computed(() => {
|
|
const count = filteredData.value.length;
|
|
if (count === 0) return [];
|
|
const availableWidth = svgWidth - padding.left - padding.right;
|
|
const gap = count > 1 ? (availableWidth - barWidth.value * count) / (count - 1) : 0;
|
|
let interval = 1;
|
|
if (count > 14) interval = 2;
|
|
if (count > 30) interval = 5;
|
|
if (count > 60) interval = 10;
|
|
return filteredData.value.map((item, idx) => {
|
|
const x = padding.left + idx * (barWidth.value + gap);
|
|
const d = new Date(item.date);
|
|
const label = `${d.getMonth() + 1}/${d.getDate()}`;
|
|
return { x, label, idx };
|
|
}).filter((_, i) => i % interval === 0 || i === count - 1);
|
|
});
|
|
const averageCount = computed(() => {
|
|
if (filteredData.value.length === 0) return 0;
|
|
const sum = filteredData.value.reduce((acc, d) => acc + d.count, 0);
|
|
return Math.round(sum / filteredData.value.length);
|
|
});
|
|
const totalCount = computed(() => {
|
|
return filteredData.value.reduce((acc, d) => acc + d.count, 0);
|
|
});
|
|
const trendDirection = computed(() => {
|
|
const data = filteredData.value;
|
|
if (data.length < 4) return "stable";
|
|
const half = Math.floor(data.length / 2);
|
|
const firstHalf = data.slice(0, half).reduce((acc, d) => acc + d.count, 0) / half;
|
|
const secondHalf = data.slice(half).reduce((acc, d) => acc + d.count, 0) / (data.length - half);
|
|
if (secondHalf > firstHalf * 1.05) return "up";
|
|
if (secondHalf < firstHalf * 0.95) return "down";
|
|
return "stable";
|
|
});
|
|
const trendPercent = computed(() => {
|
|
const data = filteredData.value;
|
|
if (data.length < 4) return 0;
|
|
const half = Math.floor(data.length / 2);
|
|
const firstHalf = data.slice(0, half).reduce((acc, d) => acc + d.count, 0) / half;
|
|
if (firstHalf === 0) return 100;
|
|
const secondHalf = data.slice(half).reduce((acc, d) => acc + d.count, 0) / (data.length - half);
|
|
return Math.round(Math.abs((secondHalf - firstHalf) / firstHalf) * 100);
|
|
});
|
|
function formatDate(dateStr) {
|
|
try {
|
|
const d = new Date(dateStr);
|
|
return `${d.getMonth() + 1}/${d.getDate()}`;
|
|
} catch {
|
|
return dateStr;
|
|
}
|
|
}
|
|
function updateChart() {
|
|
hoveredBar.value = null;
|
|
}
|
|
watch(() => props.data, () => {
|
|
updateChart();
|
|
});
|
|
return (_ctx, _push, _parent, _attrs) => {
|
|
_push(`<div${ssrRenderAttrs(mergeProps({ class: "bg-slate-800 rounded-xl border border-slate-700 p-5" }, _attrs))}><div class="flex items-center justify-between mb-4"><div><h3 class="text-sm font-semibold text-white">${ssrInterpolate(_ctx.$t("users.trend_title"))}</h3><p class="text-xs text-slate-400 mt-0.5">${ssrInterpolate(_ctx.$t("users.trend_subtitle"))}</p></div><select 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"><option value="7"${ssrIncludeBooleanAttr(Array.isArray(selectedPeriod.value) ? ssrLooseContain(selectedPeriod.value, "7") : ssrLooseEqual(selectedPeriod.value, "7")) ? " selected" : ""}>${ssrInterpolate(_ctx.$t("users.trend_7d"))}</option><option value="14"${ssrIncludeBooleanAttr(Array.isArray(selectedPeriod.value) ? ssrLooseContain(selectedPeriod.value, "14") : ssrLooseEqual(selectedPeriod.value, "14")) ? " selected" : ""}>${ssrInterpolate(_ctx.$t("users.trend_14d"))}</option><option value="30"${ssrIncludeBooleanAttr(Array.isArray(selectedPeriod.value) ? ssrLooseContain(selectedPeriod.value, "30") : ssrLooseEqual(selectedPeriod.value, "30")) ? " selected" : ""}>${ssrInterpolate(_ctx.$t("users.trend_30d"))}</option><option value="90"${ssrIncludeBooleanAttr(Array.isArray(selectedPeriod.value) ? ssrLooseContain(selectedPeriod.value, "90") : ssrLooseEqual(selectedPeriod.value, "90")) ? " selected" : ""}>${ssrInterpolate(_ctx.$t("users.trend_90d"))}</option></select></div>`);
|
|
if (__props.loading) {
|
|
_push(`<div class="flex items-center justify-center py-12"><svg class="w-6 h-6 text-indigo-400 animate-spin" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path></svg></div>`);
|
|
} else if (filteredData.value.length === 0) {
|
|
_push(`<div class="flex flex-col items-center justify-center py-12 text-center"><svg class="w-10 h-10 text-slate-600 mb-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><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"></path></svg><p class="text-slate-500 text-xs">${ssrInterpolate(_ctx.$t("users.trend_no_data"))}</p></div>`);
|
|
} else {
|
|
_push(`<div class="relative"><svg${ssrRenderAttr("viewBox", `0 0 ${svgWidth} ${svgHeight}`)} class="w-full h-auto overflow-visible" preserveAspectRatio="xMidYMid meet"><!--[-->`);
|
|
ssrRenderList(gridLines.value, (y, yi) => {
|
|
_push(`<line${ssrRenderAttr("x1", padding.left)}${ssrRenderAttr("y1", y)}${ssrRenderAttr("x2", svgWidth - padding.right)}${ssrRenderAttr("y2", y)} stroke="currentColor" class="text-slate-700/50" stroke-width="1" stroke-dasharray="4,4"></line>`);
|
|
});
|
|
_push(`<!--]--><!--[-->`);
|
|
ssrRenderList(yLabels.value, (label, li) => {
|
|
_push(`<text${ssrRenderAttr("x", padding.left - 8)}${ssrRenderAttr("y", label.y + 4)} text-anchor="end" class="fill-slate-500" font-size="10">${ssrInterpolate(label.text)}</text>`);
|
|
});
|
|
_push(`<!--]--><!--[-->`);
|
|
ssrRenderList(visibleBars.value, (item, idx) => {
|
|
_push(`<g class="chart-bar-group"><rect${ssrRenderAttr("x", item.x)}${ssrRenderAttr("y", item.y)}${ssrRenderAttr("width", barWidth.value)}${ssrRenderAttr("height", item.height)}${ssrRenderAttr("rx", 3)}${ssrRenderAttr("ry", 3)}${ssrRenderAttr("fill", item.color)}${ssrRenderAttr("opacity", hoveredBar.value === idx ? 0.9 : 0.7)} class="transition-all duration-200 cursor-pointer"></rect>`);
|
|
if (hoveredBar.value === idx) {
|
|
_push(`<g><rect${ssrRenderAttr("x", item.tooltipX)}${ssrRenderAttr("y", item.tooltipY - 28)}${ssrRenderAttr("width", item.tooltipWidth)} height="22" rx="4" fill="#1e293b" stroke="#475569" stroke-width="1"></rect><text${ssrRenderAttr("x", item.tooltipX + item.tooltipWidth / 2)}${ssrRenderAttr("y", item.tooltipY - 13)} text-anchor="middle" fill="#e2e8f0" font-size="11" font-weight="600">${ssrInterpolate(item.tooltipText)}</text></g>`);
|
|
} else {
|
|
_push(`<!---->`);
|
|
}
|
|
_push(`</g>`);
|
|
});
|
|
_push(`<!--]--><!--[-->`);
|
|
ssrRenderList(xLabels.value, (item, xi) => {
|
|
_push(`<text${ssrRenderAttr("x", item.x + barWidth.value / 2)}${ssrRenderAttr("y", svgHeight - padding.bottom + 16)} text-anchor="middle" class="fill-slate-500" font-size="9">${ssrInterpolate(item.label)}</text>`);
|
|
});
|
|
_push(`<!--]--></svg><div class="flex items-center justify-between mt-4 pt-3 border-t border-slate-700/50"><div class="flex items-center gap-4 text-xs"><div class="flex items-center gap-1.5"><span class="w-2.5 h-2.5 rounded-sm bg-indigo-500/70"></span><span class="text-slate-400">${ssrInterpolate(_ctx.$t("users.trend_daily"))}</span></div><div><span class="text-slate-400">${ssrInterpolate(_ctx.$t("users.trend_avg"))}:</span><span class="text-white font-semibold ml-1">${ssrInterpolate(averageCount.value)}</span></div><div><span class="text-slate-400">${ssrInterpolate(_ctx.$t("users.trend_total"))}:</span><span class="text-white font-semibold ml-1">${ssrInterpolate(totalCount.value)}</span></div></div>`);
|
|
if (trendDirection.value !== "stable") {
|
|
_push(`<div class="flex items-center gap-1 text-xs">`);
|
|
if (trendDirection.value === "up") {
|
|
_push(`<svg class="w-3.5 h-3.5 text-emerald-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7h8m0 0v8m0-8l-8 8-4-4-6 6"></path></svg>`);
|
|
} else {
|
|
_push(`<svg class="w-3.5 h-3.5 text-red-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 17h8m0 0V9m0 8l-8-8-4 4-6-6"></path></svg>`);
|
|
}
|
|
_push(`<span class="${ssrRenderClass([trendDirection.value === "up" ? "text-emerald-400" : "text-red-400", "font-medium"])}">${ssrInterpolate(trendPercent.value)}% </span><span class="text-slate-500">${ssrInterpolate(_ctx.$t("users.trend_vs_prev"))}</span></div>`);
|
|
} else {
|
|
_push(`<!---->`);
|
|
}
|
|
_push(`</div></div>`);
|
|
}
|
|
_push(`</div>`);
|
|
};
|
|
}
|
|
});
|
|
const _sfc_setup$1 = _sfc_main$1.setup;
|
|
_sfc_main$1.setup = (props, ctx) => {
|
|
const ssrContext = useSSRContext();
|
|
(ssrContext.modules || (ssrContext.modules = /* @__PURE__ */ new Set())).add("components/charts/UserTrendChart.vue");
|
|
return _sfc_setup$1 ? _sfc_setup$1(props, ctx) : void 0;
|
|
};
|
|
const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
__name: "index",
|
|
__ssrInlineRender: true,
|
|
setup(__props) {
|
|
const router = useRouter();
|
|
const auth = useAuthStore();
|
|
const canViewUsers = computed(() => {
|
|
var _a, _b;
|
|
if (auth.isAdmin) return true;
|
|
return !!((_b = (_a = auth.user) == null ? void 0 : _a.system_capabilities) == null ? void 0 : _b["users:view"]);
|
|
});
|
|
const canEditUsers = computed(() => {
|
|
var _a, _b;
|
|
if (auth.isAdmin) return true;
|
|
return !!((_b = (_a = auth.user) == null ? void 0 : _a.system_capabilities) == null ? void 0 : _b["users:edit"]);
|
|
});
|
|
computed(() => {
|
|
var _a, _b;
|
|
if (auth.isAdmin) return true;
|
|
return !!((_b = (_a = auth.user) == null ? void 0 : _a.system_capabilities) == null ? void 0 : _b["users:ban"]);
|
|
});
|
|
computed(() => {
|
|
var _a, _b;
|
|
if (auth.isAdmin) return true;
|
|
return !!((_b = (_a = auth.user) == null ? void 0 : _a.system_capabilities) == null ? void 0 : _b["users:delete"]);
|
|
});
|
|
if (!canViewUsers.value && !auth.isAdmin) {
|
|
router.push("/");
|
|
}
|
|
const loading = ref(true);
|
|
const error = ref(null);
|
|
const users = ref([]);
|
|
const totalCount = ref(0);
|
|
const skip = ref(0);
|
|
const limit = ref(50);
|
|
const searchQuery = ref("");
|
|
const statusFilter = ref("all");
|
|
const roleFilter = ref("all");
|
|
const planFilter = ref("all");
|
|
const stats = ref({
|
|
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,
|
|
registration_trend: [],
|
|
active_organizations_count: 0,
|
|
total_memberships: 0
|
|
});
|
|
const selectedUserIds = ref([]);
|
|
const notification = ref(null);
|
|
let searchDebounceTimer = null;
|
|
function debouncedFetchUsers() {
|
|
if (searchDebounceTimer) {
|
|
clearTimeout(searchDebounceTimer);
|
|
}
|
|
searchDebounceTimer = setTimeout(() => {
|
|
skip.value = 0;
|
|
fetchUsers();
|
|
}, 400);
|
|
}
|
|
watch(searchQuery, () => {
|
|
debouncedFetchUsers();
|
|
});
|
|
watch(statusFilter, () => {
|
|
skip.value = 0;
|
|
fetchUsers();
|
|
});
|
|
watch(roleFilter, () => {
|
|
skip.value = 0;
|
|
fetchUsers();
|
|
});
|
|
watch(planFilter, () => {
|
|
skip.value = 0;
|
|
fetchUsers();
|
|
});
|
|
const isFilterActive = computed(() => {
|
|
return searchQuery.value !== "" || statusFilter.value !== "all" || roleFilter.value !== "all" || planFilter.value !== "all";
|
|
});
|
|
const allSelected = computed(() => {
|
|
return users.value.length > 0 && selectedUserIds.value.length === users.value.length;
|
|
});
|
|
function getAuthHeaders() {
|
|
const tokenCookie = useCookie("access_token");
|
|
const token = tokenCookie.value;
|
|
return token ? { Authorization: `Bearer ${token}` } : {};
|
|
}
|
|
async function fetchUsers() {
|
|
var _a;
|
|
loading.value = true;
|
|
error.value = null;
|
|
try {
|
|
const params = { skip: skip.value, limit: limit.value };
|
|
if (searchQuery.value) {
|
|
params.search_term = searchQuery.value;
|
|
}
|
|
if (statusFilter.value !== "all") {
|
|
params.status_filter = statusFilter.value;
|
|
}
|
|
if (roleFilter.value !== "all") {
|
|
params.role_filter = roleFilter.value;
|
|
}
|
|
if (planFilter.value !== "all") {
|
|
params.plan_filter = planFilter.value;
|
|
}
|
|
const response = await $fetch("/api/v1/admin/users", {
|
|
params,
|
|
headers: getAuthHeaders()
|
|
});
|
|
users.value = response.users;
|
|
totalCount.value = response.total;
|
|
} catch (err) {
|
|
console.error("[Users] Failed to fetch users:", err);
|
|
error.value = ((_a = err == null ? void 0 : err.data) == null ? void 0 : _a.detail) || (err == null ? void 0 : err.message) || "Failed to load users";
|
|
} finally {
|
|
loading.value = false;
|
|
}
|
|
}
|
|
function roleBadgeClass(role) {
|
|
const r = role.toLowerCase();
|
|
if (r === "superadmin") return "bg-purple-500/20 text-purple-300";
|
|
if (r === "admin") return "bg-indigo-500/20 text-indigo-300";
|
|
if (r === "staff") return "bg-cyan-500/20 text-cyan-300";
|
|
return "bg-slate-500/20 text-slate-300";
|
|
}
|
|
function roleLabel(role) {
|
|
const r = role.toLowerCase();
|
|
if (r === "superadmin") return "Superadmin";
|
|
if (r === "admin") return "Admin";
|
|
if (r === "staff") return "Staff";
|
|
return "User";
|
|
}
|
|
function statusBadgeClass(user) {
|
|
if (user.is_deleted) return "bg-red-500/10 text-red-400";
|
|
if (!user.is_active) return "bg-amber-500/10 text-amber-400";
|
|
return "bg-emerald-500/10 text-emerald-400";
|
|
}
|
|
function statusDotClass(user) {
|
|
if (user.is_deleted) return "bg-red-400";
|
|
if (!user.is_active) return "bg-amber-400";
|
|
return "bg-emerald-400";
|
|
}
|
|
function statusLabel(user) {
|
|
if (user.is_deleted) return "Deleted";
|
|
if (!user.is_active) return "Inactive";
|
|
return "Active";
|
|
}
|
|
function planBadgeClass(plan) {
|
|
const p = (plan || "").toLowerCase();
|
|
if (p.includes("enterprise") || p.includes("vip")) return "bg-purple-500/20 text-purple-300";
|
|
if (p.includes("premium") || p.includes("pro")) return "bg-amber-500/20 text-amber-300";
|
|
return "bg-slate-500/20 text-slate-300";
|
|
}
|
|
function formatDate(dateStr) {
|
|
if (!dateStr) return "\u2014";
|
|
try {
|
|
const d = new Date(dateStr);
|
|
return d.toLocaleDateString("hu-HU", {
|
|
year: "numeric",
|
|
month: "short",
|
|
day: "numeric"
|
|
});
|
|
} catch {
|
|
return dateStr;
|
|
}
|
|
}
|
|
return (_ctx, _push, _parent, _attrs) => {
|
|
const _component_NuxtLink = __nuxt_component_0;
|
|
_push(`<div${ssrRenderAttrs(_attrs)}><div class="mb-8"><h1 class="text-2xl font-bold text-white">${ssrInterpolate(_ctx.$t("users.title"))}</h1><p class="text-slate-400 mt-1">${ssrInterpolate(_ctx.$t("users.subtitle"))}</p></div><div class="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4 mb-6"><div class="relative flex-1 max-w-md"><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"><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"></path></svg><input${ssrRenderAttr("value", searchQuery.value)} type="text"${ssrRenderAttr("placeholder", _ctx.$t("users.search_placeholder"))} 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"></div><div class="flex items-center gap-2 flex-wrap"><select 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"><option value="all"${ssrIncludeBooleanAttr(Array.isArray(statusFilter.value) ? ssrLooseContain(statusFilter.value, "all") : ssrLooseEqual(statusFilter.value, "all")) ? " selected" : ""}>${ssrInterpolate(_ctx.$t("users.all_statuses"))}</option><option value="active"${ssrIncludeBooleanAttr(Array.isArray(statusFilter.value) ? ssrLooseContain(statusFilter.value, "active") : ssrLooseEqual(statusFilter.value, "active")) ? " selected" : ""}>${ssrInterpolate(_ctx.$t("users.status_active"))}</option><option value="inactive"${ssrIncludeBooleanAttr(Array.isArray(statusFilter.value) ? ssrLooseContain(statusFilter.value, "inactive") : ssrLooseEqual(statusFilter.value, "inactive")) ? " selected" : ""}>${ssrInterpolate(_ctx.$t("users.status_inactive"))}</option><option value="deleted"${ssrIncludeBooleanAttr(Array.isArray(statusFilter.value) ? ssrLooseContain(statusFilter.value, "deleted") : ssrLooseEqual(statusFilter.value, "deleted")) ? " selected" : ""}>${ssrInterpolate(_ctx.$t("users.status_deleted"))}</option><option value="banned"${ssrIncludeBooleanAttr(Array.isArray(statusFilter.value) ? ssrLooseContain(statusFilter.value, "banned") : ssrLooseEqual(statusFilter.value, "banned")) ? " selected" : ""}>${ssrInterpolate(_ctx.$t("users.status_banned"))}</option></select><select 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"><option value="all"${ssrIncludeBooleanAttr(Array.isArray(roleFilter.value) ? ssrLooseContain(roleFilter.value, "all") : ssrLooseEqual(roleFilter.value, "all")) ? " selected" : ""}>${ssrInterpolate(_ctx.$t("users.all_roles"))}</option><option value="user"${ssrIncludeBooleanAttr(Array.isArray(roleFilter.value) ? ssrLooseContain(roleFilter.value, "user") : ssrLooseEqual(roleFilter.value, "user")) ? " selected" : ""}>${ssrInterpolate(_ctx.$t("users.role_user"))}</option><option value="admin"${ssrIncludeBooleanAttr(Array.isArray(roleFilter.value) ? ssrLooseContain(roleFilter.value, "admin") : ssrLooseEqual(roleFilter.value, "admin")) ? " selected" : ""}>${ssrInterpolate(_ctx.$t("users.role_admin"))}</option><option value="staff"${ssrIncludeBooleanAttr(Array.isArray(roleFilter.value) ? ssrLooseContain(roleFilter.value, "staff") : ssrLooseEqual(roleFilter.value, "staff")) ? " selected" : ""}>${ssrInterpolate(_ctx.$t("users.role_staff"))}</option><option value="superadmin"${ssrIncludeBooleanAttr(Array.isArray(roleFilter.value) ? ssrLooseContain(roleFilter.value, "superadmin") : ssrLooseEqual(roleFilter.value, "superadmin")) ? " selected" : ""}>${ssrInterpolate(_ctx.$t("users.role_superadmin"))}</option></select><select 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"><option value="all"${ssrIncludeBooleanAttr(Array.isArray(planFilter.value) ? ssrLooseContain(planFilter.value, "all") : ssrLooseEqual(planFilter.value, "all")) ? " selected" : ""}>${ssrInterpolate(_ctx.$t("users.all_plans"))}</option><option value="FREE"${ssrIncludeBooleanAttr(Array.isArray(planFilter.value) ? ssrLooseContain(planFilter.value, "FREE") : ssrLooseEqual(planFilter.value, "FREE")) ? " selected" : ""}>${ssrInterpolate(_ctx.$t("users.plan_free"))}</option><option value="PREMIUM"${ssrIncludeBooleanAttr(Array.isArray(planFilter.value) ? ssrLooseContain(planFilter.value, "PREMIUM") : ssrLooseEqual(planFilter.value, "PREMIUM")) ? " selected" : ""}>${ssrInterpolate(_ctx.$t("users.plan_premium"))}</option><option value="ENTERPRISE"${ssrIncludeBooleanAttr(Array.isArray(planFilter.value) ? ssrLooseContain(planFilter.value, "ENTERPRISE") : ssrLooseEqual(planFilter.value, "ENTERPRISE")) ? " selected" : ""}>${ssrInterpolate(_ctx.$t("users.plan_enterprise"))}</option></select></div></div>`);
|
|
if (loading.value) {
|
|
_push(`<div class="flex items-center justify-center py-20"><div class="flex flex-col items-center gap-3"><svg class="w-8 h-8 text-indigo-400 animate-spin" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path></svg><p class="text-slate-400 text-sm">${ssrInterpolate(_ctx.$t("users.loading"))}</p></div></div>`);
|
|
} else if (error.value) {
|
|
_push(`<div class="flex flex-col items-center justify-center py-20 text-center"><svg class="w-12 h-12 text-red-500 mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><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"></path></svg><p class="text-red-400 text-sm mb-2">${ssrInterpolate(error.value)}</p><button class="px-4 py-2 text-sm bg-indigo-600/20 text-indigo-300 hover:bg-indigo-600/30 rounded-lg transition">${ssrInterpolate(_ctx.$t("users.retry"))}</button></div>`);
|
|
} else {
|
|
_push(`<!--[--><div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-5 gap-4 mb-8"><div class="bg-slate-800 rounded-xl border border-slate-700 p-4"><div class="flex items-center gap-3"><div class="p-2 rounded-lg bg-indigo-500/10 text-indigo-400"><svg class="w-5 h-5" 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 7a4 4 0 11-8 0 4 4 0 018 0z"></path></svg></div><div><p class="text-xs text-slate-400 uppercase tracking-wider">${ssrInterpolate(_ctx.$t("users.total_users"))}</p><p class="text-2xl font-bold text-white">${ssrInterpolate(stats.value.total_users)}</p></div></div></div><div class="bg-slate-800 rounded-xl border border-slate-700 p-4"><div class="flex items-center gap-3"><div class="p-2 rounded-lg bg-emerald-500/10 text-emerald-400"><svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><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"></path></svg></div><div><p class="text-xs text-slate-400 uppercase tracking-wider">${ssrInterpolate(_ctx.$t("users.active_users"))}</p><p class="text-2xl font-bold text-emerald-400">${ssrInterpolate(stats.value.active_users)}</p></div></div></div><div class="bg-slate-800 rounded-xl border border-slate-700 p-4"><div class="flex items-center gap-3"><div class="p-2 rounded-lg bg-red-500/10 text-red-400"><svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><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"></path></svg></div><div><p class="text-xs text-slate-400 uppercase tracking-wider">${ssrInterpolate(_ctx.$t("users.deleted_users"))}</p><p class="text-2xl font-bold text-red-400">${ssrInterpolate(stats.value.deleted_users)}</p></div></div></div><div class="bg-slate-800 rounded-xl border border-slate-700 p-4"><div class="flex items-center gap-3"><div class="p-2 rounded-lg bg-amber-500/10 text-amber-400"><svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><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"></path></svg></div><div><p class="text-xs text-slate-400 uppercase tracking-wider">${ssrInterpolate(_ctx.$t("users.banned_users"))}</p><p class="text-2xl font-bold text-amber-400">${ssrInterpolate(stats.value.banned_users)}</p></div></div></div><div class="bg-slate-800 rounded-xl border border-slate-700 p-4"><div class="flex items-center gap-3"><div class="p-2 rounded-lg bg-cyan-500/10 text-cyan-400"><svg class="w-5 h-5" 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"></path></svg></div><div><p class="text-xs text-slate-400 uppercase tracking-wider">${ssrInterpolate(_ctx.$t("users.new_today"))}</p><p class="text-2xl font-bold text-cyan-400">${ssrInterpolate(stats.value.new_users_today)}</p></div></div></div></div><div class="mb-8">`);
|
|
_push(ssrRenderComponent(_sfc_main$1, {
|
|
data: stats.value.registration_trend,
|
|
loading: loading.value
|
|
}, null, _parent));
|
|
_push(`</div>`);
|
|
if (isFilterActive.value) {
|
|
_push(`<div class="mb-4 text-xs text-slate-500">${ssrInterpolate(_ctx.$t("users.filtered_results"))}: <span class="text-slate-300 font-semibold">${ssrInterpolate(users.value.length)}</span> / ${ssrInterpolate(totalCount.value)} db </div>`);
|
|
} else {
|
|
_push(`<!---->`);
|
|
}
|
|
if (selectedUserIds.value.length > 0 && canEditUsers.value) {
|
|
_push(`<div class="mb-4 px-4 py-3 bg-indigo-900/30 border border-indigo-700/50 rounded-xl flex items-center justify-between"><p class="text-sm text-indigo-300">${ssrInterpolate(_ctx.$t("users.selected_count", { count: selectedUserIds.value.length }))}</p><div class="flex items-center gap-2"><button 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">${ssrInterpolate(_ctx.$t("users.bulk_activate"))}</button><button 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">${ssrInterpolate(_ctx.$t("users.bulk_deactivate"))}</button><button 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">${ssrInterpolate(_ctx.$t("users.clear_selection"))}</button></div></div>`);
|
|
} else {
|
|
_push(`<!---->`);
|
|
}
|
|
_push(`<div class="bg-slate-800/50 border border-slate-700 rounded-xl overflow-hidden"><div class="overflow-x-auto"><table class="w-full text-sm"><thead><tr class="border-b border-slate-700 bg-slate-800/80"><th class="text-left py-3.5 px-4 w-10"><input type="checkbox"${ssrIncludeBooleanAttr(allSelected.value) ? " checked" : ""} class="rounded border-slate-600 bg-slate-700 text-indigo-600 focus:ring-indigo-500/50"></th><th class="text-left py-3.5 px-4 text-xs font-semibold uppercase tracking-wider text-slate-400">ID</th><th class="text-left py-3.5 px-4 text-xs font-semibold uppercase tracking-wider text-slate-400">${ssrInterpolate(_ctx.$t("users.email"))}</th><th class="text-left py-3.5 px-4 text-xs font-semibold uppercase tracking-wider text-slate-400">${ssrInterpolate(_ctx.$t("users.name"))}</th><th class="text-left py-3.5 px-4 text-xs font-semibold uppercase tracking-wider text-slate-400">${ssrInterpolate(_ctx.$t("users.role"))}</th><th class="text-left py-3.5 px-4 text-xs font-semibold uppercase tracking-wider text-slate-400">${ssrInterpolate(_ctx.$t("users.status"))}</th><th class="text-left py-3.5 px-4 text-xs font-semibold uppercase tracking-wider text-slate-400">${ssrInterpolate(_ctx.$t("users.package"))}</th><th class="text-left py-3.5 px-4 text-xs font-semibold uppercase tracking-wider text-slate-400">${ssrInterpolate(_ctx.$t("users.registration"))}</th><th class="text-left py-3.5 px-4 text-xs font-semibold uppercase tracking-wider text-slate-400">${ssrInterpolate(_ctx.$t("users.language"))}</th><th class="text-left py-3.5 px-4 text-xs font-semibold uppercase tracking-wider text-slate-400">${ssrInterpolate(_ctx.$t("users.actions"))}</th></tr></thead><tbody><!--[-->`);
|
|
ssrRenderList(users.value, (user) => {
|
|
_push(`<tr class="border-b border-slate-700/50 hover:bg-slate-700/30 transition"><td class="py-3.5 px-4"><input type="checkbox"${ssrIncludeBooleanAttr(selectedUserIds.value.includes(user.id)) ? " checked" : ""} class="rounded border-slate-600 bg-slate-700 text-indigo-600 focus:ring-indigo-500/50"></td><td class="py-3.5 px-4 text-slate-400 font-mono text-xs">${ssrInterpolate(user.id)}</td><td class="py-3.5 px-4">`);
|
|
_push(ssrRenderComponent(_component_NuxtLink, {
|
|
to: "/users/" + user.id,
|
|
class: "text-sm font-medium text-white hover:underline cursor-pointer hover:text-indigo-400 transition-colors"
|
|
}, {
|
|
default: withCtx((_, _push2, _parent2, _scopeId) => {
|
|
if (_push2) {
|
|
_push2(`${ssrInterpolate(user.email)}`);
|
|
} else {
|
|
return [
|
|
createTextVNode(toDisplayString(user.email), 1)
|
|
];
|
|
}
|
|
}),
|
|
_: 2
|
|
}, _parent));
|
|
_push(`</td><td class="py-3.5 px-4"><span class="text-slate-300">${ssrInterpolate(user.person_name || "\u2014")}</span></td><td class="py-3.5 px-4"><span class="${ssrRenderClass([roleBadgeClass(user.role), "inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium"])}">${ssrInterpolate(roleLabel(user.role))}</span></td><td class="py-3.5 px-4"><span class="${ssrRenderClass([statusBadgeClass(user), "inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium"])}"><span class="${ssrRenderClass([statusDotClass(user), "w-1.5 h-1.5 rounded-full mr-1.5"])}"></span> ${ssrInterpolate(statusLabel(user))}</span></td><td class="py-3.5 px-4"><span class="${ssrRenderClass([planBadgeClass(user.subscription_plan), "px-2.5 py-0.5 rounded-full text-xs font-medium"])}">${ssrInterpolate(user.subscription_plan || "\u2014")}</span></td><td class="py-3.5 px-4 text-slate-300 text-xs">${ssrInterpolate(formatDate(user.created_at))}</td><td class="py-3.5 px-4"><span class="text-slate-400 text-xs uppercase">${ssrInterpolate(user.preferred_language || "\u2014")}</span></td><td class="py-3.5 px-4"><div class="flex items-center gap-2">`);
|
|
_push(ssrRenderComponent(_component_NuxtLink, {
|
|
to: "/users/" + user.id,
|
|
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"
|
|
}, {
|
|
default: withCtx((_, _push2, _parent2, _scopeId) => {
|
|
if (_push2) {
|
|
_push2(`${ssrInterpolate(_ctx.$t("users.view"))}`);
|
|
} else {
|
|
return [
|
|
createTextVNode(toDisplayString(_ctx.$t("users.view")), 1)
|
|
];
|
|
}
|
|
}),
|
|
_: 2
|
|
}, _parent));
|
|
if (!user.is_deleted && canEditUsers.value) {
|
|
_push(`<button class="${ssrRenderClass([user.is_active ? "bg-red-500/10 text-red-300 hover:bg-red-500/20" : "bg-emerald-500/10 text-emerald-300 hover:bg-emerald-500/20", "px-3 py-1.5 text-xs font-medium rounded-lg transition"])}">${ssrInterpolate(user.is_active ? _ctx.$t("users.deactivate") : _ctx.$t("users.activate"))}</button>`);
|
|
} else {
|
|
_push(`<!---->`);
|
|
}
|
|
_push(`</div></td></tr>`);
|
|
});
|
|
_push(`<!--]--></tbody></table></div>`);
|
|
if (users.value.length === 0 && !loading.value) {
|
|
_push(`<div class="flex flex-col items-center justify-center py-16 text-center"><svg class="w-12 h-12 text-slate-600 mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><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"></path></svg><p class="text-slate-400 text-sm">${ssrInterpolate(_ctx.$t("users.no_results"))}</p></div>`);
|
|
} else {
|
|
_push(`<!---->`);
|
|
}
|
|
_push(`</div>`);
|
|
if (totalCount.value > limit.value) {
|
|
_push(`<div class="flex items-center justify-between mt-6"><p class="text-xs text-slate-500">${ssrInterpolate(_ctx.$t("users.showing"))} ${ssrInterpolate(skip.value + 1)}\u2013${ssrInterpolate(Math.min(skip.value + limit.value, totalCount.value))} / ${ssrInterpolate(totalCount.value)}</p><div class="flex items-center gap-2"><button${ssrIncludeBooleanAttr(skip.value === 0) ? " disabled" : ""} class="${ssrRenderClass([skip.value > 0 ? "bg-slate-700 text-slate-300 hover:bg-slate-600" : "bg-slate-800 text-slate-600", "px-3 py-1.5 text-xs font-medium rounded-lg transition disabled:opacity-30 disabled:cursor-not-allowed"])}">${ssrInterpolate(_ctx.$t("users.prev"))}</button><button${ssrIncludeBooleanAttr(skip.value + limit.value >= totalCount.value) ? " disabled" : ""} class="${ssrRenderClass([skip.value + limit.value < totalCount.value ? "bg-slate-700 text-slate-300 hover:bg-slate-600" : "bg-slate-800 text-slate-600", "px-3 py-1.5 text-xs font-medium rounded-lg transition disabled:opacity-30 disabled:cursor-not-allowed"])}">${ssrInterpolate(_ctx.$t("users.next"))}</button></div></div>`);
|
|
} else {
|
|
_push(`<!---->`);
|
|
}
|
|
_push(`<!--]-->`);
|
|
}
|
|
if (notification.value) {
|
|
_push(`<div class="${ssrRenderClass([notification.value.type === "success" ? "bg-emerald-600 text-white" : "bg-red-600 text-white", "fixed bottom-6 right-6 px-5 py-3 rounded-xl shadow-xl text-sm font-medium z-50"])}">${ssrInterpolate(notification.value.message)}</div>`);
|
|
} else {
|
|
_push(`<!---->`);
|
|
}
|
|
_push(`</div>`);
|
|
};
|
|
}
|
|
});
|
|
const _sfc_setup = _sfc_main.setup;
|
|
_sfc_main.setup = (props, ctx) => {
|
|
const ssrContext = useSSRContext();
|
|
(ssrContext.modules || (ssrContext.modules = /* @__PURE__ */ new Set())).add("pages/users/index.vue");
|
|
return _sfc_setup ? _sfc_setup(props, ctx) : void 0;
|
|
};
|
|
|
|
export { _sfc_main as default };
|
|
//# sourceMappingURL=index-BXFH9zj5.mjs.map
|