Add initial work from Codex
This commit is contained in:
363
frontend/src/App.tsx
Normal file
363
frontend/src/App.tsx
Normal file
@@ -0,0 +1,363 @@
|
||||
import { trace, SpanStatusCode } from "@opentelemetry/api";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { startTransition, useDeferredValue } from "react";
|
||||
import {
|
||||
Area,
|
||||
AreaChart,
|
||||
CartesianGrid,
|
||||
Line,
|
||||
LineChart,
|
||||
ResponsiveContainer,
|
||||
Tooltip,
|
||||
XAxis,
|
||||
YAxis,
|
||||
} from "recharts";
|
||||
|
||||
import { getDashboard } from "./api/client";
|
||||
import { useAuth } from "./auth/AuthContext";
|
||||
|
||||
const money = new Intl.NumberFormat("en-US", {
|
||||
style: "currency",
|
||||
currency: "USD",
|
||||
maximumFractionDigits: 0,
|
||||
});
|
||||
|
||||
const tracer = trace.getTracer("bi-frontend-ui");
|
||||
|
||||
function formatCompactDate(value: string): string {
|
||||
return new Date(value).toLocaleDateString("en-US", {
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
});
|
||||
}
|
||||
|
||||
function formatTooltipMoney(
|
||||
value: string | number | readonly (string | number)[] | undefined,
|
||||
): string {
|
||||
const raw = Array.isArray(value) ? Number(value[0]) : Number(value);
|
||||
return money.format(Number.isFinite(raw) ? raw : 0);
|
||||
}
|
||||
|
||||
function formatTooltipNumber(
|
||||
value: string | number | readonly (string | number)[] | undefined,
|
||||
): string {
|
||||
const raw = Array.isArray(value) ? Number(value[0]) : Number(value);
|
||||
return Number.isFinite(raw) ? raw.toFixed(2) : "0.00";
|
||||
}
|
||||
|
||||
export default function App() {
|
||||
const auth = useAuth();
|
||||
const dashboardQuery = useQuery({
|
||||
queryKey: ["dashboard"],
|
||||
queryFn: getDashboard,
|
||||
staleTime: 30_000,
|
||||
refetchInterval: 120_000,
|
||||
enabled: auth.authenticated || !auth.enabled,
|
||||
});
|
||||
|
||||
const deferredRankings = useDeferredValue(
|
||||
dashboardQuery.data?.rankings ?? [],
|
||||
);
|
||||
|
||||
const chartHistory =
|
||||
dashboardQuery.data?.history.slice(-120).map((point) => ({
|
||||
date: point.date,
|
||||
actual: point.revenue,
|
||||
forecast: null as number | null,
|
||||
lower: null as number | null,
|
||||
upper: null as number | null,
|
||||
})) ?? [];
|
||||
const chartForecast =
|
||||
dashboardQuery.data?.forecasts.slice(0, 45).map((point) => ({
|
||||
date: point.date,
|
||||
actual: null as number | null,
|
||||
forecast: point.predicted_revenue,
|
||||
lower: point.lower_bound,
|
||||
upper: point.upper_bound,
|
||||
})) ?? [];
|
||||
const trendData = [...chartHistory, ...chartForecast];
|
||||
|
||||
const refreshData = () => {
|
||||
tracer.startActiveSpan("frontend.refresh_click", async (span) => {
|
||||
try {
|
||||
startTransition(() => {
|
||||
void dashboardQuery.refetch();
|
||||
});
|
||||
span.setStatus({ code: SpanStatusCode.OK });
|
||||
} catch (error) {
|
||||
span.recordException(error as Error);
|
||||
span.setStatus({
|
||||
code: SpanStatusCode.ERROR,
|
||||
message: "Failed to refresh dashboard data.",
|
||||
});
|
||||
} finally {
|
||||
span.end();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
if (auth.loading) {
|
||||
return <div className="loading-shell">Initializing OIDC session...</div>;
|
||||
}
|
||||
|
||||
if (auth.error) {
|
||||
return (
|
||||
<div className="loading-shell">
|
||||
Authentication setup error.
|
||||
<br />
|
||||
{auth.error}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (auth.enabled && !auth.authenticated) {
|
||||
return (
|
||||
<div className="loading-shell">
|
||||
Authentication required.
|
||||
<br />
|
||||
<button
|
||||
className="refresh-button"
|
||||
onClick={() => void auth.login()}
|
||||
type="button"
|
||||
>
|
||||
Sign In with OIDC
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (dashboardQuery.isLoading) {
|
||||
return (
|
||||
<div className="loading-shell">
|
||||
Loading telemetry-enabled BI dashboard...
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (dashboardQuery.error || !dashboardQuery.data) {
|
||||
return (
|
||||
<div className="loading-shell">
|
||||
Dashboard could not load.
|
||||
<br />
|
||||
{(dashboardQuery.error as Error | undefined)?.message ??
|
||||
"No response from backend."}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const { kpis, recommendations, telemetry } = dashboardQuery.data;
|
||||
const topScore = deferredRankings[0]?.score ?? 0;
|
||||
|
||||
return (
|
||||
<main className="app-shell">
|
||||
<div className="radial-glow" />
|
||||
<header className="dashboard-header">
|
||||
<div>
|
||||
<p className="eyebrow">Business Intelligence Command Center</p>
|
||||
<h1>Warehouse Forecasting and Ranking Dashboard</h1>
|
||||
<p className="subtitle">
|
||||
Data sources: <strong>WorldWideImporters</strong> +{" "}
|
||||
<strong>AdventureWorks2022DWH</strong> (read-only) with
|
||||
OpenTelemetry traces from browser to SQL.
|
||||
</p>
|
||||
<p className="trace-id">
|
||||
Last backend trace:{" "}
|
||||
<code>{telemetry.backendTraceId ?? "missing-trace-id-header"}</code>
|
||||
</p>
|
||||
</div>
|
||||
<div className="auth-actions">
|
||||
<p className="subtitle">
|
||||
User: <strong>{auth.subject ?? "unknown"}</strong>
|
||||
</p>
|
||||
<div className="header-actions">
|
||||
<button
|
||||
className="refresh-button"
|
||||
onClick={refreshData}
|
||||
type="button"
|
||||
>
|
||||
Refresh
|
||||
</button>
|
||||
{auth.enabled ? (
|
||||
<button
|
||||
className="logout-button"
|
||||
onClick={() => void auth.logout()}
|
||||
type="button"
|
||||
>
|
||||
Sign Out
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section className="kpi-grid">
|
||||
<article className="kpi-card">
|
||||
<p>Total Revenue</p>
|
||||
<h2>{money.format(kpis.total_revenue)}</h2>
|
||||
</article>
|
||||
<article className="kpi-card">
|
||||
<p>Gross Margin</p>
|
||||
<h2>{kpis.gross_margin_pct.toFixed(2)}%</h2>
|
||||
</article>
|
||||
<article className="kpi-card">
|
||||
<p>Avg Order Value</p>
|
||||
<h2>{money.format(kpis.avg_order_value)}</h2>
|
||||
</article>
|
||||
<article className="kpi-card">
|
||||
<p>Total Quantity</p>
|
||||
<h2>
|
||||
{kpis.total_quantity.toLocaleString("en-US", {
|
||||
maximumFractionDigits: 0,
|
||||
})}
|
||||
</h2>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<section className="panel-grid">
|
||||
<article className="panel wide">
|
||||
<div className="panel-title-row">
|
||||
<h3>Revenue Trend + Forecast</h3>
|
||||
<span>{trendData.length} points</span>
|
||||
</div>
|
||||
<div className="chart-wrap">
|
||||
<ResponsiveContainer width="100%" height={320}>
|
||||
<LineChart data={trendData}>
|
||||
<CartesianGrid
|
||||
strokeDasharray="4 4"
|
||||
stroke="rgba(255,255,255,0.08)"
|
||||
/>
|
||||
<XAxis
|
||||
dataKey="date"
|
||||
tickFormatter={formatCompactDate}
|
||||
stroke="rgba(255,255,255,0.65)"
|
||||
/>
|
||||
<YAxis
|
||||
tickFormatter={(value) => money.format(value)}
|
||||
stroke="rgba(255,255,255,0.65)"
|
||||
/>
|
||||
<Tooltip
|
||||
labelFormatter={(label) =>
|
||||
new Date(label).toLocaleDateString("en-US")
|
||||
}
|
||||
formatter={formatTooltipMoney}
|
||||
/>
|
||||
<Area
|
||||
type="monotone"
|
||||
dataKey="upper"
|
||||
stroke="none"
|
||||
fill="rgba(90, 201, 255, 0.1)"
|
||||
/>
|
||||
<Area
|
||||
type="monotone"
|
||||
dataKey="lower"
|
||||
stroke="none"
|
||||
fill="rgba(15, 20, 31, 0.9)"
|
||||
/>
|
||||
<Line
|
||||
type="monotone"
|
||||
dataKey="actual"
|
||||
stroke="#f9de70"
|
||||
strokeWidth={2.5}
|
||||
dot={false}
|
||||
/>
|
||||
<Line
|
||||
type="monotone"
|
||||
dataKey="forecast"
|
||||
stroke="#57d4ff"
|
||||
strokeWidth={2.5}
|
||||
strokeDasharray="8 5"
|
||||
dot={false}
|
||||
/>
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article className="panel">
|
||||
<div className="panel-title-row">
|
||||
<h3>Top Product Score</h3>
|
||||
<span>Weighted ranking index</span>
|
||||
</div>
|
||||
<div className="score-wrap">
|
||||
<ResponsiveContainer width="100%" height={240}>
|
||||
<AreaChart
|
||||
data={[
|
||||
{ label: "baseline", value: 0 },
|
||||
{ label: "current", value: topScore },
|
||||
]}
|
||||
>
|
||||
<CartesianGrid
|
||||
strokeDasharray="3 3"
|
||||
stroke="rgba(255,255,255,0.08)"
|
||||
/>
|
||||
<XAxis dataKey="label" stroke="rgba(255,255,255,0.65)" />
|
||||
<YAxis stroke="rgba(255,255,255,0.65)" />
|
||||
<Tooltip formatter={formatTooltipNumber} />
|
||||
<Area
|
||||
type="monotone"
|
||||
dataKey="value"
|
||||
stroke="#8ef2c7"
|
||||
fill="rgba(142, 242, 199, 0.28)"
|
||||
/>
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
<p className="score-caption">
|
||||
Current leader score <strong>{topScore.toFixed(2)}</strong> / 100
|
||||
</p>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article className="panel wide">
|
||||
<div className="panel-title-row">
|
||||
<h3>Product Rankings</h3>
|
||||
<span>Top {deferredRankings.length}</span>
|
||||
</div>
|
||||
<div className="table-wrap">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Rank</th>
|
||||
<th>Product</th>
|
||||
<th>Category</th>
|
||||
<th>Revenue</th>
|
||||
<th>Margin</th>
|
||||
<th>Score</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{deferredRankings.map((item) => (
|
||||
<tr key={`${item.rank}-${item.product_id}`}>
|
||||
<td>{item.rank}</td>
|
||||
<td>{item.product_name}</td>
|
||||
<td>{item.category}</td>
|
||||
<td>{money.format(item.revenue)}</td>
|
||||
<td>{item.margin_pct.toFixed(2)}%</td>
|
||||
<td>{item.score.toFixed(2)}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article className="panel">
|
||||
<div className="panel-title-row">
|
||||
<h3>Recommendations</h3>
|
||||
<span>Action queue</span>
|
||||
</div>
|
||||
<ul className="recommendations-list">
|
||||
{recommendations.map((item, index) => (
|
||||
<li key={`${item.title}-${index}`}>
|
||||
<span className={`priority ${item.priority}`}>
|
||||
{item.priority}
|
||||
</span>
|
||||
<h4>{item.title}</h4>
|
||||
<p>{item.summary}</p>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</article>
|
||||
</section>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user