"use client";
import { useQuery } from "@tanstack/react-query";
import { cockpitApi, type CockpitSummary } from "@/lib/api/cockpit";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Skeleton } from "@/components/ui/skeleton";
import { OfflineState } from "@/components/ui/offline-state";
import { HelpTip } from "@/components/ui/help-tip";
import { useUsageTimeSeries } from "@/hooks/use-usage";
import { SpendTrendChart } from "./spend-trend-chart";
import type { UsageTimePoint } from "@/types";
// ---------------------------------------------------------------------------
// Loading skeleton — three grouped skeleton blocks
// ---------------------------------------------------------------------------
function ScorecardSkeleton() {
return (
{/* Group 1: Delivery + Spend */}
{/* Group 2: Spend */}
{/* Group 3: Speed + Objectives */}
);
}
// ---------------------------------------------------------------------------
// Section header helper
// ---------------------------------------------------------------------------
function SectionLabel({ children, ...props }: React.ComponentProps<"p">) {
return (
{children}
);
}
// ---------------------------------------------------------------------------
// Delivery section
// ---------------------------------------------------------------------------
interface DeliveryMetricProps {
label: string;
value: number;
hint: string;
}
function DeliveryMetric({ label, value, hint }: DeliveryMetricProps) {
return (
);
}
interface DeliverySectionProps {
delivery: CockpitSummary["delivery"];
}
function DeliverySection({ delivery }: DeliverySectionProps) {
return (
);
}
// ---------------------------------------------------------------------------
// Spend section
// ---------------------------------------------------------------------------
interface SpendSectionProps {
spend: CockpitSummary["spend"];
spendTrend: UsageTimePoint[] | undefined;
spendTrendLoading: boolean;
}
function SpendSection({
spend,
spendTrend,
spendTrendLoading,
}: SpendSectionProps) {
const {
monthly_budget_cap_usd,
spend_30d_usd,
projected_monthly_usd,
over_budget,
} = spend;
// Red/destructive only when cap is a non-null number AND over_budget is true
const isOverBudget = monthly_budget_cap_usd !== null && over_budget;
return (
Spend
30-day spend
${spend_30d_usd.toFixed(2)}
{projected_monthly_usd !== null && (
Projected monthly
${projected_monthly_usd.toFixed(2)}
)}
Monthly cap
{monthly_budget_cap_usd === null ? (
No budget cap set
) : (
${monthly_budget_cap_usd.toFixed(2)}
{isOverBudget && (
(over budget)
)}
)}
);
}
// ---------------------------------------------------------------------------
// Speed section
// ---------------------------------------------------------------------------
interface SpeedSectionProps {
medianLeadTimeHours: number | null | undefined;
}
function SpeedSection({ medianLeadTimeHours }: SpeedSectionProps) {
// Show 'No data yet' when null or undefined. Never render '0h'.
const hasData = medianLeadTimeHours != null;
return (
Speed
Median lead time
{hasData ? (
{medianLeadTimeHours.toFixed(1)}h median — target:
< 24h
) : (
No data yet
)}
);
}
// ---------------------------------------------------------------------------
// Objectives section — three charter objective cards, each showing a live
// metric against its target.
//
// ponytail: The charter `objectives` field is free text
// (Record[]) while the three metrics are hardcoded
// (first_pass_yield, median_lead_time_hours, escaped_defects). The mapping
// is positional-by-convention, not derived — one card per charter objective
// only holds until you edit the Goals tab. Stated assumption, not
// discovered later.
// ---------------------------------------------------------------------------
const OBJECTIVE_FALLBACK_LABELS = [
"Tasks shipped to merge with no human code edits",
"Median lead time, intake → merged",
"Critical escaped defects per release",
] as const;
interface ObjectivesSectionProps {
objectives: Record[];
firstPassYield: number | null | undefined;
medianLeadTimeHours: number | null | undefined;
escapedDefects: number | null | undefined;
}
function objectiveLabel(
objectives: Record[],
index: number,
): string {
const raw = objectives[index]?.metric;
return typeof raw === "string" && raw.length > 0
? raw
: OBJECTIVE_FALLBACK_LABELS[index];
}
interface ObjectiveCardProps {
label: string;
hasData: boolean;
formattedValue: string;
targetText: string;
}
function ObjectiveCard({
label,
hasData,
formattedValue,
targetText,
}: ObjectiveCardProps) {
return (
{label}
{hasData ? (
{formattedValue}
) : (
No data yet
)}
target: {targetText}
);
}
function ObjectivesSection({
objectives,
firstPassYield,
medianLeadTimeHours,
escapedDefects,
}: ObjectivesSectionProps) {
// Show 'No data yet' when a metric is null or undefined — never a fabricated
// value. Mirrors SpeedSection's hasData guard.
const fpyHasData = firstPassYield != null;
const ltHasData = medianLeadTimeHours != null;
const edHasData = escapedDefects != null;
return (
);
}
// ---------------------------------------------------------------------------
// Scorecard body — rendered when data is available
// ---------------------------------------------------------------------------
interface ScorecardBodyProps {
data: CockpitSummary;
spendTrend: UsageTimePoint[] | undefined;
spendTrendLoading: boolean;
}
function ScorecardBody({
data,
spendTrend,
spendTrendLoading,
}: ScorecardBodyProps) {
return (
Company Scorecard
Live performance against the charter
);
}
// ---------------------------------------------------------------------------
// Public export
// ---------------------------------------------------------------------------
export function CompanyScorecardCard() {
const { data, isLoading, isError, refetch } = useQuery({
queryKey: ["cockpit-summary"],
queryFn: cockpitApi.summary,
});
const { data: spendTrend, isLoading: spendTrendLoading } =
useUsageTimeSeries("30d");
if (isLoading) return ;
if (isError || !data) {
return (
void refetch()}
/>
);
}
return (
);
}