feat: integrate real data into dashboard
- Created dashboard service with DB queries for users, economy, events - Added client stats provider with 30s caching for Discord metrics - Implemented /api/stats endpoint aggregating all dashboard data - Created useDashboardStats React hook with auto-refresh - Updated Dashboard.tsx to display real data with loading/error states - Added comprehensive test coverage (11 tests passing) - Replaced all mock values with live Discord and database metrics
This commit is contained in:
157
shared/modules/dashboard/dashboard.service.ts
Normal file
157
shared/modules/dashboard/dashboard.service.ts
Normal file
@@ -0,0 +1,157 @@
|
||||
import { DrizzleClient } from "@shared/db/DrizzleClient";
|
||||
import { users, transactions, moderationCases, inventory } from "@db/schema";
|
||||
import { desc, sql, and, gte } from "drizzle-orm";
|
||||
import type { RecentEvent } from "./dashboard.types";
|
||||
|
||||
export const dashboardService = {
|
||||
/**
|
||||
* Get count of active users from database
|
||||
*/
|
||||
getActiveUserCount: async (): Promise<number> => {
|
||||
const result = await DrizzleClient
|
||||
.select({ count: sql<string>`COUNT(*)` })
|
||||
.from(users)
|
||||
.where(sql`${users.isActive} = true`);
|
||||
|
||||
return Number(result[0]?.count || 0);
|
||||
},
|
||||
|
||||
/**
|
||||
* Get total user count
|
||||
*/
|
||||
getTotalUserCount: async (): Promise<number> => {
|
||||
const result = await DrizzleClient
|
||||
.select({ count: sql<string>`COUNT(*)` })
|
||||
.from(users);
|
||||
|
||||
return Number(result[0]?.count || 0);
|
||||
},
|
||||
|
||||
/**
|
||||
* Get economy statistics
|
||||
*/
|
||||
getEconomyStats: async (): Promise<{
|
||||
totalWealth: bigint;
|
||||
avgLevel: number;
|
||||
topStreak: number;
|
||||
}> => {
|
||||
const allUsers = await DrizzleClient.select().from(users);
|
||||
|
||||
const totalWealth = allUsers.reduce(
|
||||
(acc: bigint, u: any) => acc + (u.balance || 0n),
|
||||
0n
|
||||
);
|
||||
|
||||
const avgLevel = allUsers.length > 0
|
||||
? Math.round(
|
||||
allUsers.reduce((acc: number, u: any) => acc + (u.level || 1), 0) / allUsers.length
|
||||
)
|
||||
: 1;
|
||||
|
||||
const topStreak = allUsers.reduce(
|
||||
(max: number, u: any) => Math.max(max, u.dailyStreak || 0),
|
||||
0
|
||||
);
|
||||
|
||||
return { totalWealth, avgLevel, topStreak };
|
||||
},
|
||||
|
||||
/**
|
||||
* Get total items in circulation
|
||||
*/
|
||||
getTotalItems: async (): Promise<number> => {
|
||||
const result = await DrizzleClient
|
||||
.select({ total: sql<string>`COALESCE(SUM(${inventory.quantity}), 0)` })
|
||||
.from(inventory);
|
||||
|
||||
return Number(result[0]?.total || 0);
|
||||
},
|
||||
|
||||
/**
|
||||
* Get recent transactions as events (last 24 hours)
|
||||
*/
|
||||
getRecentTransactions: async (limit: number = 10): Promise<RecentEvent[]> => {
|
||||
const oneDayAgo = new Date(Date.now() - 24 * 60 * 60 * 1000);
|
||||
|
||||
const recentTx = await DrizzleClient.query.transactions.findMany({
|
||||
limit,
|
||||
orderBy: [desc(transactions.createdAt)],
|
||||
where: gte(transactions.createdAt, oneDayAgo),
|
||||
with: {
|
||||
user: true,
|
||||
},
|
||||
});
|
||||
|
||||
return recentTx.map((tx: any) => ({
|
||||
type: 'info' as const,
|
||||
message: `${tx.user?.username || 'Unknown'}: ${tx.description || 'Transaction'}`,
|
||||
timestamp: tx.createdAt || new Date(),
|
||||
icon: getTransactionIcon(tx.type),
|
||||
}));
|
||||
},
|
||||
|
||||
/**
|
||||
* Get recent moderation cases as events (last 24 hours)
|
||||
*/
|
||||
getRecentModerationCases: async (limit: number = 10): Promise<RecentEvent[]> => {
|
||||
const oneDayAgo = new Date(Date.now() - 24 * 60 * 60 * 1000);
|
||||
|
||||
const recentCases = await DrizzleClient.query.moderationCases.findMany({
|
||||
limit,
|
||||
orderBy: [desc(moderationCases.createdAt)],
|
||||
where: gte(moderationCases.createdAt, oneDayAgo),
|
||||
});
|
||||
|
||||
return recentCases.map((modCase: any) => ({
|
||||
type: modCase.type === 'warn' || modCase.type === 'ban' ? 'error' : 'info',
|
||||
message: `${modCase.type.toUpperCase()}: ${modCase.username} - ${modCase.reason}`,
|
||||
timestamp: modCase.createdAt || new Date(),
|
||||
icon: getModerationIcon(modCase.type),
|
||||
}));
|
||||
},
|
||||
|
||||
/**
|
||||
* Get combined recent events (transactions + moderation)
|
||||
*/
|
||||
getRecentEvents: async (limit: number = 10): Promise<RecentEvent[]> => {
|
||||
const [txEvents, modEvents] = await Promise.all([
|
||||
dashboardService.getRecentTransactions(limit),
|
||||
dashboardService.getRecentModerationCases(limit),
|
||||
]);
|
||||
|
||||
// Combine and sort by timestamp
|
||||
const allEvents = [...txEvents, ...modEvents]
|
||||
.sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime())
|
||||
.slice(0, limit);
|
||||
|
||||
return allEvents;
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Helper to get icon for transaction type
|
||||
*/
|
||||
function getTransactionIcon(type: string): string {
|
||||
if (type.includes("LOOT")) return "🌠";
|
||||
if (type.includes("GIFT")) return "🎁";
|
||||
if (type.includes("SHOP")) return "🛒";
|
||||
if (type.includes("DAILY")) return "☀️";
|
||||
if (type.includes("QUEST")) return "📜";
|
||||
if (type.includes("TRANSFER")) return "💸";
|
||||
return "💫";
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to get icon for moderation type
|
||||
*/
|
||||
function getModerationIcon(type: string): string {
|
||||
switch (type) {
|
||||
case 'warn': return '⚠️';
|
||||
case 'timeout': return '⏸️';
|
||||
case 'kick': return '👢';
|
||||
case 'ban': return '🔨';
|
||||
case 'note': return '📝';
|
||||
case 'prune': return '🧹';
|
||||
default: return '🛡️';
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user