Use Cases (Atualizado: 01/06/2026)

Criar um dashboard SaaS confiável com Claude Code

Guia prático de KPIs, SQL, UI Next.js, acessibilidade, permissões e revisão para dashboards SaaS.

Criar um dashboard SaaS confiável com Claude Code

Um dashboard SaaS precisa ser confiável antes de ser bonito

Um dashboard SaaS não é apenas uma página com gráficos. Ele é a mesa de trabalho onde liderança, finanças, customer success, vendas e produto decidem o que corrigir primeiro. Se o MRR não mostra moeda, a taxa de ativação usa denominador incerto ou o SLA não informa fuso horário, o time passa a decidir com base frágil.

Claude Code acelera o layout, mas precisa de uma especificação confiável. Aqui, KPI significa indicador-chave de desempenho, API contract é o acordo de dados entre backend e frontend, e stale data são dados antigos demais para uma decisão segura. O fluxo cobre definição de KPI, contrato API, agregação SQL, UI React/Next.js, acessibilidade de gráficos, filtros, estados de carregamento e erro, limites de função e revisão.

flowchart LR
  A["Definição de KPI"] --> B["API contract"]
  B --> C["Agregação SQL"]
  C --> D["Next.js API"]
  D --> E["React UI"]
  E --> F["Permissões e revisão"]
  F --> A

Defina as condições de confiança

Antes de pedir cartões e gráficos ao Claude Code, defina unidade, período, fuso horário, escopo de permissão e frescor dos dados.

ÁreaDashboard fracoDashboard confiável
UnidadeMostra 123,456Mostra JPY, USD, users ou %
PeríodoDiz “este mês”Mostra 2026-05-01 até 2026-05-31
Fuso horárioUsa padrão do bancoMostra Asia/Tokyo ou UTC na API e UI
PermissõesEsconde cards no ReactValida tenant e role na API e no SQL
FrescorDados antigos parecem normaisMostra generatedAt e estado stale
ExplicaçãoSó há gráficoInclui fórmula, período comparado e exclusões

Três usos práticos aparecem quase sempre. Liderança acompanha MRR, ARR, churn, ARPA e mix de planos com moeda clara. Produto acompanha ativação, trial conversion e adoção de funcionalidades com denominador explícito. Customer success acompanha tickets abertos, violações de SLA, contas de risco e filas por responsável sem cruzar limites de acesso. Finanças acrescenta faturas pagas, reembolsos, vencidos e receita por plano.

API contract pronto para copiar

Defina a resposta de /api/dashboard/summary antes da UI. Este JSON pode ser usado como mock, fixture ou teste frontend.

{
  "meta": {
    "tenantId": "tenant_123",
    "dateRange": {
      "from": "2026-05-01",
      "to": "2026-05-31"
    },
    "timezone": "Asia/Tokyo",
    "generatedAt": "2026-06-01T09:00:00+09:00",
    "staleAfterMinutes": 60,
    "permissions": ["dashboard:read", "finance:read"]
  },
  "metrics": [
    {
      "id": "mrr",
      "label": "MRR",
      "unit": "JPY",
      "current": 4820000,
      "previous": 4510000,
      "deltaPct": 6.87,
      "formula": "paid monthly subscription revenue excluding tax and refunds"
    },
    {
      "id": "activation_rate",
      "label": "Activation rate",
      "unit": "percent",
      "current": 42.3,
      "previous": 39.8,
      "deltaPct": 2.5,
      "formula": "activated accounts divided by new trial accounts"
    }
  ],
  "series": [
    { "date": "2026-05-01", "mrr": 4380000, "activationRate": 37.5 },
    { "date": "2026-05-08", "mrr": 4510000, "activationRate": 39.1 },
    { "date": "2026-05-15", "mrr": 4620000, "activationRate": 40.4 },
    { "date": "2026-05-22", "mrr": 4740000, "activationRate": 41.8 },
    { "date": "2026-05-31", "mrr": 4820000, "activationRate": 42.3 }
  ]
}

O bloco meta é essencial. A interface não deve adivinhar período, fuso, permissões ou validade. Para rotas e componentes no App Router, use a documentação oficial do Next.js App Router. Para componentes, estado e renderização condicional, consulte React Learn.

SQL com fronteiras explícitas

Este exemplo PostgreSQL assume uma tabela invoices com tenant_id, status, amount_cents, paid_at e currency. A data final é exclusiva para evitar bugs de fim de mês e fuso horário.

WITH params AS (
  SELECT
    'tenant_123'::text AS tenant_id,
    '2026-05-01'::date AS from_date,
    '2026-06-01'::date AS exclusive_to_date,
    'Asia/Tokyo'::text AS report_timezone
),
paid_invoices AS (
  SELECT
    date_trunc('day', paid_at AT TIME ZONE params.report_timezone)::date AS paid_day,
    amount_cents,
    currency
  FROM invoices
  CROSS JOIN params
  WHERE invoices.tenant_id = params.tenant_id
    AND invoices.status = 'paid'
    AND invoices.paid_at >= params.from_date AT TIME ZONE params.report_timezone
    AND invoices.paid_at < params.exclusive_to_date AT TIME ZONE params.report_timezone
),
daily AS (
  SELECT
    paid_day,
    currency,
    SUM(amount_cents) / 100.0 AS revenue,
    COUNT(*) AS paid_invoice_count
  FROM paid_invoices
  GROUP BY paid_day, currency
)
SELECT
  paid_day,
  currency,
  revenue,
  paid_invoice_count,
  SUM(revenue) OVER (PARTITION BY currency ORDER BY paid_day) AS cumulative_revenue
FROM daily
ORDER BY paid_day, currency;

Armadilhas comuns: usar 23:59:59 como fim do período, tirar média de percentuais diários, esquecer filtro de tenant em consulta de gráfico e misturar reembolso com receita paga. Peça ao Claude Code para revisar isso como correção de negócio, não como estilo.

Tipos TypeScript para proteger o contrato

export type MetricUnit = "JPY" | "USD" | "users" | "percent" | "count";

export type DashboardMetric = {
  id: "mrr" | "activation_rate" | "trial_conversion" | "support_sla";
  label: string;
  unit: MetricUnit;
  current: number;
  previous: number;
  deltaPct: number;
  formula: string;
};

export type DashboardPoint = {
  date: string;
  mrr: number;
  activationRate: number;
};

export type DashboardPayload = {
  meta: {
    tenantId: string;
    dateRange: {
      from: string;
      to: string;
    };
    timezone: string;
    generatedAt: string;
    staleAfterMinutes: number;
    permissions: string[];
  };
  metrics: DashboardMetric[];
  series: DashboardPoint[];
};

UI React com loading, erro, vazio e stale

Este client component cobre loading, 403, erro de API, dados vazios, stale data, cards KPI e gráfico com tabela fallback. Para acessibilidade, use MDN Web Accessibility e a wiki de acessibilidade do Recharts.

"use client";

import { useEffect, useMemo, useState } from "react";
import { CartesianGrid, Line, LineChart, ResponsiveContainer, Tooltip, XAxis, YAxis } from "recharts";
import type { DashboardPayload, DashboardMetric } from "./dashboard-types";

type LoadState =
  | { status: "loading" }
  | { status: "error"; message: string }
  | { status: "empty" }
  | { status: "ready"; data: DashboardPayload };

const money = new Intl.NumberFormat("ja-JP", {
  style: "currency",
  currency: "JPY",
  maximumFractionDigits: 0
});

function formatMetric(metric: DashboardMetric) {
  if (metric.unit === "JPY") return money.format(metric.current);
  if (metric.unit === "percent") return `${metric.current.toFixed(1)}%`;
  return metric.current.toLocaleString("pt-BR");
}

function isStale(data: DashboardPayload) {
  const generated = new Date(data.meta.generatedAt).getTime();
  const limit = data.meta.staleAfterMinutes * 60 * 1000;
  return Date.now() - generated > limit;
}

export default function DashboardPage() {
  const [state, setState] = useState<LoadState>({ status: "loading" });

  useEffect(() => {
    const controller = new AbortController();

    async function loadDashboard() {
      try {
        setState({ status: "loading" });
        const response = await fetch(
          "/api/dashboard/summary?from=2026-05-01&to=2026-05-31&timezone=Asia/Tokyo",
          { signal: controller.signal }
        );

        if (response.status === 403) {
          setState({ status: "error", message: "Você não tem permissão para ver este dashboard." });
          return;
        }

        if (!response.ok) {
          throw new Error(`Dashboard API failed: ${response.status}`);
        }

        const data = (await response.json()) as DashboardPayload;
        setState(data.metrics.length === 0 ? { status: "empty" } : { status: "ready", data });
      } catch (error) {
        if (!controller.signal.aborted) {
          setState({ status: "error", message: error instanceof Error ? error.message : "Unknown dashboard error." });
        }
      }
    }

    loadDashboard();
    return () => controller.abort();
  }, []);

  if (state.status === "loading") return <section aria-busy="true" className="p-6">Carregando dashboard...</section>;
  if (state.status === "empty") return <section className="p-6">Não há KPIs para este filtro.</section>;
  if (state.status === "error") {
    return (
      <section role="alert" className="p-6">
        <h2 className="text-lg font-semibold">Não foi possível carregar o dashboard</h2>
        <p>{state.message}</p>
        <button className="mt-4 rounded bg-slate-900 px-4 py-2 text-white" onClick={() => location.reload()}>
          Tentar novamente
        </button>
      </section>
    );
  }

  return <DashboardContent data={state.data} />;
}

function DashboardContent({ data }: { data: DashboardPayload }) {
  const stale = useMemo(() => isStale(data), [data]);

  return (
    <main className="space-y-6 p-6">
      <header className="space-y-2">
        <h1 className="text-2xl font-bold">SaaS KPI Dashboard</h1>
        <p className="text-sm text-slate-600">
          {data.meta.dateRange.from} até {data.meta.dateRange.to}, fuso {data.meta.timezone}.
          Gerado em {data.meta.generatedAt}.
        </p>
        {stale && (
          <p role="status" className="rounded border border-amber-300 bg-amber-50 p-3 text-sm text-amber-900">
            Estes dados estão vencidos. Recalcule antes de decidir.
          </p>
        )}
      </header>

      <section className="grid gap-4 md:grid-cols-2 lg:grid-cols-4" aria-label="Key metrics">
        {data.metrics.map((metric) => (
          <article key={metric.id} className="rounded border bg-white p-4">
            <p className="text-sm text-slate-500">{metric.label}</p>
            <p className="mt-2 text-2xl font-bold">{formatMetric(metric)}</p>
            <p className="text-sm text-slate-600">Previous period {metric.deltaPct >= 0 ? "+" : ""}{metric.deltaPct.toFixed(2)}%</p>
            <p className="mt-2 text-xs text-slate-500">Formula: {metric.formula}</p>
          </article>
        ))}
      </section>

      <section className="rounded border bg-white p-4">
        <h2 className="text-lg font-semibold">MRR trend</h2>
        <ResponsiveContainer width="100%" height={320}>
          <LineChart data={data.series} accessibilityLayer aria-label="MRR trend chart">
            <CartesianGrid strokeDasharray="3 3" />
            <XAxis dataKey="date" />
            <YAxis tickFormatter={(value) => money.format(Number(value))} />
            <Tooltip formatter={(value) => money.format(Number(value))} />
            <Line type="monotone" dataKey="mrr" stroke="#2563eb" strokeWidth={2} dot={false} />
          </LineChart>
        </ResponsiveContainer>
        <table className="sr-only">
          <caption>MRR trend table</caption>
          <thead><tr><th>Date</th><th>MRR</th><th>Activation rate</th></tr></thead>
          <tbody>{data.series.map((point) => <tr key={point.date}><td>{point.date}</td><td>{point.mrr}</td><td>{point.activationRate}</td></tr>)}</tbody>
        </table>
      </section>
    </main>
  );
}

Não dependa só de cor. Alta e queda precisam de rótulos, valores, período comparado e tabela. Se a UI filtra por plano, região, responsável ou data, URL, API e SQL precisam receber os mesmos parâmetros.

Prompt de revisão para Claude Code

Os Claude Code common workflows funcionam melhor em ciclos pequenos de investigação, edição, teste e revisão.

You are reviewing a SaaS KPI dashboard implementation.

Check these files:
- app/api/dashboard/summary/route.ts
- app/dashboard/page.tsx
- lib/dashboard-types.ts
- sql/dashboard-summary.sql

Review priorities:
1. KPI correctness: units, formulas, date range, timezone, and previous-period comparison.
2. Trust signals: generatedAt, staleAfterMinutes, empty state, loading state, error state.
3. Security: tenant isolation, role boundaries, finance-only metrics, API-side authorization.
4. Accessibility: chart labels, non-color-only meaning, keyboard navigation, table fallback.
5. Maintainability: duplicated formatter logic, unsafe casts, missing tests.

Return findings as P0, P1, or P2. Include file paths, exact code references, and a suggested fix.
Do not rewrite the whole dashboard unless a finding requires it.

Para continuar, leia visualização de dados com Claude Code e o guia de RBAC. Para transformar isso em padrão de equipe, ClaudeCodeLab oferece templates Claude Code e treinamento e consultoria para dicionários KPI, prompts de revisão e regras do repositório.

Ao testar este fluxo com dados SaaS de exemplo do ClaudeCodeLab, o maior ganho foi não começar pela tela. Quando o contrato passou a incluir fuso, horário de geração, stale status e permissões, a revisão do Claude Code ficou objetiva. A UI evolui mais rápido quando as condições de confiança vêm antes do acabamento visual.

#Claude Code #dashboard SaaS #React #Next.js #visualização de dados #Recharts
Grátis

PDF grátis: cheatsheet do Claude Code

Informe seu e-mail e baixe uma página com comandos, hábitos de revisão e workflows seguros.

Cuidamos dos seus dados e não enviamos spam.

Masa

Sobre o autor

Masa

Engenheiro focado em workflows práticos com Claude Code.