๐ Projeto Final: Dashboard Enterprise Completo
Integre todos os conhecimentos das 3 trilhas em um dashboard de producao real com autenticacao, integracao de dados, visualizacoes avancadas e deploy na nuvem.
๐ฏ Visao Geral do Projeto
Voce construira um Dashboard Analytics Enterprise completo que integra multiplas fontes de dados, oferece visualizacoes interativas, controle de acesso por roles, e pode ser deployado em producao.
๐ Especificacao do Projeto
Requisitos funcionais, nao-funcionais e escopo
๐ฏ Requisitos Funcionais
- โ๏ธ Login com email/senha e OAuth (Google)
- โ๏ธ Roles: Admin, Manager, Analyst, Viewer
- โ๏ธ Convite de usuarios por email
- โ๏ธ Audit log de acoes
- โ๏ธ KPIs em tempo real
- โ๏ธ Graficos interativos (linha, barra, pizza)
- โ๏ธ Filtros por periodo, departamento, categoria
- โ๏ธ Export para PDF e Excel
- โ๏ธ Conexao com banco PostgreSQL
- โ๏ธ API REST para dados
- โ๏ธ Import de CSV/Excel
- โ๏ธ Webhook para updates externos
- โ๏ธ Dark/Light mode
- โ๏ธ Responsivo (desktop, tablet, mobile)
- โ๏ธ Dashboards customizaveis
- โ๏ธ Notificacoes de alertas
โก Requisitos Nao-Funcionais
๐ Estrutura de Pastas
dashboard-enterprise/ โโโ apps/ โ โโโ web/ # Frontend React โ โ โโโ src/ โ โ โ โโโ components/ # UI components โ โ โ โโโ features/ # Feature modules โ โ โ โโโ hooks/ # Custom hooks โ โ โ โโโ lib/ # Utilities โ โ โ โโโ pages/ # Route pages โ โ โ โโโ stores/ # State management โ โ โโโ package.json โ โ โ โโโ api/ # Backend Node.js โ โโโ src/ โ โ โโโ modules/ # Feature modules โ โ โโโ middleware/ # Express middlewares โ โ โโโ services/ # Business logic โ โ โโโ utils/ # Helpers โ โโโ prisma/ โ โ โโโ schema.prisma # Database schema โ โโโ package.json โ โโโ packages/ โ โโโ ui/ # Shared UI components โ โโโ config/ # ESLint, TS configs โ โโโ types/ # Shared TypeScript types โ โโโ docker-compose.yml โโโ turbo.json # Monorepo config โโโ package.json
๐๏ธ Arquitetura do Sistema
Design de alto nivel e decisoes tecnicas
๐ Diagrama de Arquitetura
๐ง Stack Tecnologico
| Camada | Tecnologia | Justificativa |
|---|---|---|
| Frontend | React 18 + Vite | Ecosystem, performance, DX |
| Styling | TailwindCSS + Radix UI | Utility-first, acessibilidade |
| State | TanStack Query + Zustand | Server state vs client state |
| Charts | Recharts | React-native, customizavel |
| Backend | Node.js + Express | TypeScript, performance |
| ORM | Prisma | Type safety, migrations |
| Auth | JWT + OAuth (Auth.js) | Stateless, social login |
| Cache | Redis | Sessions, rate limiting |
๐ป Schema do Banco de Dados
// prisma/schema.prisma
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model User {
id String @id @default(cuid())
email String @unique
name String?
passwordHash String?
role Role @default(VIEWER)
department String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
dashboards Dashboard[]
auditLogs AuditLog[]
sessions Session[]
}
model Dashboard {
id String @id @default(cuid())
name String
description String?
config Json // Layout, widgets config
isPublic Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
owner User @relation(fields: [ownerId], references: [id])
ownerId String
widgets Widget[]
}
model Widget {
id String @id @default(cuid())
type WidgetType
title String
config Json // Chart config, data source
position Json // x, y, width, height
dashboard Dashboard @relation(fields: [dashboardId], references: [id])
dashboardId String
}
model DataSource {
id String @id @default(cuid())
name String
type DataSourceType
config Json // Connection details (encrypted)
lastSync DateTime?
createdAt DateTime @default(now())
}
model Metric {
id String @id @default(cuid())
name String
value Float
category String
department String?
timestamp DateTime @default(now())
@@index([category, timestamp])
@@index([department, timestamp])
}
model AuditLog {
id String @id @default(cuid())
action String
resource String
details Json?
ip String?
timestamp DateTime @default(now())
user User @relation(fields: [userId], references: [id])
userId String
@@index([userId, timestamp])
}
enum Role {
ADMIN
MANAGER
ANALYST
VIEWER
}
enum WidgetType {
LINE_CHART
BAR_CHART
PIE_CHART
KPI_CARD
TABLE
MAP
}
enum DataSourceType {
POSTGRESQL
MYSQL
API
CSV
}
โ๏ธ Implementacao Backend
APIs, autenticacao e servicos de dados
๐ก Endpoints da API
๐ป Codigo: API de Metricas
// src/modules/metrics/metrics.controller.ts
import { Router } from 'express';
import { authenticate, authorize } from '../../middleware/auth';
import { validateRequest } from '../../middleware/validation';
import { metricsService } from './metrics.service';
import { z } from 'zod';
const router = Router();
const getMetricsSchema = z.object({
query: z.object({
range: z.enum(['24h', '7d', '30d', '90d', '1y']).default('7d'),
department: z.string().optional(),
category: z.string().optional(),
granularity: z.enum(['hour', 'day', 'week', 'month']).default('day'),
}),
});
router.get(
'/',
authenticate,
validateRequest(getMetricsSchema),
async (req, res) => {
const { range, department, category, granularity } = req.query;
// Cache key baseado nos parametros
const cacheKey = `metrics:${range}:${department || 'all'}:${category || 'all'}`;
// Tenta buscar do cache
const cached = await redis.get(cacheKey);
if (cached) {
return res.json(JSON.parse(cached));
}
const metrics = await metricsService.getMetrics({
range,
department: req.user.role === 'ADMIN' ? department : req.user.department,
category,
granularity,
});
// Cache por 5 minutos
await redis.setex(cacheKey, 300, JSON.stringify(metrics));
res.json(metrics);
}
);
// Agregacao customizada
const aggregateSchema = z.object({
body: z.object({
metrics: z.array(z.string()),
groupBy: z.array(z.enum(['department', 'category', 'date'])),
aggregation: z.enum(['sum', 'avg', 'count', 'min', 'max']),
filters: z.record(z.any()).optional(),
dateRange: z.object({
start: z.string().datetime(),
end: z.string().datetime(),
}),
}),
});
router.post(
'/aggregate',
authenticate,
authorize(['ADMIN', 'MANAGER', 'ANALYST']),
validateRequest(aggregateSchema),
async (req, res) => {
const result = await metricsService.aggregate(req.body);
res.json(result);
}
);
export default router;
๐ป Codigo: Middleware de Autenticacao
// src/middleware/auth.ts
import { Request, Response, NextFunction } from 'express';
import { verifyToken } from '../lib/jwt';
import { prisma } from '../lib/prisma';
import { auditService } from '../services/audit.service';
export interface AuthRequest extends Request {
user: {
id: string;
email: string;
role: 'ADMIN' | 'MANAGER' | 'ANALYST' | 'VIEWER';
department?: string;
};
}
export const authenticate = async (
req: Request,
res: Response,
next: NextFunction
) => {
const authHeader = req.headers.authorization;
if (!authHeader?.startsWith('Bearer ')) {
return res.status(401).json({ error: 'Authentication required' });
}
try {
const token = authHeader.split(' ')[1];
const payload = await verifyToken(token);
const user = await prisma.user.findUnique({
where: { id: payload.sub },
select: { id: true, email: true, role: true, department: true },
});
if (!user) {
return res.status(401).json({ error: 'User not found' });
}
(req as AuthRequest).user = user;
next();
} catch (error) {
auditService.log({
action: 'AUTH_FAILED',
resource: 'api',
details: { error: error.message },
ip: req.ip,
});
return res.status(401).json({ error: 'Invalid token' });
}
};
export const authorize = (allowedRoles: string[]) => {
return (req: Request, res: Response, next: NextFunction) => {
const user = (req as AuthRequest).user;
if (!allowedRoles.includes(user.role)) {
auditService.log({
action: 'ACCESS_DENIED',
resource: req.path,
userId: user.id,
details: { requiredRoles: allowedRoles, userRole: user.role },
ip: req.ip,
});
return res.status(403).json({ error: 'Insufficient permissions' });
}
next();
};
};
โ๏ธ Implementacao Frontend
Componentes, estado e visualizacoes
๐งฉ Componentes Principais
๐ป Codigo: Dashboard Page
// src/pages/Dashboard.tsx
import { useMetrics } from '@/hooks/useMetrics';
import { useDashboardFilters } from '@/stores/filters';
import { KPIGrid } from '@/components/KPIGrid';
import { ChartGrid } from '@/components/ChartGrid';
import { FilterBar } from '@/components/FilterBar';
import { DateRangePicker } from '@/components/DateRangePicker';
import { ExportMenu } from '@/components/ExportMenu';
import { Skeleton } from '@/components/ui/Skeleton';
export function DashboardPage() {
const { dateRange, department, setDateRange } = useDashboardFilters();
const { data: metrics, isLoading, error } = useMetrics({
range: dateRange,
department,
});
if (error) {
return <ErrorBoundary error={error} />;
}
return (
<div className="space-y-6">
{/* Header com filtros */}
<div className="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4">
<div>
<h1 className="text-2xl font-bold">Dashboard</h1>
<p className="text-neutral-400">Visao geral das metricas</p>
</div>
<div className="flex items-center gap-3">
<DateRangePicker value={dateRange} onChange={setDateRange} />
<FilterBar />
<ExportMenu data={metrics} />
</div>
</div>
{/* KPIs */}
{isLoading ? (
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
{[...Array(4)].map((_, i) => (
<Skeleton key={i} className="h-24" />
))}
</div>
) : (
<KPIGrid
metrics={[
{
label: 'Receita Total',
value: metrics.revenue,
change: metrics.revenueChange,
format: 'currency',
},
{
label: 'Usuarios Ativos',
value: metrics.activeUsers,
change: metrics.usersChange,
format: 'number',
},
{
label: 'Conversao',
value: metrics.conversionRate,
change: metrics.conversionChange,
format: 'percent',
},
{
label: 'Ticket Medio',
value: metrics.avgTicket,
change: metrics.ticketChange,
format: 'currency',
},
]}
/>
)}
{/* Charts Grid */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<ChartCard title="Receita por Periodo">
<LineChart
data={metrics?.revenueTimeSeries}
loading={isLoading}
/>
</ChartCard>
<ChartCard title="Vendas por Categoria">
<BarChart
data={metrics?.salesByCategory}
loading={isLoading}
/>
</ChartCard>
<ChartCard title="Distribuicao por Regiao">
<PieChart
data={metrics?.salesByRegion}
loading={isLoading}
/>
</ChartCard>
<ChartCard title="Top Produtos">
<DataTable
columns={productColumns}
data={metrics?.topProducts}
loading={isLoading}
/>
</ChartCard>
</div>
</div>
);
}
๐ป Codigo: Hook de Metricas
// src/hooks/useMetrics.ts
import { useQuery, UseQueryOptions } from '@tanstack/react-query';
import { api } from '@/lib/api';
interface MetricsParams {
range: '24h' | '7d' | '30d' | '90d' | '1y';
department?: string;
category?: string;
}
interface MetricsData {
revenue: number;
revenueChange: number;
activeUsers: number;
usersChange: number;
conversionRate: number;
conversionChange: number;
avgTicket: number;
ticketChange: number;
revenueTimeSeries: TimeSeriesPoint[];
salesByCategory: CategoryData[];
salesByRegion: RegionData[];
topProducts: Product[];
}
export function useMetrics(
params: MetricsParams,
options?: UseQueryOptions<MetricsData>
) {
return useQuery({
queryKey: ['metrics', params],
queryFn: async () => {
const { data } = await api.get<MetricsData>('/metrics', { params });
return data;
},
staleTime: 5 * 60 * 1000, // 5 minutos
refetchInterval: 60 * 1000, // Refetch a cada minuto
refetchOnWindowFocus: true,
retry: 3,
...options,
});
}
// Hook para metricas em tempo real via WebSocket
export function useRealtimeMetrics(dashboardId: string) {
const queryClient = useQueryClient();
useEffect(() => {
const ws = new WebSocket(`${WS_URL}/metrics/${dashboardId}`);
ws.onmessage = (event) => {
const update = JSON.parse(event.data);
// Atualiza cache do React Query
queryClient.setQueryData(['metrics', { dashboardId }], (old: MetricsData) => ({
...old,
...update,
}));
};
return () => ws.close();
}, [dashboardId, queryClient]);
}
๐งช Testes e Integracao
Garantia de qualidade em todas as camadas
๐ฏ Piramide de Testes
๐ป Codigo: Testes de Componente
// src/components/__tests__/KPICard.test.tsx
import { render, screen } from '@testing-library/react';
import { KPICard } from '../KPICard';
describe('KPICard', () => {
it('renders metric value correctly', () => {
render(
<KPICard
label="Receita"
value={150000}
format="currency"
change={12.5}
/>
);
expect(screen.getByText('Receita')).toBeInTheDocument();
expect(screen.getByText('R$ 150.000,00')).toBeInTheDocument();
expect(screen.getByText('+12.5%')).toBeInTheDocument();
});
it('shows negative change with correct styling', () => {
render(
<KPICard
label="Custos"
value={50000}
format="currency"
change={-8.3}
/>
);
const changeElement = screen.getByText('-8.3%');
expect(changeElement).toHaveClass('text-red-400');
});
it('renders skeleton when loading', () => {
render(<KPICard label="Receita" loading />);
expect(screen.getByTestId('kpi-skeleton')).toBeInTheDocument();
});
});
// Teste de hook com mock
describe('useMetrics', () => {
it('fetches and caches metrics', async () => {
const { result } = renderHook(() =>
useMetrics({ range: '7d' }),
{ wrapper: QueryWrapper }
);
await waitFor(() => expect(result.current.isSuccess).toBe(true));
expect(result.current.data?.revenue).toBeDefined();
expect(mockApi.get).toHaveBeenCalledWith('/metrics', {
params: { range: '7d' },
});
});
});
๐ป Codigo: Teste E2E
// e2e/dashboard.spec.ts
import { test, expect } from '@playwright/test';
test.describe('Dashboard', () => {
test.beforeEach(async ({ page }) => {
// Login via API para speed
await page.request.post('/api/auth/login', {
data: { email: 'test@example.com', password: 'test123' },
});
await page.goto('/dashboard');
});
test('displays KPIs and charts', async ({ page }) => {
// Aguarda carregamento
await expect(page.getByText('Receita Total')).toBeVisible();
await expect(page.getByText('Usuarios Ativos')).toBeVisible();
// Verifica charts
await expect(page.locator('[data-testid="line-chart"]')).toBeVisible();
await expect(page.locator('[data-testid="bar-chart"]')).toBeVisible();
});
test('filters update charts', async ({ page }) => {
// Muda filtro de periodo
await page.getByRole('button', { name: '7 dias' }).click();
await page.getByRole('option', { name: '30 dias' }).click();
// Verifica que charts atualizaram (via network)
await page.waitForResponse(resp =>
resp.url().includes('/api/metrics') &&
resp.url().includes('range=30d')
);
});
test('export generates PDF', async ({ page }) => {
// Clica em exportar
await page.getByRole('button', { name: 'Exportar' }).click();
await page.getByRole('menuitem', { name: 'PDF' }).click();
// Verifica download
const download = await page.waitForEvent('download');
expect(download.suggestedFilename()).toContain('.pdf');
});
});
๐ Deploy e Producao
Levando o projeto para o ar
๐ Checklist de Deploy
๐ Infraestrutura de Producao
๐ป GitHub Actions Completo
# .github/workflows/deploy.yml
name: CI/CD Pipeline
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
env:
NODE_VERSION: '20'
jobs:
lint-and-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v2
- uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'pnpm'
- run: pnpm install --frozen-lockfile
- run: pnpm lint
- run: pnpm type-check
- run: pnpm test:coverage
- uses: codecov/codecov-action@v3
e2e-tests:
runs-on: ubuntu-latest
needs: lint-and-test
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v2
- uses: actions/setup-node@v4
- run: pnpm install
- run: pnpm exec playwright install --with-deps
- name: Run E2E tests
run: pnpm test:e2e
env:
DATABASE_URL: ${{ secrets.TEST_DATABASE_URL }}
deploy-preview:
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
needs: [lint-and-test, e2e-tests]
steps:
- uses: actions/checkout@v4
- uses: amondnet/vercel-action@v25
with:
vercel-token: ${{ secrets.VERCEL_TOKEN }}
vercel-org-id: ${{ secrets.VERCEL_ORG_ID }}
vercel-project-id: ${{ secrets.VERCEL_PROJECT_ID }}
deploy-production:
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
needs: [lint-and-test, e2e-tests]
environment: production
steps:
- uses: actions/checkout@v4
# Deploy Frontend (Vercel)
- uses: amondnet/vercel-action@v25
with:
vercel-token: ${{ secrets.VERCEL_TOKEN }}
vercel-args: '--prod'
vercel-org-id: ${{ secrets.VERCEL_ORG_ID }}
vercel-project-id: ${{ secrets.VERCEL_PROJECT_ID }}
# Deploy Backend (Cloud Run)
- uses: google-github-actions/auth@v2
with:
credentials_json: ${{ secrets.GCP_CREDENTIALS }}
- uses: google-github-actions/deploy-cloudrun@v2
with:
service: dashboard-api
region: us-central1
source: ./apps/api
# Notify
- uses: slackapi/slack-github-action@v1
with:
payload: |
{"text": "๐ Dashboard deployed to production!"}
๐ Resultado Final
Ao completar este projeto, voce tera construido:
- โ Dashboard completo com autenticacao multi-tenant
- โ API RESTful com TypeScript e validacao
- โ Visualizacoes interativas e responsivas
- โ Pipeline CI/CD automatizado
- โ Infraestrutura escalavel na nuvem
- โ Suite de testes completa
- โ Documentacao e codigo limpo
Parabens! Voce Completou o Dashboard Mastery
144 topicos, 3 trilhas, conhecimento completo
Agora voce tem o conhecimento para construir dashboards profissionais de nivel enterprise.
Continue praticando, explorando novas tecnologias e compartilhando conhecimento!