import { Injectable } from '@nestjs/common'; import { PrismaService } from '../../shared/prisma/prisma.service'; import { Decimal } from '@prisma/client/runtime/library'; function dec(v: Decimal | null | undefined) { return v?.toString() ?? '0'; } function sub(a: Decimal | null | undefined, b: Decimal | null | undefined) { return new Decimal(a ?? 0).sub(b ?? 0).toString(); } @Injectable() export class AdminDashboardService { constructor(private prisma: PrismaService) {} async getOverview() { const today = new Date(); today.setHours(0, 0, 0, 0); const trend7d = await Promise.all( Array.from({ length: 7 }, (_, i) => { const dayStart = new Date(today); dayStart.setDate(dayStart.getDate() - (6 - i)); const dayEnd = new Date(dayStart); dayEnd.setDate(dayEnd.getDate() + 1); return this.prisma.bet .aggregate({ where: { placedAt: { gte: dayStart, lt: dayEnd } }, _sum: { stake: true, actualReturn: true }, _count: true, }) .then((agg) => ({ date: dayStart.toISOString().slice(0, 10), label: `${dayStart.getMonth() + 1}/${dayStart.getDate()}`, betCount: agg._count, stake: dec(agg._sum.stake), payout: dec(agg._sum.actualReturn), ggr: sub(agg._sum.stake, agg._sum.actualReturn), })); }), ); const playerWhere = { userType: 'PLAYER', deletedAt: null }; const [ todayBets, yesterdayBets, pendingBets, betStatusToday, matchGroups, matchTotal, playerTotal, playerActive, playerSuspended, playerDirect, newPlayersToday, agentProfiles, agentsActive, walletAgg, recentBets, recentPlayers, ] = await Promise.all([ this.prisma.bet.aggregate({ where: { placedAt: { gte: today } }, _sum: { stake: true, actualReturn: true }, _count: true, }), this.prisma.bet.aggregate({ where: { placedAt: { gte: new Date(today.getTime() - 86400000), lt: today, }, }, _sum: { stake: true, actualReturn: true }, _count: true, }), this.prisma.bet.count({ where: { status: 'PENDING' } }), this.prisma.bet.groupBy({ by: ['status'], where: { placedAt: { gte: today } }, _count: { _all: true }, _sum: { stake: true }, }), this.prisma.match.groupBy({ by: ['status'], where: { deletedAt: null }, _count: { _all: true }, }), this.prisma.match.count({ where: { deletedAt: null } }), this.prisma.user.count({ where: playerWhere }), this.prisma.user.count({ where: { ...playerWhere, status: 'ACTIVE' } }), this.prisma.user.count({ where: { ...playerWhere, status: 'SUSPENDED' } }), this.prisma.user.count({ where: { ...playerWhere, parentId: null }, }), this.prisma.user.count({ where: { ...playerWhere, createdAt: { gte: today } }, }), this.prisma.agentProfile.aggregate({ _sum: { creditLimit: true, usedCredit: true }, _count: { _all: true }, }), this.prisma.agentProfile.count({ where: { status: 'ACTIVE' } }), this.prisma.wallet.aggregate({ where: { user: playerWhere }, _sum: { availableBalance: true, frozenBalance: true }, _count: { _all: true }, }), this.prisma.bet.findMany({ take: 8, orderBy: { placedAt: 'desc' }, include: { user: { select: { username: true } } }, }), this.prisma.user.findMany({ where: playerWhere, take: 6, orderBy: { createdAt: 'desc' }, select: { id: true, username: true, status: true, createdAt: true, parent: { select: { username: true } }, }, }), ]); const matchByStatus: Record = {}; for (const g of matchGroups) { matchByStatus[g.status] = g._count._all; } const todayBetByStatus: Record = {}; for (const g of betStatusToday) { todayBetByStatus[g.status] = { count: g._count._all, stake: dec(g._sum.stake), }; } const creditLimit = agentProfiles._sum.creditLimit ?? new Decimal(0); const usedCredit = agentProfiles._sum.usedCredit ?? new Decimal(0); return { generatedAt: new Date().toISOString(), trend7d, today: { betCount: todayBets._count, stake: dec(todayBets._sum.stake), payout: dec(todayBets._sum.actualReturn), ggr: sub(todayBets._sum.stake, todayBets._sum.actualReturn), newPlayers: newPlayersToday, }, yesterday: { betCount: yesterdayBets._count, stake: dec(yesterdayBets._sum.stake), payout: dec(yesterdayBets._sum.actualReturn), ggr: sub(yesterdayBets._sum.stake, yesterdayBets._sum.actualReturn), }, users: { playersTotal: playerTotal, playersActive: playerActive, playersSuspended: playerSuspended, playersDirect: playerDirect, agentsTotal: agentProfiles._count._all, agentsActive, }, wallets: { totalAvailable: dec(walletAgg._sum.availableBalance), totalFrozen: dec(walletAgg._sum.frozenBalance), playerWalletCount: walletAgg._count._all, }, agents: { totalCreditLimit: dec(creditLimit), totalUsedCredit: dec(usedCredit), totalAvailableCredit: creditLimit.sub(usedCredit).toString(), }, matches: { total: matchTotal, draft: matchByStatus.DRAFT ?? 0, published: matchByStatus.PUBLISHED ?? 0, closed: matchByStatus.CLOSED ?? 0, cancelled: matchByStatus.CANCELLED ?? 0, pendingSettlement: matchByStatus.PENDING_SETTLEMENT ?? 0, settled: matchByStatus.SETTLED ?? 0, }, bets: { pendingTotal: pendingBets, todayByStatus: todayBetByStatus, }, recentBets: recentBets.map((b) => ({ betNo: b.betNo, username: b.user.username, stake: dec(b.stake), status: b.status, placedAt: b.placedAt, })), recentPlayers: recentPlayers.map((p) => ({ id: p.id.toString(), username: p.username, status: p.status, parentUsername: p.parent?.username ?? null, createdAt: p.createdAt, })), }; } }