From f511a3c31a0db480a43b9ac4cdfc8933b9b93f8c Mon Sep 17 00:00:00 2001 From: Tracewebstudio Dev Date: Sun, 14 Jun 2026 20:28:07 +0200 Subject: [PATCH] feat: add AI management page to admin panel New AI Management page (/admin/ai-management): - Overview tab: AI users, daily/monthly generations, active plans - User Usage tab: searchable table with plan, usage, addon balance - Plans tab: grid of available AI plans Sidebar: - Added AI Management nav item with Sparkles icon Dashboard: - Added kpi_ai_users widget - Added kpi_ai_generations_today widget Files: - src/routes/admin/ai-management.tsx (new) --- src/components/AdminSidebar.tsx | 7 + src/lib/admin/dashboard.ts | 78 +++++++ src/routes/admin/ai-management.tsx | 342 +++++++++++++++++++++++++++++ 3 files changed, 427 insertions(+) create mode 100644 src/routes/admin/ai-management.tsx diff --git a/src/components/AdminSidebar.tsx b/src/components/AdminSidebar.tsx index e08717b..504cc13 100644 --- a/src/components/AdminSidebar.tsx +++ b/src/components/AdminSidebar.tsx @@ -37,6 +37,7 @@ import { Megaphone, Bell, Video, + Sparkles, } from "lucide-solid"; type NavItem = { @@ -246,6 +247,12 @@ const GROUPS: NavItem[][] = [ icon: CreditCard, moduleKeys: ["CREDIT_MANAGEMENT", "CREDITS"], }, + { + href: "/admin/ai-management", + label: "AI Management", + icon: Sparkles, + moduleKeys: ["AI_MANAGEMENT", "AI"], + }, { href: "/admin/coupon", label: "Coupon Management", diff --git a/src/lib/admin/dashboard.ts b/src/lib/admin/dashboard.ts index 1f3462e..7321a27 100644 --- a/src/lib/admin/dashboard.ts +++ b/src/lib/admin/dashboard.ts @@ -95,6 +95,62 @@ const adapterMap: Record Promise ({ state: 'success', payload: { kind: 'kpi', data: kpiData.creditsPurchased } }), chart_leads_trend: async () => ({ state: 'success', payload: { kind: 'line_chart', data: lineChartData } }), chart_revenue_overview: async () => ({ state: 'success', payload: { kind: 'bar_chart', data: barChartData } }), + kpi_ai_users: async () => { + try { + const token = typeof sessionStorage !== 'undefined' + ? sessionStorage.getItem('nxtgauge_admin_access_token') || '' + : ''; + const res = await fetch('/api/admin/ai/stats', { + headers: { Authorization: `Bearer ${token}` }, + credentials: 'include', + }); + if (!res.ok) throw new Error('Failed'); + const data = await res.json(); + return { + state: 'success', + payload: { + kind: 'kpi', + data: { + value: String(data.total_users_with_ai ?? 0), + delta: '', + note: `${data.total_generations_today ?? 0} today`, + tone: 'up' as const, + icon: 'users' as const, + }, + }, + }; + } catch { + return { state: 'error', message: 'Failed to load AI stats' }; + } + }, + kpi_ai_generations_today: async () => { + try { + const token = typeof sessionStorage !== 'undefined' + ? sessionStorage.getItem('nxtgauge_admin_access_token') || '' + : ''; + const res = await fetch('/api/admin/ai/stats', { + headers: { Authorization: `Bearer ${token}` }, + credentials: 'include', + }); + if (!res.ok) throw new Error('Failed'); + const data = await res.json(); + return { + state: 'success', + payload: { + kind: 'kpi', + data: { + value: String(data.total_generations_today ?? 0), + delta: '', + note: `${data.total_generations_month ?? 0} this month`, + tone: 'up' as const, + icon: 'trend' as const, + }, + }, + }; + } catch { + return { state: 'error', message: 'Failed to load AI usage' }; + } + }, }; export const ADMIN_DASHBOARD_WIDGETS: DashboardWidgetDefinition[] = [ @@ -180,6 +236,28 @@ export const ADMIN_DASHBOARD_WIDGETS: DashboardWidgetDefinition[] = [ readiness: 'ready', description: 'Weekly revenue overview', }, + { + widgetKey: 'kpi_ai_users', + title: 'AI Users', + moduleKey: 'AI_MANAGEMENT', + defaultSize: 'S', + defaultVisible: true, + order: 9, + dataAdapterKey: 'kpi_ai_users', + readiness: 'ready', + description: 'Total users with active AI plans', + }, + { + widgetKey: 'kpi_ai_generations_today', + title: 'AI Generations Today', + moduleKey: 'AI_MANAGEMENT', + defaultSize: 'S', + defaultVisible: true, + order: 10, + dataAdapterKey: 'kpi_ai_generations_today', + readiness: 'ready', + description: 'AI generations used today', + }, ]; export function loadWidgetData(definition: DashboardWidgetDefinition): Promise> { diff --git a/src/routes/admin/ai-management.tsx b/src/routes/admin/ai-management.tsx new file mode 100644 index 0000000..d5c765d --- /dev/null +++ b/src/routes/admin/ai-management.tsx @@ -0,0 +1,342 @@ +import { createMemo, createSignal, Show, For, onMount } from "solid-js"; +import { Sparkles, Users, TrendingUp, Calendar, Zap, BarChart3 } from "lucide-solid"; + +const API = ""; + +function getToken(): string { + return typeof sessionStorage !== "undefined" + ? sessionStorage.getItem("nxtgauge_admin_access_token") || "" + : ""; +} + +function authHeaders(): Record { + const token = getToken(); + return { + Accept: "application/json", + "Content-Type": "application/json", + ...(token ? { Authorization: `Bearer ${token}` } : {}), + }; +} + +type AiStats = { + total_users_with_ai: number; + total_generations_today: number; + total_generations_month: number; + by_plan: PlanUsage[]; +}; + +type PlanUsage = { + plan_name: string; + user_count: number; + monthly_limit: number; + total_monthly_usage: number; +}; + +type UserAiUsage = { + user_id: string; + email: string; + role: string; + plan_name: string; + monthly_limit: number; + monthly_used: number; + addon_balance: number; + renewal_date: string | null; +}; + +type AiPlan = { + id: string; + name: string; + monthly_action_limit: number; + daily_action_limit: number | null; + is_active: boolean; +}; + +export default function AiManagementPage() { + const [activeTab, setActiveTab] = createSignal<"overview" | "users" | "plans">("overview"); + const [loading, setLoading] = createSignal(false); + const [stats, setStats] = createSignal(null); + const [users, setUsers] = createSignal([]); + const [plans, setPlans] = createSignal([]); + const [error, setError] = createSignal(""); + + const [userPage, setUserPage] = createSignal(1); + const [userSearch, setUserSearch] = createSignal(""); + + const loadStats = async () => { + setLoading(true); + setError(""); + try { + const res = await fetch(`${API}/api/admin/ai/stats`, { + headers: authHeaders(), + credentials: "include", + }); + if (!res.ok) throw new Error("Failed to load AI stats"); + const data = await res.json(); + setStats(data); + } catch (e: any) { + setError(e.message); + } finally { + setLoading(false); + } + }; + + const loadUsers = async (page: number) => { + setLoading(true); + setError(""); + try { + const res = await fetch(`${API}/api/admin/ai/users?page=${page}&limit=20`, { + headers: authHeaders(), + credentials: "include", + }); + if (!res.ok) throw new Error("Failed to load users"); + const data = await res.json(); + setUsers(data); + setUserPage(page); + } catch (e: any) { + setError(e.message); + } finally { + setLoading(false); + } + }; + + const loadPlans = async () => { + setLoading(true); + setError(""); + try { + const res = await fetch(`${API}/api/admin/ai/plans`, { + headers: authHeaders(), + credentials: "include", + }); + if (!res.ok) throw new Error("Failed to load plans"); + const data = await res.json(); + setPlans(data); + } catch (e: any) { + setError(e.message); + } finally { + setLoading(false); + } + }; + + onMount(() => { + loadStats(); + if (activeTab() === "users") loadUsers(1); + if (activeTab() === "plans") loadPlans(); + }); + + const handleTabChange = (tab: "overview" | "users" | "plans") => { + setActiveTab(tab); + if (tab === "users") loadUsers(1); + if (tab === "plans") loadPlans(); + }; + + const filteredUsers = createMemo(() => { + const search = userSearch().toLowerCase(); + if (!search) return users(); + return users().filter( + (u) => + u.email.toLowerCase().includes(search) || + u.user_id.toLowerCase().includes(search) || + u.role.toLowerCase().includes(search) + ); + }); + + return ( +
+
+ +

AI Management

+
+ +
+ + + +
+ + +
+ {error()} +
+
+ + + +
Loading...
+
+ +
+
+
+ + AI Users +
+

{stats()?.total_users_with_ai ?? 0}

+
+
+
+ + Today +
+

{stats()?.total_generations_today ?? 0}

+
+
+
+ + This Month +
+

{stats()?.total_generations_month ?? 0}

+
+
+
+ + Active Plans +
+

{stats()?.by_plan?.length ?? 0}

+
+
+ +
+

Usage by Plan

+ + + + + + + + + + + {(plan) => ( + + + + + + )} + + +
PlanUsersMonthly Limit
{plan.plan_name}{plan.user_count}{plan.monthly_limit}
+
+
+
+ + +
+ setUserSearch(e.currentTarget.value)} + style="width:100%;padding:10px 12px;border:1px solid #E5E7EB;border-radius:8px;font-size:14px" + /> +
+ +
+ + + + + + + + + + + + + + + {(user) => ( + + + + + + + + + + )} + + +
UserRolePlanMonthly UsedLimitAdd-onRenews
+
{user.email}
+
{user.user_id.slice(0, 8)}...
+
+ + {user.role} + + {user.plan_name}{user.monthly_used}{user.monthly_limit} + 0 ? "#FEF3C7" : "#F3F4F6"};color:${user.addon_balance > 0 ? "#D97706" : "#6B7280"};border-radius:4px;font-size:12px;font-weight:500`}> + {user.addon_balance} + + + {user.renewal_date || "N/A"} +
+
+ +
+ + Page {userPage()} + +
+
+ + +
+ + {(plan) => ( +
+
+

{plan.name}

+ + {plan.is_active ? "Active" : "Inactive"} + +
+
+ + Monthly Limit +
+

{plan.monthly_action_limit}

+ +
+ + Daily Limit: {plan.daily_action_limit} +
+
+
+ )} +
+
+
+
+ ); +}