Use Cases (Aktualisiert: 1.6.2026)

Ein vertrauenswürdiges SaaS-Dashboard mit Claude Code bauen

Praxisleitfaden für KPI-Definition, SQL, Next.js UI, Barrierefreiheit, Rollen und Review von SaaS-Dashboards.

Ein vertrauenswürdiges SaaS-Dashboard mit Claude Code bauen

Ein SaaS-Dashboard muss zuerst verlässlich sein

Ein SaaS-Dashboard ist nicht nur eine Seite mit hübschen Diagrammen. Es ist die Arbeitsfläche, auf der Geschäftsführung, Finance, Customer Success, Sales und Produkt entscheiden, was als Nächstes verbessert wird. Wenn MRR ohne Währung erscheint, die Aktivierungsrate einen unklaren Nenner hat oder SLA-Zahlen die Zeitzone verschweigen, wird das Dashboard zur Risikoquelle.

Claude Code kann Layouts schnell erzeugen, braucht aber klare Vertrauensbedingungen. KPI bedeutet hier Leistungskennzahl, API contract ist die Datenvereinbarung zwischen Backend und Frontend, und stale data sind Daten, die für Entscheidungen zu alt sein können. Dieser Leitfaden führt durch KPI-Definition, API contract, SQL-Aggregation, React/Next.js UI, Chart-Barrierefreiheit, Filter, Lade- und Fehlerzustände, Rollengrenzen und Review-Schleife.

flowchart LR
  A["KPI-Definition"] --> B["API contract"]
  B --> C["SQL-Aggregation"]
  C --> D["Next.js API"]
  D --> E["React UI"]
  E --> F["Berechtigungen und Review"]
  F --> A

Vertrauensbedingungen vor dem UI festlegen

Bevor Claude Code Karten und Charts baut, müssen Einheit, Zeitraum, Zeitzone, Berechtigungsumfang und Datenfrische feststehen.

BereichDünnes DashboardVertrauenswürdiges Dashboard
EinheitZeigt nur 123,456Zeigt JPY, USD, users oder %
ZeitraumSagt “diesen Monat”Zeigt 2026-05-01 bis 2026-05-31
ZeitzoneNutzt DB-DefaultZeigt Asia/Tokyo oder UTC in API und UI
RechteVersteckt Karten in ReactPrüft Tenant und Rolle in API und SQL
FrischeAlte Daten sehen normal ausZeigt generatedAt und stale Status
ErklärungNur ChartEnthält Formel, Vergleichszeitraum und Ausschlüsse

Typische Anwendungsfälle sind mindestens diese drei: Management betrachtet MRR, ARR, Churn, ARPA und Plan-Mix mit klarer Währung und Monatslogik. Produktteams prüfen Aktivierung, Trial Conversion, Feature Adoption und Kohorten mit eindeutigem Nenner. Customer Success sieht offene Tickets, SLA-Verstöße, Risikokonten und Owner-Queues mit strikten Rollengrenzen. Finance ergänzt bezahlte Rechnungen, Refunds, überfällige Beträge und Umsatz nach Plan.

API contract zum Kopieren

Fixieren Sie die Antwort von /api/dashboard/summary, bevor die Oberfläche entsteht. Dieses JSON funktioniert als Mock, Fixture oder Frontend-Testdaten.

{
  "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 }
  ]
}

meta ist nicht optional. Die UI sollte Zeitraum, Zeitzone, Rechte und Datenfrische nicht erraten. Für Routing und Komponenten im App Router nutzen Sie die offiziellen Next.js App Router docs. Für Komponenten, State und bedingtes Rendering ist React Learn die solide Basis.

SQL-Aggregation mit klaren Grenzen

Dieses PostgreSQL-Beispiel geht von einer Tabelle invoices mit tenant_id, status, amount_cents, paid_at und currency aus. Das Periodenende ist exklusiv, damit Monatsende, Millisekunden und Zeitzonen sauber bleiben.

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;

Konkrete Fallen: 23:59:59 als Monatsende, Durchschnitt aus Tagesquoten, fehlender Tenant-Filter in Chart-Abfragen und Refunds als bezahlter Umsatz. Lassen Sie Claude Code diese Punkte als fachliche Korrektheit prüfen, nicht als Stilfragen.

TypeScript-Typen für den Vertrag

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[];
};

React UI mit Loading, Error, Empty und Stale

Diese client component behandelt Ladezustand, 403, API-Fehler, leere Daten, stale data, KPI-Karten und ein Diagramm mit Tabellen-Fallback. Für Barrierefreiheit sind MDN Web Accessibility und die Recharts accessibility wiki die Referenzen.

"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("de-DE");
}

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: "Sie haben keine Berechtigung für dieses 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">Dashboard wird geladen...</section>;
  if (state.status === "empty") return <section className="p-6">Für diesen Filter sind keine KPIs verfügbar.</section>;
  if (state.status === "error") {
    return (
      <section role="alert" className="p-6">
        <h2 className="text-lg font-semibold">Dashboard konnte nicht geladen werden</h2>
        <p>{state.message}</p>
        <button className="mt-4 rounded bg-slate-900 px-4 py-2 text-white" onClick={() => location.reload()}>
          Erneut versuchen
        </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} bis {data.meta.dateRange.to}, Zeitzone {data.meta.timezone}.
          Generiert um {data.meta.generatedAt}.
        </p>
        {stale && (
          <p role="status" className="rounded border border-amber-300 bg-amber-50 p-3 text-sm text-amber-900">
            Diese Daten sind veraltet. Aggregieren Sie neu, bevor Sie entscheiden.
          </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>
  );
}

Verlassen Sie sich nicht nur auf Farbe. Steigende und fallende Werte brauchen Labels, Zahlen, Vergleichszeitraum und Tabellen-Fallback. Wenn die UI nach Plan, Region, Owner oder Datum filtert, müssen URL, API und SQL dieselben Parameter nutzen.

Review-Prompt für Claude Code

Die offiziellen Claude Code common workflows funktionieren am besten in kleinen Schleifen aus Untersuchung, Änderung, Test und Review.

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.

Passend dazu sind Datenvisualisierung mit Claude Code und der RBAC-Leitfaden. Wenn dieser Ablauf Teamstandard werden soll, bietet ClaudeCodeLab Claude Code Templates sowie Training und Beratung für KPI-Wörterbücher, Review-Prompts und Repository-Regeln.

Beim Test mit ClaudeCodeLab-Beispieldaten war der wichtigste Effekt, nicht mit dem Bildschirm zu starten. Sobald der Contract Zeitzone, Generierungszeit, stale Status und Rechte enthielt, wurden die Claude-Code-Reviews konkret. Das Dashboard entsteht schneller, wenn die Vertrauensbedingungen vor dem visuellen Feinschliff feststehen.

#Claude Code #SaaS-Dashboard #React #Next.js #Datenvisualisierung #Recharts
Kostenlos

Kostenloses PDF: Claude-Code-Cheatsheet

E-Mail eintragen und eine Seite mit Befehlen, Review-Gewohnheiten und sicheren Workflows herunterladen.

Wir schützen Ihre Daten und senden keinen Spam.

Masa

Über den Autor

Masa

Engineer für praktische Claude-Code-Workflows und Team-Einführung.