Claude Code से भरोसेमंद SaaS डैशबोर्ड बनाना
KPI, SQL, Next.js UI, accessibility, permissions और review loop के साथ SaaS dashboard बनाने की व्यावहारिक गाइड।
SaaS dashboard पहले भरोसेमंद होना चाहिए
SaaS dashboard सिर्फ सुंदर charts वाली screen नहीं है। यही वह जगह है जहां leadership, finance, customer success, sales और product team तय करती हैं कि पहले किस समस्या को ठीक करना है। अगर MRR में currency नहीं दिखती, activation rate का denominator साफ नहीं है, या SLA में timezone छिपा है, तो dashboard गलत निर्णय करा सकता है।
Claude Code layout जल्दी बना सकता है, लेकिन उसे भरोसे की शर्तें साफ देनी होंगी। इस लेख में KPI का मतलब key performance indicator है, API contract का मतलब backend और frontend के बीच data agreement है, और stale data का मतलब ऐसा data है जो निर्णय के लिए पुराना हो सकता है। हम KPI definition, API contract, SQL aggregation, React/Next.js UI, chart accessibility, filters, loading और error states, role boundaries और review loop देखेंगे।
flowchart LR
A["KPI definition"] --> B["API contract"]
B --> C["SQL aggregation"]
C --> D["Next.js API"]
D --> E["React UI"]
E --> F["Permissions and review"]
F --> A
KPI की trust conditions पहले लिखें
Claude Code से cards और charts बनवाने से पहले units, date range, timezone, permission scope और freshness तय करें।
| Area | कमजोर dashboard | भरोसेमंद dashboard |
|---|---|---|
| Unit | सिर्फ 123,456 दिखाता है | JPY, USD, users, % साफ दिखाता है |
| Date range | ”this month” लिखता है | 2026-05-01 से 2026-05-31 दिखाता है |
| Timezone | database default पर चलता है | API और UI में Asia/Tokyo या UTC दिखाता है |
| Permissions | React में cards छिपाता है | API और SQL में tenant और role check करता है |
| Freshness | पुराना data भी normal दिखता है | generatedAt और stale status दिखाता है |
| Explanation | सिर्फ graph | formula, comparison period और exclusions देता है |
व्यावहारिक use cases कम से कम तीन होते हैं। Leadership MRR, ARR, churn, ARPA और plan mix देखती है, इसलिए currency और monthly allocation साफ होना चाहिए। Product team activation, trial conversion और feature adoption देखती है, इसलिए denominator साफ होना चाहिए। Customer success open tickets, SLA breaches और risk accounts देखती है, इसलिए role boundaries मजबूत होनी चाहिए। Finance paid invoices, refunds, overdue invoices और revenue by plan देखती है।
Copy-paste API contract
/api/dashboard/summary की response shape पहले fix करें। यह JSON mock API, fixture या frontend test में सीधे इस्तेमाल हो सकता है।
{
"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 जरूरी है। UI को date range, timezone, permissions या freshness guess नहीं करनी चाहिए। Next.js App Router के लिए official Next.js docs देखें। React components, state और conditional rendering के लिए React Learn उपयोगी है।
SQL aggregation में boundaries साफ रखें
यह PostgreSQL query मानती है कि invoices table में tenant_id, status, amount_cents, paid_at, currency fields हैं। end date exclusive है, इसलिए month-end और timezone bugs कम होते हैं।
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;
Common pitfalls हैं: 23:59:59 से period बंद करना, daily percentages का average लेना, tenant filter भूलना, और refunds को paid revenue में मिला देना। Claude Code से इन्हें business correctness issues की तरह review कराएं।
TypeScript types contract बचाते हैं
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 में loading, error, empty और stale states
यह client component loading, 403, API error, empty data, stale data, KPI cards और table fallback वाला chart handle करता है। Accessibility के लिए MDN Web Accessibility और Recharts accessibility wiki देखें।
"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("hi-IN");
}
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: "आपको यह dashboard देखने की permission नहीं है." });
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 load हो रहा है...</section>;
if (state.status === "empty") return <section className="p-6">इस filter के लिए KPI data नहीं है.</section>;
if (state.status === "error") {
return (
<section role="alert" className="p-6">
<h2 className="text-lg font-semibold">Dashboard load नहीं हुआ</h2>
<p>{state.message}</p>
<button className="mt-4 rounded bg-slate-900 px-4 py-2 text-white" onClick={() => location.reload()}>
Retry
</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} तक, timezone {data.meta.timezone}.
Generated at {data.meta.generatedAt}.
</p>
{stale && (
<p role="status" className="rounded border border-amber-300 bg-amber-50 p-3 text-sm text-amber-900">
यह data stale है. Decision से पहले aggregation refresh करें.
</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>
);
}
Meaning सिर्फ color से न दिखाएं। Labels, values, comparison period और table fallback दें। अगर UI plan, region, owner या date से filter करता है, तो URL, API और SQL में वही parameters जाने चाहिए।
Claude Code review prompt
Claude Code common workflows की तरह investigation, edit, test और review को छोटे cycles में चलाएं।
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.
आगे पढ़ने के लिए Claude Code data visualization और RBAC implementation guide देखें। अगर आपकी team इस workflow को standard बनाना चाहती है, तो ClaudeCodeLab के Claude Code templates और training तथा consultation KPI dictionary, review prompts और repository rules बनाने में मदद करते हैं।
ClaudeCodeLab के sample SaaS data पर यह flow आजमाने पर सबसे बड़ा लाभ यह था कि हमने screen से शुरुआत नहीं की। जब contract में timezone, generated time, stale status और permissions आ गए, तो Claude Code की review बहुत ठोस हो गई। भरोसे की conditions पहले fix हों, तो UI polish तेज और सुरक्षित होती है।
मुफ़्त PDF: Claude Code cheatsheet
Email डालें और commands, review habits तथा safe workflow वाली एक-page PDF पाएँ.
हम आपका data सुरक्षित रखते हैं और spam नहीं भेजते.
लेखक के बारे में
Masa
Claude Code workflow और team adoption पर काम करने वाला engineer.
संबंधित लेख
Claude Code Obsidian to CLAUDE.md workflow: context बार-बार न समझाएं
Obsidian notes को CLAUDE.md operating notes में बदलकर Claude Code sessions को resume करना आसान बनाएं.
Claude Code Revenue CTA Routing: article से PDF, Gumroad और consultation तक
Reader intent के आधार पर free PDF, Gumroad products और consultation तक CTA route करने वाला workflow.
Claude Code टीम हैंडऑफ नियम: review proof, permissions, rollback और revenue path
Claude Code टीम काम के लिए evidence, permission rules, rollback, free PDF, Gumroad और consultation path वाला handoff.