Use Cases (Mis à jour: 01/06/2026)

Créer un dashboard SaaS fiable avec Claude Code

Guide pratique pour KPIs, SQL, UI Next.js, accessibilité, permissions et revue d'un dashboard SaaS.

Créer un dashboard SaaS fiable avec Claude Code

Un dashboard SaaS doit d’abord être fiable

Un dashboard SaaS n’est pas une simple page de graphiques. C’est l’espace où direction, finance, customer success, vente et produit décident quoi corriger en priorité. Si le MRR n’affiche pas sa devise, si le taux d’activation a un dénominateur flou ou si le SLA cache le fuseau horaire, l’interface pousse l’équipe vers de mauvaises décisions.

Claude Code peut accélérer la mise en page, mais il faut lui donner les conditions de confiance. Ici, KPI signifie indicateur clé de performance, API contract désigne le contrat de données entre backend et frontend, et stale data désigne une donnée trop ancienne pour décider. Nous allons couvrir la définition des KPIs, le contrat API, l’agrégation SQL, l’UI React/Next.js, l’accessibilité des graphiques, les filtres, les états de chargement et d’erreur, les limites de rôle et la boucle de revue.

flowchart LR
  A["Définition KPI"] --> B["API contract"]
  B --> C["Agrégation SQL"]
  C --> D["API Next.js"]
  D --> E["UI React"]
  E --> F["Permissions et revue"]
  F --> A

Définir les conditions de confiance

Avant de demander à Claude Code de générer des cartes et des courbes, définissez l’unité, la période, le fuseau horaire, le périmètre de permission et la fraîcheur des données.

SujetDashboard fragileDashboard fiable
UnitéAffiche 123,456Affiche JPY, USD, users ou %
PériodeDit “ce mois-ci”Affiche 2026-05-01 à 2026-05-31
Fuseau horaireUtilise le défaut de la DBAffiche Asia/Tokyo ou UTC dans l’API et l’UI
PermissionsCache des cartes côté ReactVérifie tenant et rôle dans l’API et SQL
FraîcheurLes vieilles données semblent normalesAffiche generatedAt et l’état stale
ExplicationGraphique seulDonne formule, période comparée et exclusions

Les cas d’usage pratiques sont au moins trois. Les dirigeants suivent MRR, ARR, churn, ARPA et mix de plans avec devise et règles de prorata. Le produit suit activation, conversion d’essai, adoption de fonctionnalités et cohorts avec un dénominateur explicite. Le customer success suit tickets ouverts, dépassements de SLA, comptes à risque et files par owner avec des frontières de rôle strictes. La finance ajoute factures payées, remboursements, impayés et revenu par plan.

Contrat API prêt à copier

Fixez la réponse de /api/dashboard/summary avant l’UI. Ce JSON peut servir de mock, fixture ou test 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 }
  ]
}

Le bloc meta est indispensable. L’UI ne doit pas deviner la période, le fuseau, les droits ou la fraîcheur. Pour les routes et composants App Router, appuyez-vous sur la documentation officielle Next.js App Router. Pour les composants, le rendu conditionnel et l’état, consultez React Learn.

SQL avec frontières explicites

Cet exemple PostgreSQL suppose une table invoices avec tenant_id, status, amount_cents, paid_at et currency. La date de fin est exclusive pour éviter les bugs de fin de mois et de fuseau horaire.

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;

Les pièges concrets sont récurrents: utiliser 23:59:59 en fin de période, moyenner des pourcentages quotidiens, oublier le filtre tenant, ou compter des remboursements comme revenu payé. Demandez à Claude Code de traiter ces points comme des risques métier, pas comme des détails de style.

Types TypeScript du contrat

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 avec chargement, erreur, vide et stale

Ce client component gère chargement, 403, erreur API, absence de données, stale data, cartes KPI et graphique avec tableau de secours. Pour l’accessibilité, utilisez MDN Web Accessibility et la wiki Recharts accessibility.

"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("fr-FR");
}

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: "Vous n'avez pas le droit de voir ce 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">Chargement du dashboard...</section>;
  if (state.status === "empty") return <section className="p-6">Aucun KPI disponible pour ce filtre.</section>;
  if (state.status === "error") {
    return (
      <section role="alert" className="p-6">
        <h2 className="text-lg font-semibold">Le dashboard n'a pas chargé</h2>
        <p>{state.message}</p>
        <button className="mt-4 rounded bg-slate-900 px-4 py-2 text-white" onClick={() => location.reload()}>
          Réessayer
        </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} à {data.meta.dateRange.to}, fuseau {data.meta.timezone}.
          Généré à {data.meta.generatedAt}.
        </p>
        {stale && (
          <p role="status" className="rounded border border-amber-300 bg-amber-50 p-3 text-sm text-amber-900">
            Ces données sont périmées. Relancez l'agrégation avant de décider.
          </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>
  );
}

Ne transmettez pas le sens par la couleur seule. Les variations doivent aussi être visibles par les libellés, les valeurs, la période comparée et un tableau. Si l’UI filtre par plan, région, owner ou date, l’URL, l’API et SQL doivent utiliser les mêmes paramètres.

Prompt de revue pour Claude Code

Les Claude Code common workflows recommandent de boucler par petites étapes: investigation, édition, test, revue.

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.

Pour compléter, lisez la visualisation de données avec Claude Code et le guide RBAC. Pour transformer ce flux en standard d’équipe, ClaudeCodeLab propose des templates Claude Code et de la formation et consultation Claude Code pour dictionnaires KPI, prompts de revue et règles de dépôt.

Après avoir essayé ce flux avec des données SaaS d’exemple ClaudeCodeLab, le gain principal a été de ne pas commencer par l’écran. Dès que le contrat contenait fuseau horaire, heure de génération, état stale et permissions, la revue Claude Code devenait précise. Une UI se construit plus vite quand les conditions de confiance sont fixées avant le polissage visuel.

#Claude Code #dashboard SaaS #React #Next.js #visualisation de données #Recharts
Gratuit

PDF gratuit: cheatsheet Claude Code

Saisissez votre email et téléchargez une page avec commandes, habitudes de review et workflow sûr.

Nous protégeons vos données et n'envoyons pas de spam.

Masa

À propos de l'auteur

Masa

Ingénieur spécialisé dans les workflows pratiques avec Claude Code.