Skip to content

Dashboard

O dashboard administrativo exibe métricas em tempo real, estatísticas financeiras e atividades recentes da plataforma.

Visão Geral

Métricas Principais

Stats Cards

typescript
// get-dashboard-stats.use-case.ts
interface DashboardStats {
  users: {
    total: number;
    activeToday: number;
    newToday: number;
    growth: number;        // % em relação a ontem
  };
  
  financial: {
    revenueToday: bigint;
    profitToday: bigint;
    depositsToday: bigint;
    withdrawalsToday: bigint;
    pendingWithdrawals: number;
  };
  
  games: {
    casesOpenedToday: number;
    battlesToday: number;
    upgradesToday: number;
    totalVolume: bigint;
  };
  
  topItems: {
    mostDropped: Item;
    highestValue: Item;
    mostOpened: Case;
  };
}

Implementação

typescript
// get-dashboard-stats.use-case.ts
@Injectable()
export class GetDashboardStatsUseCase {
  async execute(): Promise<DashboardStats> {
    const today = startOfDay(new Date());
    const yesterday = subDays(today, 1);
    
    const [
      totalUsers,
      activeToday,
      newToday,
      activeYesterday,
      revenueToday,
      depositsToday,
      withdrawalsToday,
      pendingWithdrawals,
      casesOpenedToday,
      battlesToday,
    ] = await Promise.all([
      this.userRepository.count(),
      this.userRepository.countActiveAfter(today),
      this.userRepository.countCreatedAfter(today),
      this.userRepository.countActiveAfter(yesterday),
      this.transactionRepository.sumRevenueAfter(today),
      this.depositRepository.sumConfirmedAfter(today),
      this.withdrawalRepository.sumCompletedAfter(today),
      this.withdrawalRepository.countByStatus('PENDING'),
      this.caseOpeningRepository.countAfter(today),
      this.battleRepository.countAfter(today),
    ]);
    
    // Calcular crescimento
    const growth = activeYesterday > 0
      ? ((activeToday - activeYesterday) / activeYesterday) * 100
      : 0;
    
    // Calcular lucro
    const profitToday = revenueToday - withdrawalsToday;
    
    return {
      users: {
        total: totalUsers,
        activeToday,
        newToday,
        growth: Math.round(growth * 100) / 100,
      },
      financial: {
        revenueToday,
        profitToday,
        depositsToday,
        withdrawalsToday,
        pendingWithdrawals,
      },
      games: {
        casesOpenedToday,
        battlesToday,
        upgradesToday: 0, // TODO
        totalVolume: revenueToday,
      },
      // ...
    };
  }
}

Gráfico de Receita

Endpoint

http
GET /api/admin/dashboard/revenue-chart?period=7d

Response

json
{
  "data": [
    { "date": "2024-01-10", "revenue": 15000, "profit": 12000, "deposits": 20000, "withdrawals": 8000 },
    { "date": "2024-01-11", "revenue": 18000, "profit": 14500, "deposits": 25000, "withdrawals": 10500 },
    { "date": "2024-01-12", "revenue": 12000, "profit": 9000, "deposits": 15000, "withdrawals": 6000 }
  ],
  "summary": {
    "totalRevenue": 45000,
    "totalProfit": 35500,
    "avgDailyRevenue": 15000,
    "growthRate": 12.5
  }
}

Implementação

typescript
// get-revenue-chart.use-case.ts
@Injectable()
export class GetRevenueChartUseCase {
  async execute(period: '7d' | '30d' | '90d'): Promise<RevenueChart> {
    const days = period === '7d' ? 7 : period === '30d' ? 30 : 90;
    const startDate = subDays(new Date(), days);
    
    // Query agregada por dia
    const data = await this.prisma.$queryRaw<RevenueDataPoint[]>`
      SELECT 
        DATE(created_at) as date,
        SUM(CASE WHEN type IN ('CASE_OPEN', 'BATTLE_ENTRY', 'UPGRADE') THEN amount_cents ELSE 0 END) as revenue,
        SUM(CASE WHEN type = 'DEPOSIT' THEN amount_cents ELSE 0 END) as deposits,
        SUM(CASE WHEN type = 'WITHDRAWAL' THEN amount_cents ELSE 0 END) as withdrawals
      FROM "Transaction"
      WHERE created_at >= ${startDate}
      GROUP BY DATE(created_at)
      ORDER BY date ASC
    `;
    
    // Calcular lucro por dia
    const chartData = data.map((d) => ({
      date: d.date,
      revenue: Number(d.revenue),
      profit: Number(d.revenue) - Number(d.withdrawals),
      deposits: Number(d.deposits),
      withdrawals: Number(d.withdrawals),
    }));
    
    // Calcular resumo
    const totalRevenue = chartData.reduce((sum, d) => sum + d.revenue, 0);
    const totalProfit = chartData.reduce((sum, d) => sum + d.profit, 0);
    
    return {
      data: chartData,
      summary: {
        totalRevenue,
        totalProfit,
        avgDailyRevenue: totalRevenue / days,
        growthRate: this.calculateGrowthRate(chartData),
      },
    };
  }
}

Frontend Component

tsx
// components/dashboard/revenue-chart.tsx
'use client';

import { useQuery } from '@tanstack/react-query';
import {
  LineChart,
  Line,
  XAxis,
  YAxis,
  CartesianGrid,
  Tooltip,
  Legend,
  ResponsiveContainer,
} from 'recharts';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';

export function RevenueChart() {
  const [period, setPeriod] = useState<'7d' | '30d' | '90d'>('7d');
  
  const { data, isLoading } = useQuery({
    queryKey: ['revenue-chart', period],
    queryFn: () => fetchRevenueChart(period),
  });

  return (
    <Card>
      <CardHeader className="flex flex-row items-center justify-between">
        <CardTitle>Receita e Lucro</CardTitle>
        <Select value={period} onValueChange={setPeriod}>
          <SelectTrigger className="w-32">
            <SelectValue />
          </SelectTrigger>
          <SelectContent>
            <SelectItem value="7d">7 dias</SelectItem>
            <SelectItem value="30d">30 dias</SelectItem>
            <SelectItem value="90d">90 dias</SelectItem>
          </SelectContent>
        </Select>
      </CardHeader>
      
      <CardContent>
        {isLoading ? (
          <Skeleton className="h-80" />
        ) : (
          <ResponsiveContainer width="100%" height={320}>
            <LineChart data={data?.data}>
              <CartesianGrid strokeDasharray="3 3" />
              <XAxis dataKey="date" />
              <YAxis />
              <Tooltip formatter={(v) => `R$ ${(v / 100).toFixed(2)}`} />
              <Legend />
              <Line
                type="monotone"
                dataKey="revenue"
                name="Receita"
                stroke="#22c55e"
                strokeWidth={2}
              />
              <Line
                type="monotone"
                dataKey="profit"
                name="Lucro"
                stroke="#3b82f6"
                strokeWidth={2}
              />
            </LineChart>
          </ResponsiveContainer>
        )}
      </CardContent>
    </Card>
  );
}

Atividades Recentes

Endpoint

http
GET /api/admin/dashboard/recent-activity?limit=20

Response

json
{
  "activities": [
    {
      "id": "1",
      "type": "CASE_OPEN",
      "user": { "id": "123", "username": "player1" },
      "description": "Abriu Caixa Premium",
      "value": 1500,
      "item": { "name": "AWP | Dragon Lore", "rarity": "EXTRAORDINARY" },
      "createdAt": "2024-01-15T10:30:00Z"
    },
    {
      "id": "2",
      "type": "DEPOSIT",
      "user": { "id": "456", "username": "player2" },
      "description": "Depósito via PIX",
      "value": 10000,
      "createdAt": "2024-01-15T10:29:00Z"
    },
    {
      "id": "3",
      "type": "WITHDRAWAL_REQUEST",
      "user": { "id": "789", "username": "player3" },
      "description": "Solicitou saque",
      "value": 5000,
      "createdAt": "2024-01-15T10:28:00Z"
    }
  ]
}

Frontend Component

tsx
// components/dashboard/recent-activity.tsx
export function RecentActivity() {
  const { data, isLoading } = useQuery({
    queryKey: ['recent-activity'],
    queryFn: fetchRecentActivity,
    refetchInterval: 10000, // Atualizar a cada 10s
  });

  return (
    <Card>
      <CardHeader>
        <CardTitle className="flex items-center gap-2">
          <ActivityIcon className="h-5 w-5" />
          Atividades Recentes
        </CardTitle>
      </CardHeader>
      
      <CardContent>
        <div className="space-y-4">
          {data?.activities.map((activity) => (
            <div
              key={activity.id}
              className="flex items-center justify-between border-b pb-4"
            >
              <div className="flex items-center gap-3">
                <ActivityIcon type={activity.type} />
                <div>
                  <p className="font-medium">{activity.user.username}</p>
                  <p className="text-sm text-muted-foreground">
                    {activity.description}
                  </p>
                </div>
              </div>
              
              <div className="text-right">
                <p className="font-medium">
                  R$ {(activity.value / 100).toFixed(2)}
                </p>
                <p className="text-sm text-muted-foreground">
                  {formatDistanceToNow(new Date(activity.createdAt))}
                </p>
              </div>
            </div>
          ))}
        </div>
      </CardContent>
    </Card>
  );
}

Stats Cards Component

tsx
// components/dashboard/stats-cards.tsx
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import {
  Users,
  DollarSign,
  TrendingUp,
  Package,
  ArrowUpRight,
  ArrowDownRight,
} from 'lucide-react';

interface StatsCardsProps {
  stats: DashboardStats;
}

export function StatsCards({ stats }: StatsCardsProps) {
  const cards = [
    {
      title: 'Total Usuários',
      value: stats.users.total.toLocaleString(),
      change: `+${stats.users.newToday} hoje`,
      changeType: 'positive',
      icon: Users,
    },
    {
      title: 'Receita Hoje',
      value: `R$ ${(Number(stats.financial.revenueToday) / 100).toFixed(2)}`,
      change: `${stats.users.growth > 0 ? '+' : ''}${stats.users.growth}%`,
      changeType: stats.users.growth >= 0 ? 'positive' : 'negative',
      icon: DollarSign,
    },
    {
      title: 'Lucro Hoje',
      value: `R$ ${(Number(stats.financial.profitToday) / 100).toFixed(2)}`,
      change: 'vs. depósitos',
      changeType: 'neutral',
      icon: TrendingUp,
    },
    {
      title: 'Caixas Abertas',
      value: stats.games.casesOpenedToday.toLocaleString(),
      change: 'hoje',
      changeType: 'neutral',
      icon: Package,
    },
  ];

  return (
    <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
      {cards.map((card) => (
        <Card key={card.title}>
          <CardHeader className="flex flex-row items-center justify-between pb-2">
            <CardTitle className="text-sm font-medium text-muted-foreground">
              {card.title}
            </CardTitle>
            <card.icon className="h-4 w-4 text-muted-foreground" />
          </CardHeader>
          <CardContent>
            <div className="text-2xl font-bold">{card.value}</div>
            <p className="text-xs text-muted-foreground flex items-center gap-1">
              {card.changeType === 'positive' && (
                <ArrowUpRight className="h-3 w-3 text-green-500" />
              )}
              {card.changeType === 'negative' && (
                <ArrowDownRight className="h-3 w-3 text-red-500" />
              )}
              {card.change}
            </p>
          </CardContent>
        </Card>
      ))}
    </div>
  );
}

Dashboard Page

tsx
// app/(dashboard)/page.tsx
import { StatsCards } from '@/components/dashboard/stats-cards';
import { RevenueChart } from '@/components/dashboard/revenue-chart';
import { RecentActivity } from '@/components/dashboard/recent-activity';
import { PendingWithdrawals } from '@/components/dashboard/pending-withdrawals';
import { ActiveBattles } from '@/components/dashboard/active-battles';

export default async function DashboardPage() {
  const stats = await fetchDashboardStats();

  return (
    <div className="space-y-6">
      <h1 className="text-3xl font-bold">Dashboard</h1>
      
      {/* Stats Cards */}
      <StatsCards stats={stats} />
      
      {/* Gráficos */}
      <div className="grid gap-6 lg:grid-cols-2">
        <RevenueChart />
        <RecentActivity />
      </div>
      
      {/* Pendências */}
      <div className="grid gap-6 lg:grid-cols-2">
        <PendingWithdrawals />
        <ActiveBattles />
      </div>
    </div>
  );
}

Alertas

tsx
// components/dashboard/alerts.tsx
export function DashboardAlerts() {
  const { data: alerts } = useQuery({
    queryKey: ['dashboard-alerts'],
    queryFn: fetchAlerts,
    refetchInterval: 30000,
  });

  if (!alerts?.length) return null;

  return (
    <div className="space-y-2">
      {alerts.map((alert) => (
        <Alert
          key={alert.id}
          variant={alert.level === 'CRITICAL' ? 'destructive' : 'default'}
        >
          <AlertCircle className="h-4 w-4" />
          <AlertTitle>{alert.title}</AlertTitle>
          <AlertDescription>{alert.message}</AlertDescription>
        </Alert>
      ))}
    </div>
  );
}

Documentação Técnica CSGOFlip