All checks were successful
Build Frontend / Build Frontend (push) Successful in 22s
1143 lines
37 KiB
Vue
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>
|