Inicio / Trilha 3 / Modulo 3.8
Trilha 3 - Avancado Projeto Final

๐Ÿ† 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.

6
Topicos
8h
Duracao
Expert
Nivel
Pratico
Tipo

๐ŸŽฏ 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.

๐Ÿ“Š
Frontend
React + TailwindCSS + Recharts + TanStack Query
โš™๏ธ
Backend
Node.js + TypeScript + Prisma + Redis
โ˜๏ธ
Infraestrutura
Docker + GitHub Actions + Vercel/Cloud Run
1

๐Ÿ“‹ Especificacao do Projeto

Requisitos funcionais, nao-funcionais e escopo

๐ŸŽฏ Requisitos Funcionais

Autenticacao e Acesso
  • โ˜‘๏ธ Login com email/senha e OAuth (Google)
  • โ˜‘๏ธ Roles: Admin, Manager, Analyst, Viewer
  • โ˜‘๏ธ Convite de usuarios por email
  • โ˜‘๏ธ Audit log de acoes
Dashboard Core
  • โ˜‘๏ธ KPIs em tempo real
  • โ˜‘๏ธ Graficos interativos (linha, barra, pizza)
  • โ˜‘๏ธ Filtros por periodo, departamento, categoria
  • โ˜‘๏ธ Export para PDF e Excel
Dados e Integracoes
  • โ˜‘๏ธ Conexao com banco PostgreSQL
  • โ˜‘๏ธ API REST para dados
  • โ˜‘๏ธ Import de CSV/Excel
  • โ˜‘๏ธ Webhook para updates externos
UX e Configuracao
  • โ˜‘๏ธ Dark/Light mode
  • โ˜‘๏ธ Responsivo (desktop, tablet, mobile)
  • โ˜‘๏ธ Dashboards customizaveis
  • โ˜‘๏ธ Notificacoes de alertas

โšก Requisitos Nao-Funcionais

๐Ÿ“ˆ
Performance
LCP < 2.5s
๐Ÿ”’
Seguranca
OWASP Top 10
๐Ÿ“ฑ
Responsivo
Mobile-first
โ™ฟ
Acessibilidade
WCAG 2.1 AA

๐Ÿ“ 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
2

๐Ÿ—๏ธ Arquitetura do Sistema

Design de alto nivel e decisoes tecnicas

๐Ÿ“ Diagrama de Arquitetura

Usuarios
๐Ÿ‘ฅ
Browsers
Edge
๐ŸŒ
CDN
Vercel Edge
Frontend
โš›๏ธ
React SPA
Static hosting
Backend
โš™๏ธ
API
Cloud Run
Data
๐Ÿ—„๏ธ
PostgreSQL
+ Redis cache
โ† โ†’ โ† โ†’ โ† โ†’ โ† โ†’

๐Ÿ”ง 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
}
3

โš™๏ธ Implementacao Backend

APIs, autenticacao e servicos de dados

๐Ÿ“ก Endpoints da API

POST /api/auth/login
Login com credenciais
GET /api/dashboards
Listar dashboards
GET /api/metrics?range=7d
Metricas com filtros
POST /api/metrics/aggregate
Agregacoes customizadas
PUT /api/dashboards/:id
Atualizar dashboard
POST /api/export/:format
Export PDF/Excel

๐Ÿ’ป 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();
  };
};
4

โš›๏ธ Implementacao Frontend

Componentes, estado e visualizacoes

๐Ÿงฉ Componentes Principais

๐Ÿ“Š
KPICard
Metricas em destaque
๐Ÿ“ˆ
LineChart
Series temporais
๐Ÿ“Š
BarChart
Comparacoes
๐Ÿฅง
PieChart
Distribuicoes
๐Ÿ“‹
DataTable
Listas virtuais
๐Ÿ”
FilterBar
Filtros globais
๐Ÿ“…
DatePicker
Selecao de periodo
๐Ÿ“ค
ExportMenu
PDF, Excel, CSV

๐Ÿ’ป 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]);
}
5

๐Ÿงช Testes e Integracao

Garantia de qualidade em todas as camadas

๐ŸŽฏ Piramide de Testes

E2E (Playwright)
5% - Fluxos criticos
Integracao (Testing Library)
20% - APIs, componentes complexos
Unitarios (Vitest)
75% - Funcoes, hooks, utils

๐Ÿ’ป 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');
  });
});
6

๐Ÿš€ Deploy e Producao

Levando o projeto para o ar

๐Ÿ“‹ Checklist de Deploy

Pre-Deploy
โœ“ Testes passando (unit, integration, e2e)
โœ“ Build otimizado (bundle analyzer)
โœ“ Environment variables configuradas
โœ“ Migrations aplicadas em staging
โœ“ Security scan (npm audit, SAST)
Pos-Deploy
โ†’ Smoke tests em producao
โ†’ Monitorar metricas (LCP, error rate)
โ†’ Verificar logs de erro
โ†’ Rollback plan pronto
โ†’ Notificar stakeholders

๐ŸŒ Infraestrutura de Producao

โ–ฒ
Vercel
Frontend
โ˜๏ธ
Cloud Run
API Backend
๐Ÿ˜
Supabase
PostgreSQL
๐Ÿ“Š
Upstash
Redis
Custo estimado: $50-100/mes para MVPs

๐Ÿ’ป 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
Repositorio do projeto:
github.com/seuuser/dashboard-enterprise
๐ŸŽ“

Parabens! Voce Completou o Dashboard Mastery

144 topicos, 3 trilhas, conhecimento completo

Trilha 1
Fundamentos
8 modulos, 48 topicos
Trilha 2
Tecnicas
8 modulos, 48 topicos
Trilha 3
Avancado
8 modulos, 48 topicos

Agora voce tem o conhecimento para construir dashboards profissionais de nivel enterprise.

Continue praticando, explorando novas tecnologias e compartilhando conhecimento!