system now uses seperate pricing.
All checks were successful
Build Frontend / Build Frontend (push) Successful in 22s
All checks were successful
Build Frontend / Build Frontend (push) Successful in 22s
This commit is contained in:
523
frontend/src/components/AdminItemsPanel.vue
Normal file
523
frontend/src/components/AdminItemsPanel.vue
Normal file
@@ -0,0 +1,523 @@
|
||||
<template>
|
||||
<div class="space-y-6">
|
||||
<!-- Header with Stats -->
|
||||
<div class="bg-gray-800 rounded-lg p-6">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<div>
|
||||
<h2 class="text-2xl font-bold flex items-center gap-2">
|
||||
<Package class="w-6 h-6 text-blue-400" />
|
||||
Market Price Database
|
||||
</h2>
|
||||
<p class="text-gray-400 text-sm mt-1">
|
||||
Manage reference prices for instant sell
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
@click="refreshPrices"
|
||||
:disabled="isRefreshing"
|
||||
class="px-4 py-2 bg-blue-600 hover:bg-blue-700 disabled:bg-gray-600 disabled:cursor-not-allowed rounded-lg transition-colors flex items-center gap-2"
|
||||
>
|
||||
<RefreshCw class="w-4 h-4" :class="{ 'animate-spin': isRefreshing }" />
|
||||
{{ isRefreshing ? 'Updating...' : 'Update All Prices' }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Stats -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||
<div class="bg-gray-900 rounded-lg p-4">
|
||||
<div class="text-gray-400 text-sm mb-1">Total Items</div>
|
||||
<div class="text-2xl font-bold">{{ stats.total || 0 }}</div>
|
||||
</div>
|
||||
<div class="bg-gray-900 rounded-lg p-4">
|
||||
<div class="text-gray-400 text-sm mb-1">CS2 Items</div>
|
||||
<div class="text-2xl font-bold text-blue-400">{{ stats.cs2 || 0 }}</div>
|
||||
</div>
|
||||
<div class="bg-gray-900 rounded-lg p-4">
|
||||
<div class="text-gray-400 text-sm mb-1">Rust Items</div>
|
||||
<div class="text-2xl font-bold text-orange-400">{{ stats.rust || 0 }}</div>
|
||||
</div>
|
||||
<div class="bg-gray-900 rounded-lg p-4">
|
||||
<div class="text-gray-400 text-sm mb-1">Last Updated</div>
|
||||
<div class="text-lg font-semibold">
|
||||
{{ stats.lastUpdated ? formatDate(stats.lastUpdated) : 'Never' }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Filters -->
|
||||
<div class="bg-gray-800 rounded-lg p-6">
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||
<!-- Search -->
|
||||
<div class="md:col-span-2">
|
||||
<label class="block text-sm font-medium text-gray-400 mb-2">
|
||||
Search Items
|
||||
</label>
|
||||
<div class="relative">
|
||||
<Search class="absolute left-3 top-1/2 transform -translate-y-1/2 w-5 h-5 text-gray-400" />
|
||||
<input
|
||||
v-model="filters.search"
|
||||
@input="debouncedSearch"
|
||||
type="text"
|
||||
placeholder="Search by name..."
|
||||
class="w-full pl-10 pr-4 py-2 bg-gray-900 border border-gray-700 rounded-lg focus:outline-none focus:border-blue-500 text-white"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Game Filter -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-400 mb-2">
|
||||
Game
|
||||
</label>
|
||||
<select
|
||||
v-model="filters.game"
|
||||
@change="loadItems"
|
||||
class="w-full px-4 py-2 bg-gray-900 border border-gray-700 rounded-lg focus:outline-none focus:border-blue-500 text-white"
|
||||
>
|
||||
<option value="">All Games</option>
|
||||
<option value="cs2">CS2</option>
|
||||
<option value="rust">Rust</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Sort -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-400 mb-2">
|
||||
Sort By
|
||||
</label>
|
||||
<select
|
||||
v-model="filters.sort"
|
||||
@change="loadItems"
|
||||
class="w-full px-4 py-2 bg-gray-900 border border-gray-700 rounded-lg focus:outline-none focus:border-blue-500 text-white"
|
||||
>
|
||||
<option value="name-asc">Name (A-Z)</option>
|
||||
<option value="name-desc">Name (Z-A)</option>
|
||||
<option value="price-desc">Price (High to Low)</option>
|
||||
<option value="price-asc">Price (Low to High)</option>
|
||||
<option value="updated-desc">Recently Updated</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Items Table -->
|
||||
<div class="bg-gray-800 rounded-lg overflow-hidden">
|
||||
<div v-if="isLoading" class="p-8 text-center">
|
||||
<RefreshCw class="w-8 h-8 animate-spin mx-auto mb-2 text-blue-400" />
|
||||
<p class="text-gray-400">Loading items...</p>
|
||||
</div>
|
||||
|
||||
<div v-else-if="items.length === 0" class="p-8 text-center">
|
||||
<Package class="w-12 h-12 mx-auto mb-2 text-gray-600" />
|
||||
<p class="text-gray-400">No items found</p>
|
||||
</div>
|
||||
|
||||
<div v-else class="overflow-x-auto">
|
||||
<table class="w-full">
|
||||
<thead class="bg-gray-900 border-b border-gray-700">
|
||||
<tr>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">
|
||||
Item
|
||||
</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">
|
||||
Game
|
||||
</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">
|
||||
Price
|
||||
</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">
|
||||
Type
|
||||
</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">
|
||||
Last Updated
|
||||
</th>
|
||||
<th class="px-6 py-3 text-right text-xs font-medium text-gray-400 uppercase tracking-wider">
|
||||
Actions
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-700">
|
||||
<tr
|
||||
v-for="item in items"
|
||||
:key="item._id"
|
||||
class="hover:bg-gray-700/50 transition-colors"
|
||||
>
|
||||
<td class="px-6 py-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<img
|
||||
v-if="item.image"
|
||||
:src="item.image"
|
||||
:alt="item.name"
|
||||
class="w-12 h-12 rounded"
|
||||
@error="(e) => (e.target.src = '/placeholder.png')"
|
||||
/>
|
||||
<div class="w-12 h-12 bg-gray-700 rounded flex items-center justify-center" v-else>
|
||||
<Package class="w-6 h-6 text-gray-500" />
|
||||
</div>
|
||||
<div class="max-w-md">
|
||||
<div class="font-medium text-white truncate">{{ item.name }}</div>
|
||||
<div class="text-xs text-gray-500 truncate">{{ item.marketHashName }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<span
|
||||
class="px-2 py-1 text-xs font-semibold rounded"
|
||||
:class="
|
||||
item.game === 'cs2'
|
||||
? 'bg-blue-900/50 text-blue-400'
|
||||
: 'bg-orange-900/50 text-orange-400'
|
||||
"
|
||||
>
|
||||
{{ item.game.toUpperCase() }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<div v-if="editingItem?._id === item._id" class="flex items-center gap-2">
|
||||
<input
|
||||
v-model.number="editingItem.price"
|
||||
type="number"
|
||||
step="0.01"
|
||||
min="0"
|
||||
class="w-24 px-2 py-1 bg-gray-900 border border-gray-700 rounded text-white text-sm"
|
||||
/>
|
||||
</div>
|
||||
<div v-else class="font-semibold text-green-400">
|
||||
{{ formatCurrency(item.price) }}
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<span class="text-sm text-gray-400">{{ item.priceType || 'safe' }}</span>
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<div class="text-sm text-gray-400">
|
||||
{{ formatDate(item.lastUpdated) }}
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 text-right">
|
||||
<div v-if="editingItem?._id === item._id" class="flex items-center justify-end gap-2">
|
||||
<button
|
||||
@click="saveItem"
|
||||
class="p-2 bg-green-600 hover:bg-green-700 rounded transition-colors"
|
||||
title="Save"
|
||||
>
|
||||
<Check class="w-4 h-4" />
|
||||
</button>
|
||||
<button
|
||||
@click="cancelEdit"
|
||||
class="p-2 bg-gray-600 hover:bg-gray-700 rounded transition-colors"
|
||||
title="Cancel"
|
||||
>
|
||||
<X class="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
<div v-else class="flex items-center justify-end gap-2">
|
||||
<button
|
||||
@click="editItem(item)"
|
||||
class="p-2 bg-blue-600 hover:bg-blue-700 rounded transition-colors"
|
||||
title="Edit Price"
|
||||
>
|
||||
<Edit class="w-4 h-4" />
|
||||
</button>
|
||||
<button
|
||||
@click="deleteItem(item)"
|
||||
class="p-2 bg-red-600 hover:bg-red-700 rounded transition-colors"
|
||||
title="Delete"
|
||||
>
|
||||
<Trash2 class="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
<div class="px-6 py-4 bg-gray-900 border-t border-gray-700 flex items-center justify-between">
|
||||
<div class="text-sm text-gray-400">
|
||||
Showing {{ (pagination.page - 1) * pagination.limit + 1 }} to
|
||||
{{ Math.min(pagination.page * pagination.limit, pagination.total) }} of
|
||||
{{ pagination.total }} items
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<button
|
||||
@click="goToPage(1)"
|
||||
:disabled="pagination.page === 1"
|
||||
class="px-3 py-1 bg-gray-800 hover:bg-gray-700 disabled:bg-gray-800 disabled:text-gray-600 disabled:cursor-not-allowed rounded transition-colors"
|
||||
>
|
||||
<ChevronsLeft class="w-4 h-4" />
|
||||
</button>
|
||||
<button
|
||||
@click="goToPage(pagination.page - 1)"
|
||||
:disabled="pagination.page === 1"
|
||||
class="px-3 py-1 bg-gray-800 hover:bg-gray-700 disabled:bg-gray-800 disabled:text-gray-600 disabled:cursor-not-allowed rounded transition-colors"
|
||||
>
|
||||
<ChevronLeft class="w-4 h-4" />
|
||||
</button>
|
||||
|
||||
<!-- Page Numbers -->
|
||||
<div class="flex items-center gap-1">
|
||||
<button
|
||||
v-for="page in visiblePages"
|
||||
:key="page"
|
||||
@click="goToPage(page)"
|
||||
class="px-3 py-1 rounded transition-colors"
|
||||
:class="
|
||||
page === pagination.page
|
||||
? 'bg-blue-600 text-white'
|
||||
: 'bg-gray-800 hover:bg-gray-700'
|
||||
"
|
||||
>
|
||||
{{ page }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button
|
||||
@click="goToPage(pagination.page + 1)"
|
||||
:disabled="pagination.page >= pagination.pages"
|
||||
class="px-3 py-1 bg-gray-800 hover:bg-gray-700 disabled:bg-gray-800 disabled:text-gray-600 disabled:cursor-not-allowed rounded transition-colors"
|
||||
>
|
||||
<ChevronRight class="w-4 h-4" />
|
||||
</button>
|
||||
<button
|
||||
@click="goToPage(pagination.pages)"
|
||||
:disabled="pagination.page >= pagination.pages"
|
||||
class="px-3 py-1 bg-gray-800 hover:bg-gray-700 disabled:bg-gray-800 disabled:text-gray-600 disabled:cursor-not-allowed rounded transition-colors"
|
||||
>
|
||||
<ChevronsRight class="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue';
|
||||
import axios from '@/utils/axios';
|
||||
import { useToast } from 'vue-toastification';
|
||||
import {
|
||||
Package,
|
||||
RefreshCw,
|
||||
Search,
|
||||
Edit,
|
||||
Trash2,
|
||||
Check,
|
||||
X,
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
ChevronsLeft,
|
||||
ChevronsRight,
|
||||
} from 'lucide-vue-next';
|
||||
|
||||
const toast = useToast();
|
||||
|
||||
// State
|
||||
const items = ref([]);
|
||||
const stats = ref({
|
||||
total: 0,
|
||||
cs2: 0,
|
||||
rust: 0,
|
||||
lastUpdated: null,
|
||||
});
|
||||
const isLoading = ref(false);
|
||||
const isRefreshing = ref(false);
|
||||
const editingItem = ref(null);
|
||||
|
||||
// Filters
|
||||
const filters = ref({
|
||||
search: '',
|
||||
game: '',
|
||||
sort: 'name-asc',
|
||||
});
|
||||
|
||||
// Pagination
|
||||
const pagination = ref({
|
||||
page: 1,
|
||||
limit: 50,
|
||||
total: 0,
|
||||
pages: 0,
|
||||
});
|
||||
|
||||
// Computed
|
||||
const visiblePages = computed(() => {
|
||||
const current = pagination.value.page;
|
||||
const total = pagination.value.pages;
|
||||
const delta = 2;
|
||||
const pages = [];
|
||||
|
||||
for (let i = Math.max(1, current - delta); i <= Math.min(total, current + delta); i++) {
|
||||
pages.push(i);
|
||||
}
|
||||
|
||||
return pages;
|
||||
});
|
||||
|
||||
// Methods
|
||||
const loadItems = async () => {
|
||||
isLoading.value = true;
|
||||
try {
|
||||
const params = {
|
||||
page: pagination.value.page,
|
||||
limit: pagination.value.limit,
|
||||
game: filters.value.game,
|
||||
search: filters.value.search,
|
||||
sort: filters.value.sort,
|
||||
};
|
||||
|
||||
const response = await axios.get('/api/admin/marketprices', { params });
|
||||
|
||||
if (response.data.success) {
|
||||
items.value = response.data.items || [];
|
||||
pagination.value = {
|
||||
page: response.data.page || 1,
|
||||
limit: response.data.limit || 50,
|
||||
total: response.data.total || 0,
|
||||
pages: response.data.pages || 0,
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load items:', error);
|
||||
toast.error('Failed to load items');
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const loadStats = async () => {
|
||||
try {
|
||||
const response = await axios.get('/api/admin/marketprices/stats');
|
||||
if (response.data.success) {
|
||||
stats.value = response.data.stats;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load stats:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const refreshPrices = async () => {
|
||||
if (isRefreshing.value) return;
|
||||
|
||||
isRefreshing.value = true;
|
||||
try {
|
||||
const response = await axios.post('/api/admin/prices/update', {
|
||||
game: 'all',
|
||||
});
|
||||
|
||||
if (response.data.success) {
|
||||
toast.success('Price update started! This may take a few minutes.');
|
||||
// Reload after a delay
|
||||
setTimeout(() => {
|
||||
loadItems();
|
||||
loadStats();
|
||||
}, 5000);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to refresh prices:', error);
|
||||
toast.error('Failed to start price update');
|
||||
} finally {
|
||||
isRefreshing.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const editItem = (item) => {
|
||||
editingItem.value = { ...item };
|
||||
};
|
||||
|
||||
const cancelEdit = () => {
|
||||
editingItem.value = null;
|
||||
};
|
||||
|
||||
const saveItem = async () => {
|
||||
if (!editingItem.value) return;
|
||||
|
||||
try {
|
||||
const response = await axios.patch(
|
||||
`/api/admin/marketprices/${editingItem.value._id}`,
|
||||
{
|
||||
price: editingItem.value.price,
|
||||
}
|
||||
);
|
||||
|
||||
if (response.data.success) {
|
||||
toast.success('Price updated successfully');
|
||||
// Update item in list
|
||||
const index = items.value.findIndex((i) => i._id === editingItem.value._id);
|
||||
if (index !== -1) {
|
||||
items.value[index] = { ...editingItem.value };
|
||||
}
|
||||
editingItem.value = null;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to update item:', error);
|
||||
toast.error(error.response?.data?.message || 'Failed to update item');
|
||||
}
|
||||
};
|
||||
|
||||
const deleteItem = async (item) => {
|
||||
if (!confirm(`Delete "${item.name}"? This action cannot be undone.`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await axios.delete(`/api/admin/marketprices/${item._id}`);
|
||||
|
||||
if (response.data.success) {
|
||||
toast.success('Item deleted successfully');
|
||||
loadItems();
|
||||
loadStats();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to delete item:', error);
|
||||
toast.error(error.response?.data?.message || 'Failed to delete item');
|
||||
}
|
||||
};
|
||||
|
||||
const goToPage = (page) => {
|
||||
if (page < 1 || page > pagination.value.pages) return;
|
||||
pagination.value.page = page;
|
||||
loadItems();
|
||||
};
|
||||
|
||||
// Debounced search
|
||||
let searchTimeout;
|
||||
const debouncedSearch = () => {
|
||||
clearTimeout(searchTimeout);
|
||||
searchTimeout = setTimeout(() => {
|
||||
pagination.value.page = 1; // Reset to first page
|
||||
loadItems();
|
||||
}, 500);
|
||||
};
|
||||
|
||||
// Formatting helpers
|
||||
const formatCurrency = (value) => {
|
||||
if (value === null || value === undefined) return 'N/A';
|
||||
return new Intl.NumberFormat('en-US', {
|
||||
style: 'currency',
|
||||
currency: 'USD',
|
||||
}).format(value);
|
||||
};
|
||||
|
||||
const formatDate = (date) => {
|
||||
if (!date) return 'N/A';
|
||||
return new Date(date).toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
});
|
||||
};
|
||||
|
||||
// Lifecycle
|
||||
onMounted(() => {
|
||||
loadItems();
|
||||
loadStats();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* Add any custom styles here */
|
||||
</style>
|
||||
@@ -477,7 +477,12 @@
|
||||
</div>
|
||||
|
||||
<!-- Items Tab -->
|
||||
<div v-if="activeTab === 'items'" class="space-y-6">
|
||||
<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
|
||||
@@ -753,6 +758,7 @@ 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,
|
||||
|
||||
241
routes/admin.js
241
routes/admin.js
@@ -1,6 +1,7 @@
|
||||
import { authenticate } from "../middleware/auth.js";
|
||||
import { authenticate, isAdmin } from "../middleware/auth.js";
|
||||
import pricingService from "../services/pricing.js";
|
||||
import Item from "../models/Item.js";
|
||||
import MarketPrice from "../models/MarketPrice.js";
|
||||
import Transaction from "../models/Transaction.js";
|
||||
import User from "../models/User.js";
|
||||
|
||||
@@ -1115,4 +1116,242 @@ export default async function adminRoutes(fastify, options) {
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// ============================================
|
||||
// MARKETPRICE MANAGEMENT
|
||||
// ============================================
|
||||
|
||||
// GET /admin/marketprices - Get paginated list of MarketPrice items
|
||||
fastify.get(
|
||||
"/marketprices",
|
||||
{
|
||||
preHandler: [authenticate, isAdmin],
|
||||
schema: {
|
||||
querystring: {
|
||||
type: "object",
|
||||
properties: {
|
||||
page: { type: "integer", minimum: 1, default: 1 },
|
||||
limit: { type: "integer", minimum: 1, maximum: 100, default: 50 },
|
||||
game: { type: "string", enum: ["cs2", "rust", ""] },
|
||||
search: { type: "string" },
|
||||
sort: { type: "string", default: "name-asc" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
async (request, reply) => {
|
||||
try {
|
||||
const {
|
||||
page = 1,
|
||||
limit = 50,
|
||||
game = "",
|
||||
search = "",
|
||||
sort = "name-asc",
|
||||
} = request.query;
|
||||
|
||||
// Build query
|
||||
const query = {};
|
||||
if (game) {
|
||||
query.game = game;
|
||||
}
|
||||
if (search) {
|
||||
query.$or = [
|
||||
{ name: { $regex: search, $options: "i" } },
|
||||
{ marketHashName: { $regex: search, $options: "i" } },
|
||||
];
|
||||
}
|
||||
|
||||
// Build sort
|
||||
let sortObj = {};
|
||||
switch (sort) {
|
||||
case "name-asc":
|
||||
sortObj = { name: 1 };
|
||||
break;
|
||||
case "name-desc":
|
||||
sortObj = { name: -1 };
|
||||
break;
|
||||
case "price-asc":
|
||||
sortObj = { price: 1 };
|
||||
break;
|
||||
case "price-desc":
|
||||
sortObj = { price: -1 };
|
||||
break;
|
||||
case "updated-desc":
|
||||
sortObj = { lastUpdated: -1 };
|
||||
break;
|
||||
default:
|
||||
sortObj = { name: 1 };
|
||||
}
|
||||
|
||||
// Get total count
|
||||
const total = await MarketPrice.countDocuments(query);
|
||||
|
||||
// Get items
|
||||
const items = await MarketPrice.find(query)
|
||||
.sort(sortObj)
|
||||
.limit(limit)
|
||||
.skip((page - 1) * limit)
|
||||
.lean();
|
||||
|
||||
return reply.send({
|
||||
success: true,
|
||||
items,
|
||||
page,
|
||||
limit,
|
||||
total,
|
||||
pages: Math.ceil(total / limit),
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("❌ Failed to get market prices:", error);
|
||||
return reply.status(500).send({
|
||||
success: false,
|
||||
message: "Failed to get market prices",
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// GET /admin/marketprices/stats - Get MarketPrice statistics
|
||||
fastify.get(
|
||||
"/marketprices/stats",
|
||||
{
|
||||
preHandler: [authenticate, isAdmin],
|
||||
},
|
||||
async (request, reply) => {
|
||||
try {
|
||||
const total = await MarketPrice.countDocuments();
|
||||
const cs2 = await MarketPrice.countDocuments({ game: "cs2" });
|
||||
const rust = await MarketPrice.countDocuments({ game: "rust" });
|
||||
|
||||
// Get most recent update
|
||||
const lastItem = await MarketPrice.findOne()
|
||||
.sort({ lastUpdated: -1 })
|
||||
.select("lastUpdated")
|
||||
.lean();
|
||||
|
||||
return reply.send({
|
||||
success: true,
|
||||
stats: {
|
||||
total,
|
||||
cs2,
|
||||
rust,
|
||||
lastUpdated: lastItem?.lastUpdated || null,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("❌ Failed to get market price stats:", error);
|
||||
return reply.status(500).send({
|
||||
success: false,
|
||||
message: "Failed to get stats",
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// PATCH /admin/marketprices/:id - Update a MarketPrice item
|
||||
fastify.patch(
|
||||
"/marketprices/:id",
|
||||
{
|
||||
preHandler: [authenticate, isAdmin],
|
||||
schema: {
|
||||
params: {
|
||||
type: "object",
|
||||
required: ["id"],
|
||||
properties: {
|
||||
id: { type: "string" },
|
||||
},
|
||||
},
|
||||
body: {
|
||||
type: "object",
|
||||
properties: {
|
||||
price: { type: "number", minimum: 0 },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
async (request, reply) => {
|
||||
try {
|
||||
const { id } = request.params;
|
||||
const { price } = request.body;
|
||||
|
||||
const item = await MarketPrice.findById(id);
|
||||
if (!item) {
|
||||
return reply.status(404).send({
|
||||
success: false,
|
||||
message: "Item not found",
|
||||
});
|
||||
}
|
||||
|
||||
item.price = price;
|
||||
item.priceType = "manual"; // Mark as manually updated
|
||||
item.lastUpdated = new Date();
|
||||
await item.save();
|
||||
|
||||
console.log(
|
||||
`⚙️ Admin ${request.user.username} updated price for ${item.name}: $${price}`
|
||||
);
|
||||
|
||||
return reply.send({
|
||||
success: true,
|
||||
message: "Price updated successfully",
|
||||
item,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("❌ Failed to update market price:", error);
|
||||
return reply.status(500).send({
|
||||
success: false,
|
||||
message: "Failed to update price",
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// DELETE /admin/marketprices/:id - Delete a MarketPrice item
|
||||
fastify.delete(
|
||||
"/marketprices/:id",
|
||||
{
|
||||
preHandler: [authenticate, isAdmin],
|
||||
schema: {
|
||||
params: {
|
||||
type: "object",
|
||||
required: ["id"],
|
||||
properties: {
|
||||
id: { type: "string" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
async (request, reply) => {
|
||||
try {
|
||||
const { id } = request.params;
|
||||
|
||||
const item = await MarketPrice.findByIdAndDelete(id);
|
||||
if (!item) {
|
||||
return reply.status(404).send({
|
||||
success: false,
|
||||
message: "Item not found",
|
||||
});
|
||||
}
|
||||
|
||||
console.log(
|
||||
`⚙️ Admin ${request.user.username} deleted market price for ${item.name}`
|
||||
);
|
||||
|
||||
return reply.send({
|
||||
success: true,
|
||||
message: "Item deleted successfully",
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("❌ Failed to delete market price:", error);
|
||||
return reply.status(500).send({
|
||||
success: false,
|
||||
message: "Failed to delete item",
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user