From f55a2dad7747c76b4640e22be4982cbc0e0ba777 Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Sivakumar Date: Mon, 6 Jul 2026 01:58:29 +0530 Subject: [PATCH] feat: AI Credits Admin Panel - Add AiCreditsAdmin component for managing AI credits - View user balance with detailed credit breakdown - Transaction history (ledger) viewer with pagination - Manual credit adjustment (ADD/DEDUCT) with audit reasons - Reconcile tab for generating reports - Integrate with backend /admin/ai-credits endpoints --- src/components/DashboardShell.tsx | 39 +- src/components/admin/AiCreditsAdmin.tsx | 705 +++++++++++++++++++ src/components/dashboard/CompanyJobsPage.tsx | 18 +- src/components/dashboard/CreditsPage.tsx | 24 +- src/lib/auth.tsx | 39 + src/lib/payu.ts | 96 ++- src/routes/admin/ai-credits.tsx | 13 + src/routes/dashboard.tsx | 6 + 8 files changed, 887 insertions(+), 53 deletions(-) create mode 100644 src/components/admin/AiCreditsAdmin.tsx create mode 100644 src/routes/admin/ai-credits.tsx diff --git a/src/components/DashboardShell.tsx b/src/components/DashboardShell.tsx index 4d37322..cffdfbf 100644 --- a/src/components/DashboardShell.tsx +++ b/src/components/DashboardShell.tsx @@ -3,7 +3,7 @@ * Used for pages that need actual backend connectivity * (My Profile, My Portfolio, Verification) instead of the preview mock. */ -import { For, JSX, createMemo } from "solid-js"; +import { For, JSX, Show, createMemo } from "solid-js"; import { AiChatWidget } from "./AiChatWidget"; import NotificationBell from "./NotificationBell"; import { @@ -22,6 +22,7 @@ import { LogOut, Bell, ChevronRight, + Lock, } from "lucide-solid"; const ICON_MAP: Record = { @@ -85,6 +86,7 @@ interface Props { onSidebarSelect: (item: string) => void; roleKey: string; userName?: string; + isAdmin?: boolean; children: JSX.Element; } @@ -200,6 +202,41 @@ export default function DashboardShell(props: Props) { + {/* Admin section (only for admins) */} + +
+

+ Admin Tools +

+ + + + + AI Credits Admin + +
+
+ {/* User footer */}

{ + const token = sessionStorage.getItem('nxtgauge_access_token'); + const res = await fetch(`${API}/admin/ai-credits/balance?userId=${encodeURIComponent(userId)}`, { + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json', + }, + credentials: 'include', + }); + if (!res.ok) throw new Error(await res.text()); + return res.json(); +} + +async function fetchUserLedger(userId: string, page: number = 1, limit: number = 20): Promise<{ data: LedgerEntry[]; total: number }> { + const token = sessionStorage.getItem('nxtgauge_access_token'); + const res = await fetch( + `${API}/admin/ai-credits/ledger?userId=${encodeURIComponent(userId)}&page=${page}&limit=${limit}`, + { + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json', + }, + credentials: 'include', + } + ); + if (!res.ok) throw new Error(await res.text()); + return res.json(); +} + +async function adjustCredits(request: AdjustRequest): Promise<{ wallet: Wallet; success: boolean }> { + const token = sessionStorage.getItem('nxtgauge_access_token'); + const res = await fetch(`${API}/admin/ai-credits/adjust`, { + method: 'POST', + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json', + }, + credentials: 'include', + body: JSON.stringify(request), + }); + if (!res.ok) throw new Error(await res.text()); + return res.json(); +} + +export default function AiCreditsAdmin() { + const auth = useAuth(); + const [activeTab, setActiveTab] = createSignal<'balance' | 'ledger' | 'adjust' | 'reconcile'>('balance'); + + // Balance tab state + const [userIdInput, setUserIdInput] = createSignal(''); + const [balanceData, setBalanceData] = createSignal(null); + const [balanceError, setBalanceError] = createSignal(''); + const [balanceLoading, setBalanceLoading] = createSignal(false); + + // Ledger tab state + const [ledgerUserId, setLedgerUserId] = createSignal(''); + const [ledgerPage, setLedgerPage] = createSignal(1); + const [ledgerData, setLedgerData] = createSignal<{ data: LedgerEntry[]; total: number } | null>(null); + const [ledgerError, setLedgerError] = createSignal(''); + const [ledgerLoading, setLedgerLoading] = createSignal(false); + + // Adjust tab state + const [adjustForm, setAdjustForm] = createSignal({ + user_id: '', + amount: 100, + type: 'ADD', + reason: '', + }); + const [adjustResult, setAdjustResult] = createSignal<{ wallet: Wallet; success: boolean } | null>(null); + const [adjustError, setAdjustError] = createSignal(''); + const [adjustLoading, setAdjustLoading] = createSignal(false); + + // Reconcile tab state + const [reconcileFrom, setReconcileFrom] = createSignal(''); + const [reconcileTo, setReconcileTo] = createSignal(''); + + const handleFetchBalance = async () => { + if (!userIdInput()) { + setBalanceError('Please enter a User ID'); + return; + } + setBalanceLoading(true); + setBalanceError(''); + try { + const data = await fetchUserBalance(userIdInput()); + setBalanceData(data); + } catch (e: any) { + setBalanceError(e.message || 'Failed to fetch balance'); + } finally { + setBalanceLoading(false); + } + }; + + const handleFetchLedger = async () => { + if (!ledgerUserId()) { + setLedgerError('Please enter a User ID'); + return; + } + setLedgerLoading(true); + setLedgerError(''); + try { + const data = await fetchUserLedger(ledgerUserId(), ledgerPage()); + setLedgerData(data); + } catch (e: any) { + setLedgerError(e.message || 'Failed to fetch ledger'); + } finally { + setLedgerLoading(false); + } + }; + + const handleAdjust = async () => { + if (!adjustForm().user_id || !adjustForm().amount || !adjustForm().reason) { + setAdjustError('Please fill in all fields'); + return; + } + if (adjustForm().amount <= 0) { + setAdjustError('Amount must be positive'); + return; + } + if (adjustForm().reason.trim().length < 5) { + setAdjustError('Please provide a detailed reason (at least 5 characters)'); + return; + } + setAdjustLoading(true); + setAdjustError(''); + try { + const result = await adjustCredits(adjustForm()); + setAdjustResult(result); + } catch (e: any) { + setAdjustError(e.message || 'Failed to adjust credits'); + } finally { + setAdjustLoading(false); + } + }; + + const formatCurrency = (credits: number) => { + return credits.toLocaleString(); + }; + + const formatDate = (dateStr: string) => { + return new Date(dateStr).toLocaleString(); + }; + + return ( +

+

+ AI Credits Admin +

+

+ Manage AI credits for users across the platform +

+ + {/* Tab Navigation */} +
+ {[ + { key: 'balance', label: 'View Balance' }, + { key: 'ledger', label: 'Transaction History' }, + { key: 'adjust', label: 'Adjust Credits' }, + { key: 'reconcile', label: 'Reconcile' }, + ].map(tab => ( + + ))} +
+ + {/* Balance Tab */} + +
+
+ setUserIdInput(e.currentTarget.value)} + placeholder="Enter User ID (UUID)" + style={{ + flex: '1', + padding: '10px 14px', + 'font-size': '14px', + border: '1px solid #D1D5DB', + 'border-radius': '6px', + }} + /> + +
+ + +
+ {balanceError()} +
+
+ + + {(wallet) => ( +
+
+
+ Available Credits +
+
+ {formatCurrency(wallet().available_credits)} +
+
+ +
+
+ Monthly Credits +
+
+ {formatCurrency(wallet().monthly_credits_total - wallet().monthly_credits_used)} / {formatCurrency(wallet().monthly_credits_total)} +
+
+ Used: {formatCurrency(wallet().monthly_credits_used)} +
+
+ +
+
+ Purchased Credits +
+
+ {formatCurrency(wallet().purchased_credits_total - wallet().purchased_credits_used)} / {formatCurrency(wallet().purchased_credits_total)} +
+
+ Used: {formatCurrency(wallet().purchased_credits_used)} +
+
+ +
+
+ Bonus Credits +
+
+ {formatCurrency(wallet().bonus_credits_total - wallet().bonus_credits_used)} / {formatCurrency(wallet().bonus_credits_total)} +
+
+ Used: {formatCurrency(wallet().bonus_credits_used)} +
+
+ +
+
+ Reserved +
+
+ {formatCurrency(wallet().reserved_credits)} +
+
+ +
+
+ Locked +
+
+ {formatCurrency(wallet().locked_credits)} +
+
+
+ )} +
+
+
+ + {/* Ledger Tab */} + +
+
+ setLedgerUserId(e.currentTarget.value)} + placeholder="Enter User ID (UUID)" + style={{ + flex: '1', + padding: '10px 14px', + 'font-size': '14px', + border: '1px solid #D1D5DB', + 'border-radius': '6px', + }} + /> + +
+ + +
+ {ledgerError()} +
+
+ + + {(data) => ( +
+
+ Showing {data().data.length} of {data().total} entries +
+ +
+ + + + + + + + + + + + + {(entry) => ( + + + + + + + + )} + + +
DateTypeCreditsBalance AfterDescription
+ {formatDate(entry.created_at)} + + + {entry.entry_type} + + + {entry.credits > 0 ? '+' : ''}{formatCurrency(entry.credits)} + + {formatCurrency(entry.balance_after)} + + {entry.description || '-'} +
+
+ +
+ + + Page {ledgerPage()} + + +
+
+ )} +
+
+
+ + {/* Adjust Tab */} + +
+
+
+ + setAdjustForm(f => ({ ...f, user_id: e.currentTarget.value }))} + placeholder="Enter User ID (UUID)" + style={{ + width: '100%', + padding: '10px 14px', + 'font-size': '14px', + border: '1px solid #D1D5DB', + 'border-radius': '6px', + }} + /> +
+ +
+
+ + +
+ +
+ + setAdjustForm(f => ({ ...f, amount: parseInt(e.currentTarget.value) || 0 }))} + style={{ + width: '100%', + padding: '10px 14px', + 'font-size': '14px', + border: '1px solid #D1D5DB', + 'border-radius': '6px', + }} + /> +
+
+ +
+ +