Files
TurboTrades/frontend/src/views/AdminPage.vue
iDefineHD 02d9727a72
All checks were successful
Build Frontend / Build Frontend (push) Successful in 22s
system now uses seperate pricing.
2026-01-11 03:31:54 +00:00

1143 lines
37 KiB
Vue

<template>
<div class="min-h-screen bg-gray-900 text-white p-6">
<div class="max-w-7xl mx-auto space-y-6">
<!-- Header -->
<div class="flex items-center justify-between">
<div class="flex items-center gap-3">
<Shield class="w-8 h-8 text-purple-400" />
<div>
<h1 class="text-3xl font-bold">Admin Dashboard</h1>
<p class="text-gray-400 text-sm">System management and analytics</p>
</div>
</div>
<button
@click="loadAllData"
class="px-4 py-2 bg-gray-800 hover:bg-gray-700 rounded-lg transition-colors flex items-center gap-2"
>
<RefreshCw class="w-4 h-4" :class="{ 'animate-spin': isLoading }" />
Refresh All
</button>
</div>
<!-- Tabs -->
<div class="flex gap-2 border-b border-gray-800">
<button
v-for="tab in tabs"
:key="tab.id"
@click="activeTab = tab.id"
class="px-4 py-2 font-medium transition-colors relative"
:class="
activeTab === tab.id
? 'text-purple-400 border-b-2 border-purple-400'
: 'text-gray-400 hover:text-gray-300'
"
>
<component :is="tab.icon" class="w-4 h-4 inline mr-2" />
{{ tab.label }}
</button>
</div>
<!-- Users Tab -->
<div v-if="activeTab === 'users'">
<AdminUsersPanel />
</div>
<!-- Config Tab -->
<div v-if="activeTab === 'config'">
<AdminConfigPanel />
</div>
<!-- Debug Tab -->
<div v-if="activeTab === 'debug'">
<AdminDebugPanel />
</div>
<!-- Dashboard Tab -->
<div v-if="activeTab === 'dashboard'" class="space-y-6">
<!-- Quick Stats -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<div class="bg-gray-800 rounded-lg p-4">
<div class="flex items-center justify-between mb-2">
<h3 class="text-sm text-gray-400">Total Users</h3>
<Users class="w-5 h-5 text-blue-400" />
</div>
<p class="text-2xl font-bold">
{{ dashboard.overview?.totalUsers || 0 }}
</p>
</div>
<div class="bg-gray-800 rounded-lg p-4">
<div class="flex items-center justify-between mb-2">
<h3 class="text-sm text-gray-400">Active Items</h3>
<Package class="w-5 h-5 text-green-400" />
</div>
<p class="text-2xl font-bold">
{{ dashboard.overview?.activeItems || 0 }}
</p>
<p class="text-xs text-gray-500 mt-1">
CS2: {{ dashboard.overview?.cs2Items || 0 }} | Rust:
{{ dashboard.overview?.rustItems || 0 }}
</p>
</div>
<div class="bg-gray-800 rounded-lg p-4">
<div class="flex items-center justify-between mb-2">
<h3 class="text-sm text-gray-400">Transactions (Today)</h3>
<Activity class="w-5 h-5 text-yellow-400" />
</div>
<p class="text-2xl font-bold">
{{ dashboard.transactions?.today || 0 }}
</p>
<p class="text-xs text-gray-500 mt-1">
Week: {{ dashboard.transactions?.week || 0 }} | Month:
{{ dashboard.transactions?.month || 0 }}
</p>
</div>
<div class="bg-gray-800 rounded-lg p-4">
<div class="flex items-center justify-between mb-2">
<h3 class="text-sm text-gray-400">Total Fees</h3>
<DollarSign class="w-5 h-5 text-purple-400" />
</div>
<p class="text-2xl font-bold">
{{ formatCurrency(dashboard.revenue?.totalFees || 0) }}
</p>
</div>
</div>
<!-- Recent Activity -->
<div class="bg-gray-800 rounded-lg p-6">
<h2 class="text-xl font-bold mb-4 flex items-center gap-2">
<Activity class="w-5 h-5" />
Recent Transactions
</h2>
<div class="space-y-2">
<div
v-for="txn in dashboard.recentActivity"
:key="txn._id"
class="flex items-center justify-between p-3 bg-gray-900 rounded-lg"
>
<div class="flex items-center gap-3">
<img
v-if="txn.userId?.avatar"
:src="txn.userId.avatar"
class="w-8 h-8 rounded-full"
alt="User"
/>
<div>
<p class="text-sm font-medium">
{{ txn.userId?.username || "Unknown" }}
</p>
<p class="text-xs text-gray-400">
{{ txn.type }} - {{ formatDate(txn.createdAt) }}
</p>
</div>
</div>
<span
class="text-sm font-bold"
:class="
['deposit', 'sale'].includes(txn.type)
? 'text-green-400'
: 'text-red-400'
"
>
{{ ["deposit", "sale"].includes(txn.type) ? "+" : "-"
}}{{ formatCurrency(txn.amount) }}
</span>
</div>
</div>
</div>
</div>
<!-- Financial Tab -->
<div v-if="activeTab === 'financial'" class="space-y-6">
<!-- Period Filter -->
<div class="flex gap-2">
<button
v-for="period in periods"
:key="period.value"
@click="
selectedPeriod = period.value;
loadFinancialData();
"
class="px-4 py-2 rounded-lg transition-colors"
:class="
selectedPeriod === period.value
? 'bg-purple-600 text-white'
: 'bg-gray-800 text-gray-400 hover:bg-gray-700'
"
>
{{ period.label }}
</button>
</div>
<!-- Financial Overview -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<!-- Deposits -->
<div class="bg-gray-800 rounded-lg p-4">
<div class="flex items-center justify-between mb-2">
<h3 class="text-sm text-gray-400">Total Deposits</h3>
<TrendingUp class="w-5 h-5 text-green-400" />
</div>
<p class="text-2xl font-bold text-green-400">
{{ formatCurrency(financial.deposits?.total || 0) }}
</p>
<p class="text-xs text-gray-500 mt-1">
{{ financial.deposits?.count || 0 }} transactions
</p>
</div>
<!-- Withdrawals -->
<div class="bg-gray-800 rounded-lg p-4">
<div class="flex items-center justify-between mb-2">
<h3 class="text-sm text-gray-400">Total Withdrawals</h3>
<TrendingDown class="w-5 h-5 text-red-400" />
</div>
<p class="text-2xl font-bold text-red-400">
{{ formatCurrency(financial.withdrawals?.total || 0) }}
</p>
<p class="text-xs text-gray-500 mt-1">
{{ financial.withdrawals?.count || 0 }} transactions
</p>
</div>
<!-- Balance -->
<div class="bg-gray-800 rounded-lg p-4">
<div class="flex items-center justify-between mb-2">
<h3 class="text-sm text-gray-400">Net Balance</h3>
<Wallet class="w-5 h-5 text-blue-400" />
</div>
<p class="text-2xl font-bold text-blue-400">
{{ formatCurrency(financial.balance || 0) }}
</p>
</div>
<!-- Purchases -->
<div class="bg-gray-800 rounded-lg p-4">
<div class="flex items-center justify-between mb-2">
<h3 class="text-sm text-gray-400">User Purchases</h3>
<ShoppingCart class="w-5 h-5 text-yellow-400" />
</div>
<p class="text-2xl font-bold">
{{ formatCurrency(financial.purchases?.total || 0) }}
</p>
<p class="text-xs text-gray-500 mt-1">
{{ financial.purchases?.count || 0 }} items
</p>
</div>
<!-- Sales -->
<div class="bg-gray-800 rounded-lg p-4">
<div class="flex items-center justify-between mb-2">
<h3 class="text-sm text-gray-400">Total Sales</h3>
<Tag class="w-5 h-5 text-purple-400" />
</div>
<p class="text-2xl font-bold">
{{ formatCurrency(financial.sales?.total || 0) }}
</p>
<p class="text-xs text-gray-500 mt-1">
{{ financial.sales?.count || 0 }} items
</p>
</div>
<!-- Fees Collected -->
<div class="bg-gray-800 rounded-lg p-4">
<div class="flex items-center justify-between mb-2">
<h3 class="text-sm text-gray-400">Fees Collected</h3>
<DollarSign class="w-5 h-5 text-green-400" />
</div>
<p class="text-2xl font-bold text-green-400">
{{ formatCurrency(financial.fees?.total || 0) }}
</p>
<p class="text-xs text-gray-500 mt-1">
{{ financial.fees?.count || 0 }} transactions
</p>
</div>
</div>
<!-- Profit -->
<div
class="bg-gradient-to-r from-purple-900/50 to-pink-900/50 rounded-lg p-6 border border-purple-500/20"
>
<div class="flex items-center justify-between">
<div>
<h3 class="text-lg text-gray-300 mb-1">Gross Profit (Fees)</h3>
<p class="text-4xl font-bold text-purple-400">
{{ formatCurrency(financial.profit?.gross || 0) }}
</p>
</div>
<div class="text-right">
<h3 class="text-lg text-gray-300 mb-1">Net Profit</h3>
<p
class="text-4xl font-bold"
:class="
(financial.profit?.net || 0) >= 0
? 'text-green-400'
: 'text-red-400'
"
>
{{ formatCurrency(financial.profit?.net || 0) }}
</p>
</div>
</div>
</div>
</div>
<!-- Transactions Tab -->
<div v-if="activeTab === 'transactions'" class="space-y-6">
<!-- Filters -->
<div class="bg-gray-800 rounded-lg p-4">
<div class="grid grid-cols-1 md:grid-cols-4 gap-4">
<div>
<label class="block text-sm text-gray-400 mb-2">Type</label>
<select
v-model="transactionFilters.type"
@change="loadTransactions"
class="w-full bg-gray-900 text-white rounded-lg px-3 py-2 border border-gray-700"
>
<option value="">All Types</option>
<option value="deposit">Deposit</option>
<option value="withdrawal">Withdrawal</option>
<option value="purchase">Purchase</option>
<option value="sale">Sale</option>
<option value="trade">Trade</option>
<option value="bonus">Bonus</option>
<option value="refund">Refund</option>
</select>
</div>
<div>
<label class="block text-sm text-gray-400 mb-2">Status</label>
<select
v-model="transactionFilters.status"
@change="loadTransactions"
class="w-full bg-gray-900 text-white rounded-lg px-3 py-2 border border-gray-700"
>
<option value="">All Statuses</option>
<option value="completed">Completed</option>
<option value="pending">Pending</option>
<option value="failed">Failed</option>
<option value="cancelled">Cancelled</option>
</select>
</div>
<div>
<label class="block text-sm text-gray-400 mb-2">User ID</label>
<input
v-model="transactionFilters.userId"
@input="debouncedLoadTransactions"
type="text"
placeholder="Enter user ID..."
class="w-full bg-gray-900 text-white rounded-lg px-3 py-2 border border-gray-700"
/>
</div>
<div class="flex items-end">
<button
@click="resetTransactionFilters"
class="w-full px-4 py-2 bg-gray-700 hover:bg-gray-600 rounded-lg transition-colors"
>
Reset Filters
</button>
</div>
</div>
</div>
<!-- Transaction List -->
<div class="bg-gray-800 rounded-lg p-6">
<div class="flex items-center justify-between mb-4">
<h2 class="text-xl font-bold">Transactions</h2>
<span class="text-sm text-gray-400"
>Total: {{ transactionPagination.total }}</span
>
</div>
<div v-if="isLoadingTransactions" class="text-center py-8">
<Loader2 class="w-8 h-8 animate-spin mx-auto text-purple-400" />
<p class="text-gray-400 mt-2">Loading transactions...</p>
</div>
<div v-else-if="transactions.length === 0" class="text-center py-8">
<p class="text-gray-400">No transactions found</p>
</div>
<div v-else class="overflow-x-auto">
<table class="w-full">
<thead>
<tr
class="text-left text-sm text-gray-400 border-b border-gray-700"
>
<th class="pb-3">Date</th>
<th class="pb-3">User</th>
<th class="pb-3">Type</th>
<th class="pb-3">Status</th>
<th class="pb-3">Amount</th>
<th class="pb-3">Fee</th>
<th class="pb-3">Balance</th>
</tr>
</thead>
<tbody>
<tr
v-for="txn in transactions"
:key="txn._id"
class="border-b border-gray-700/50 hover:bg-gray-700/30 transition-colors"
>
<td class="py-3 text-sm">{{ formatDate(txn.createdAt) }}</td>
<td class="py-3">
<div class="flex items-center gap-2">
<img
v-if="txn.userId?.avatar"
:src="txn.userId.avatar"
class="w-6 h-6 rounded-full"
alt="User"
/>
<span class="text-sm">{{
txn.userId?.username || "Unknown"
}}</span>
</div>
</td>
<td class="py-3">
<span
class="px-2 py-1 text-xs rounded-full"
:class="getTypeClass(txn.type)"
>
{{ txn.type }}
</span>
</td>
<td class="py-3">
<span
class="px-2 py-1 text-xs rounded-full"
:class="getStatusClass(txn.status)"
>
{{ txn.status }}
</span>
</td>
<td class="py-3">
<span
class="font-bold"
:class="
['deposit', 'sale', 'bonus', 'refund'].includes(
txn.type
)
? 'text-green-400'
: 'text-red-400'
"
>
{{
["deposit", "sale", "bonus", "refund"].includes(
txn.type
)
? "+"
: "-"
}}{{ formatCurrency(txn.amount) }}
</span>
</td>
<td class="py-3 text-sm">
{{ formatCurrency(txn.fee || 0) }}
</td>
<td class="py-3 text-sm">
{{ formatCurrency(txn.balanceAfter || 0) }}
</td>
</tr>
</tbody>
</table>
</div>
<!-- Pagination -->
<div
v-if="transactionPagination.total > transactionFilters.limit"
class="flex items-center justify-between mt-4"
>
<button
@click="prevTransactionPage"
:disabled="transactionFilters.skip === 0"
class="px-4 py-2 bg-gray-700 hover:bg-gray-600 rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
Previous
</button>
<span class="text-sm text-gray-400">
Page
{{
Math.floor(transactionFilters.skip / transactionFilters.limit) +
1
}}
of
{{
Math.ceil(
transactionPagination.total / transactionFilters.limit
)
}}
</span>
<button
@click="nextTransactionPage"
:disabled="!transactionPagination.hasMore"
class="px-4 py-2 bg-gray-700 hover:bg-gray-600 rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
Next
</button>
</div>
</div>
</div>
<!-- Items Tab -->
<div v-if="activeTab === 'items'">
<AdminItemsPanel />
</div>
<!-- Old Items Tab (keeping for reference, remove if not needed) -->
<div v-if="activeTab === 'items-old'" class="space-y-6">
<!-- Game Filter -->
<div class="flex gap-2">
<button
v-for="game in games"
:key="game.value"
@click="
itemFilters.game = game.value;
loadItems();
"
class="px-4 py-2 rounded-lg transition-colors flex items-center gap-2"
:class="
itemFilters.game === game.value
? 'bg-purple-600 text-white'
: 'bg-gray-800 text-gray-400 hover:bg-gray-700'
"
>
<component :is="game.icon" class="w-4 h-4" />
{{ game.label }}
</button>
</div>
<!-- Filters -->
<div class="bg-gray-800 rounded-lg p-4">
<div class="grid grid-cols-1 md:grid-cols-5 gap-4">
<div>
<label class="block text-sm text-gray-400 mb-2">Status</label>
<select
v-model="itemFilters.status"
@change="loadItems"
class="w-full bg-gray-900 text-white rounded-lg px-3 py-2 border border-gray-700"
>
<option value="">All</option>
<option value="active">Active</option>
<option value="sold">Sold</option>
<option value="removed">Removed</option>
</select>
</div>
<div>
<label class="block text-sm text-gray-400 mb-2">Category</label>
<select
v-model="itemFilters.category"
@change="loadItems"
class="w-full bg-gray-900 text-white rounded-lg px-3 py-2 border border-gray-700"
>
<option value="">All</option>
<option value="rifles">Rifles</option>
<option value="pistols">Pistols</option>
<option value="knives">Knives</option>
<option value="gloves">Gloves</option>
<option value="smgs">SMGs</option>
<option value="stickers">Stickers</option>
<option value="cases">Cases</option>
<option value="other">Other</option>
</select>
</div>
<div>
<label class="block text-sm text-gray-400 mb-2">Search</label>
<input
v-model="itemFilters.search"
@input="debouncedLoadItems"
type="text"
placeholder="Search items..."
class="w-full bg-gray-900 text-white rounded-lg px-3 py-2 border border-gray-700"
/>
</div>
<div>
<label class="block text-sm text-gray-400 mb-2">Sort By</label>
<select
v-model="itemFilters.sortBy"
@change="loadItems"
class="w-full bg-gray-900 text-white rounded-lg px-3 py-2 border border-gray-700"
>
<option value="listedAt">Listed Date</option>
<option value="price">Price</option>
<option value="marketPrice">Market Price</option>
<option value="views">Views</option>
</select>
</div>
<div class="flex items-end">
<button
@click="resetItemFilters"
class="w-full px-4 py-2 bg-gray-700 hover:bg-gray-600 rounded-lg transition-colors"
>
Reset
</button>
</div>
</div>
</div>
<!-- Item List -->
<div class="bg-gray-800 rounded-lg p-6">
<div class="flex items-center justify-between mb-4">
<h2 class="text-xl font-bold">Items</h2>
<span class="text-sm text-gray-400"
>Total: {{ itemPagination.total }}</span
>
</div>
<div v-if="isLoadingItems" class="text-center py-8">
<Loader2 class="w-8 h-8 animate-spin mx-auto text-purple-400" />
<p class="text-gray-400 mt-2">Loading items...</p>
</div>
<div v-else-if="items.length === 0" class="text-center py-8">
<p class="text-gray-400">No items found</p>
</div>
<div v-else class="grid grid-cols-1 gap-4">
<div
v-for="item in items"
:key="item._id"
class="flex items-center gap-4 p-4 bg-gray-900 rounded-lg hover:bg-gray-700/50 transition-colors"
>
<img
:src="item.image"
:alt="item.name"
class="w-20 h-20 object-contain"
/>
<div class="flex-1">
<h3 class="font-bold">{{ item.name }}</h3>
<div class="flex items-center gap-2 mt-1">
<span class="text-xs px-2 py-1 rounded-full bg-gray-800">{{
item.game
}}</span>
<span
class="text-xs px-2 py-1 rounded-full"
:class="getRarityClass(item.rarity)"
>
{{ item.rarity }}
</span>
<span
v-if="item.wear"
class="text-xs px-2 py-1 rounded-full bg-gray-800"
>
{{ item.wear }}
</span>
<span
v-if="item.phase"
class="text-xs px-2 py-1 rounded-full bg-purple-900"
>
{{ item.phase }}
</span>
</div>
<p class="text-xs text-gray-400 mt-1">
Seller: {{ item.seller?.username || "Unknown" }} | Views:
{{ item.views }}
</p>
</div>
<div class="text-right">
<div class="mb-2">
<p class="text-xs text-gray-400">Listing Price</p>
<p class="text-lg font-bold text-purple-400">
{{ formatCurrency(item.price) }}
</p>
</div>
<div v-if="item.marketPrice" class="mb-2">
<p class="text-xs text-gray-400">Market Price</p>
<p class="text-sm text-gray-300">
{{ formatCurrency(item.marketPrice) }}
</p>
</div>
<button
@click="openPriceEditor(item)"
class="px-3 py-1 text-xs bg-purple-600 hover:bg-purple-700 rounded transition-colors"
>
<Edit2 class="w-3 h-3 inline mr-1" />
Edit Prices
</button>
</div>
</div>
</div>
<!-- Pagination -->
<div
v-if="itemPagination.total > itemFilters.limit"
class="flex items-center justify-between mt-4"
>
<button
@click="prevItemPage"
:disabled="itemFilters.skip === 0"
class="px-4 py-2 bg-gray-700 hover:bg-gray-600 rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
Previous
</button>
<span class="text-sm text-gray-400">
Page {{ Math.floor(itemFilters.skip / itemFilters.limit) + 1 }} of
{{ Math.ceil(itemPagination.total / itemFilters.limit) }}
</span>
<button
@click="nextItemPage"
:disabled="!itemPagination.hasMore"
class="px-4 py-2 bg-gray-700 hover:bg-gray-600 rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
Next
</button>
</div>
</div>
</div>
<!-- Price Editor Modal -->
<div
v-if="priceEditorOpen"
class="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4"
@click.self="closePriceEditor"
>
<div class="bg-gray-800 rounded-lg p-6 max-w-md w-full">
<h2 class="text-xl font-bold mb-4">Edit Item Prices</h2>
<div v-if="editingItem" class="space-y-4">
<div>
<p class="text-sm text-gray-400 mb-2">{{ editingItem.name }}</p>
<img
:src="editingItem.image"
:alt="editingItem.name"
class="w-32 h-32 object-contain mx-auto"
/>
</div>
<div>
<label class="block text-sm text-gray-400 mb-2"
>Listing Price ($)</label
>
<input
v-model.number="priceEdits.price"
type="number"
step="0.01"
min="0"
class="w-full bg-gray-900 text-white rounded-lg px-3 py-2 border border-gray-700"
/>
</div>
<div>
<label class="block text-sm text-gray-400 mb-2"
>Market Price ($)</label
>
<input
v-model.number="priceEdits.marketPrice"
type="number"
step="0.01"
min="0"
class="w-full bg-gray-900 text-white rounded-lg px-3 py-2 border border-gray-700"
/>
</div>
<div class="flex gap-2">
<button
@click="savePriceEdits"
:disabled="isSavingPrice"
class="flex-1 px-4 py-2 bg-purple-600 hover:bg-purple-700 rounded-lg transition-colors disabled:opacity-50"
>
<Loader2
v-if="isSavingPrice"
class="w-4 h-4 animate-spin inline mr-2"
/>
Save Changes
</button>
<button
@click="closePriceEditor"
class="px-4 py-2 bg-gray-700 hover:bg-gray-600 rounded-lg transition-colors"
>
Cancel
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed, onMounted } from "vue";
import { useRouter } from "vue-router";
import { useAuthStore } from "../stores/auth";
import { useToast } from "vue-toastification";
import axios from "../utils/axios";
import AdminUsersPanel from "../components/AdminUsersPanel.vue";
import AdminConfigPanel from "../components/AdminConfigPanel.vue";
import AdminDebugPanel from "../components/AdminDebugPanel.vue";
import AdminItemsPanel from "../components/AdminItemsPanel.vue";
import {
Shield,
RefreshCw,
Users,
Package,
Activity,
DollarSign,
TrendingUp,
TrendingDown,
Wallet,
ShoppingCart,
Tag,
Loader2,
Edit2,
Clock,
BarChart3,
Box,
Crosshair,
} from "lucide-vue-next";
const router = useRouter();
const authStore = useAuthStore();
const toast = useToast();
// State
const activeTab = ref("dashboard");
const isLoading = ref(false);
// Dashboard data
const dashboard = ref({});
// Financial data
const financial = ref({});
const selectedPeriod = ref("all");
const periods = [
{ label: "Today", value: "today" },
{ label: "This Week", value: "week" },
{ label: "This Month", value: "month" },
{ label: "This Year", value: "year" },
{ label: "All Time", value: "all" },
];
// Transactions
const transactions = ref([]);
const isLoadingTransactions = ref(false);
const transactionFilters = ref({
type: "",
status: "",
userId: "",
limit: 50,
skip: 0,
});
const transactionPagination = ref({
total: 0,
hasMore: false,
});
// Items
const items = ref([]);
const isLoadingItems = ref(false);
const itemFilters = ref({
game: "",
status: "",
category: "",
search: "",
sortBy: "listedAt",
sortOrder: "desc",
limit: 20,
skip: 0,
});
const itemPagination = ref({
total: 0,
hasMore: false,
});
// Price editor
const priceEditorOpen = ref(false);
const editingItem = ref(null);
const priceEdits = ref({ price: 0, marketPrice: 0 });
const isSavingPrice = ref(false);
// Tabs
const tabs = [
{ id: "dashboard", label: "Dashboard", icon: BarChart3 },
{ id: "users", label: "Users", icon: Users },
{ id: "config", label: "Config", icon: Shield },
{ id: "financial", label: "Financial", icon: DollarSign },
{ id: "transactions", label: "Transactions", icon: Activity },
{ id: "items", label: "Items", icon: Box },
{ id: "debug", label: "Debug", icon: Shield },
];
// Games
const games = [
{ label: "All Games", value: "", icon: Package },
{ label: "CS2", value: "cs2", icon: Crosshair },
{ label: "Rust", value: "rust", icon: Shield },
];
// Methods
const loadAllData = async () => {
isLoading.value = true;
try {
await Promise.all([
loadDashboard(),
loadFinancialData(),
loadTransactions(),
loadItems(),
]);
toast.success("Data refreshed successfully");
} catch (error) {
console.error("Failed to load data:", error);
toast.error("Failed to refresh data");
} finally {
isLoading.value = false;
}
};
const loadDashboard = async () => {
try {
const response = await axios.get("/api/admin/dashboard");
dashboard.value = response.data.dashboard;
} catch (error) {
console.error("Failed to load dashboard:", error);
}
};
const loadFinancialData = async () => {
try {
const response = await axios.get("/api/admin/financial/overview", {
params: { period: selectedPeriod.value },
});
financial.value = response.data.financial;
} catch (error) {
console.error("Failed to load financial data:", error);
}
};
const loadTransactions = async () => {
isLoadingTransactions.value = true;
try {
// Filter out empty values from params
const params = Object.entries(transactionFilters.value).reduce(
(acc, [key, value]) => {
if (value !== "" && value !== null && value !== undefined) {
acc[key] = value;
}
return acc;
},
{}
);
const response = await axios.get("/api/admin/transactions", {
params,
});
transactions.value = response.data.transactions;
transactionPagination.value = response.data.pagination;
} catch (error) {
console.error("Failed to load transactions:", error);
toast.error("Failed to load transactions");
} finally {
isLoadingTransactions.value = false;
}
};
const loadItems = async () => {
isLoadingItems.value = true;
try {
// Filter out empty values from params
const params = Object.entries(itemFilters.value).reduce(
(acc, [key, value]) => {
if (value !== "" && value !== null && value !== undefined) {
acc[key] = value;
}
return acc;
},
{}
);
const response = await axios.get("/api/admin/items/all", {
params,
});
items.value = response.data.items;
itemPagination.value = response.data.pagination;
} catch (error) {
console.error("Failed to load items:", error);
toast.error("Failed to load items");
} finally {
isLoadingItems.value = false;
}
};
// Debounced search
let searchTimeout = null;
const debouncedLoadTransactions = () => {
clearTimeout(searchTimeout);
searchTimeout = setTimeout(loadTransactions, 500);
};
const debouncedLoadItems = () => {
clearTimeout(searchTimeout);
searchTimeout = setTimeout(loadItems, 500);
};
// Pagination
const nextTransactionPage = () => {
transactionFilters.value.skip += transactionFilters.value.limit;
loadTransactions();
};
const prevTransactionPage = () => {
transactionFilters.value.skip = Math.max(
0,
transactionFilters.value.skip - transactionFilters.value.limit
);
loadTransactions();
};
const nextItemPage = () => {
itemFilters.value.skip += itemFilters.value.limit;
loadItems();
};
const prevItemPage = () => {
itemFilters.value.skip = Math.max(
0,
itemFilters.value.skip - itemFilters.value.limit
);
loadItems();
};
// Reset filters
const resetTransactionFilters = () => {
transactionFilters.value = {
type: "",
status: "",
userId: "",
limit: 50,
skip: 0,
};
loadTransactions();
};
const resetItemFilters = () => {
itemFilters.value = {
game: "",
status: "",
category: "",
search: "",
sortBy: "listedAt",
sortOrder: "desc",
limit: 20,
skip: 0,
};
loadItems();
};
// Price editor
const openPriceEditor = (item) => {
editingItem.value = item;
priceEdits.value = {
price: item.price,
marketPrice: item.marketPrice || 0,
};
priceEditorOpen.value = true;
};
const closePriceEditor = () => {
priceEditorOpen.value = false;
editingItem.value = null;
priceEdits.value = { price: 0, marketPrice: 0 };
};
const savePriceEdits = async () => {
if (!editingItem.value) return;
isSavingPrice.value = true;
try {
await axios.put(
`/api/admin/items/${editingItem.value._id}/price`,
priceEdits.value
);
toast.success("Prices updated successfully");
closePriceEditor();
loadItems();
} catch (error) {
console.error("Failed to update prices:", error);
toast.error("Failed to update prices");
} finally {
isSavingPrice.value = false;
}
};
// Utility functions
const formatCurrency = (amount) => {
return new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
}).format(amount);
};
const formatDate = (date) => {
return new Intl.DateTimeFormat("en-US", {
year: "numeric",
month: "short",
day: "numeric",
hour: "2-digit",
minute: "2-digit",
}).format(new Date(date));
};
const getTypeClass = (type) => {
const classes = {
deposit: "bg-green-900 text-green-400",
withdrawal: "bg-red-900 text-red-400",
purchase: "bg-blue-900 text-blue-400",
sale: "bg-purple-900 text-purple-400",
trade: "bg-yellow-900 text-yellow-400",
bonus: "bg-pink-900 text-pink-400",
refund: "bg-orange-900 text-orange-400",
};
return classes[type] || "bg-gray-900 text-gray-400";
};
const getStatusClass = (status) => {
const classes = {
completed: "bg-green-900 text-green-400",
pending: "bg-yellow-900 text-yellow-400",
failed: "bg-red-900 text-red-400",
cancelled: "bg-gray-900 text-gray-400",
processing: "bg-blue-900 text-blue-400",
};
return classes[status] || "bg-gray-900 text-gray-400";
};
const getRarityClass = (rarity) => {
const classes = {
common: "bg-gray-700 text-gray-300",
uncommon: "bg-green-900 text-green-400",
rare: "bg-blue-900 text-blue-400",
mythical: "bg-purple-900 text-purple-400",
legendary: "bg-pink-900 text-pink-400",
ancient: "bg-red-900 text-red-400",
exceedingly: "bg-yellow-900 text-yellow-400",
};
return classes[rarity] || "bg-gray-900 text-gray-400";
};
// Check admin access
onMounted(async () => {
if (!authStore.isAuthenticated) {
toast.error("Please login to access admin panel");
router.push("/");
return;
}
// Load initial data
await loadAllData();
});
</script>
<style scoped>
/* Custom scrollbar */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: #1f2937;
}
::-webkit-scrollbar-thumb {
background: #4b5563;
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: #6b7280;
}
</style>