Files
CapaKraken/apps/web/src/components/dashboard/widgets/StatCardsWidget.tsx
T
Hartmut 3d117708ff fix: stat card sub-text on separate line below main number
The sub-text (e.g. "36 active", "30 total") was rendering inline
next to the large number, creating confusing "36₃₆ active" appearance.
Fixed by wrapping the number in a block <div> and the sub-text in a
block <p>, ensuring they stack vertically.

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-03-22 22:05:55 +01:00

138 lines
4.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import { trpc } from "~/lib/trpc/client.js";
import type { WidgetProps } from "~/components/dashboard/widget-registry.js";
import { formatMoney } from "~/lib/format.js";
import { InfoTooltip } from "~/components/ui/InfoTooltip.js";
import { AnimatedNumber } from "~/components/ui/AnimatedNumber.js";
import { ProgressRing } from "~/components/ui/ProgressRing.js";
import { FadeIn } from "~/components/ui/FadeIn.js";
const ACCENT_COLORS = {
green: "var(--color-green-500, #22c55e)",
amber: "var(--color-amber-500, #f59e0b)",
red: "var(--color-red-500, #ef4444)",
} as const;
function StatCard({
label,
value,
suffix,
sub,
info,
accentColor,
delay = 0,
ring,
}: {
label: string;
value: number;
suffix?: string;
sub?: string;
info?: React.ReactNode;
accentColor?: "green" | "amber" | "red";
delay?: number;
ring?: { value: number; color: string };
}) {
const accentBorder = accentColor === "red"
? "border-l-red-500"
: accentColor === "amber"
? "border-l-amber-500"
: accentColor === "green"
? "border-l-green-500"
: "";
return (
<FadeIn delay={delay} direction="up">
<div
className={`rounded-2xl border border-gray-200 bg-white/80 p-4 shadow-sm dark:border-gray-700 dark:bg-gray-900/70 hover-lift cursor-default ${
accentColor ? `border-l-[3px] ${accentBorder}` : ""
}`}
>
<span className="flex items-center text-xs font-semibold uppercase tracking-[0.18em] text-gray-500">
{label}
{info && <InfoTooltip content={info} />}
</span>
{ring ? (
<div className="mt-2 flex items-center gap-3">
<ProgressRing value={ring.value} size={56} strokeWidth={4} color={ring.color}>
<AnimatedNumber value={value} suffix={suffix} className="text-lg font-semibold text-gray-900 dark:text-gray-50" />
</ProgressRing>
</div>
) : (
<div className="mt-2">
<span className="text-2xl font-semibold text-gray-900 dark:text-gray-50">
<AnimatedNumber value={value} suffix={suffix} />
</span>
</div>
)}
{sub && <p className="mt-0.5 text-xs text-gray-400 dark:text-gray-500">{sub}</p>}
</div>
</FadeIn>
);
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
export function StatCardsWidget(_props: Partial<WidgetProps> = {}) {
const { data, isLoading } = trpc.dashboard.getOverview.useQuery(undefined, {
staleTime: 60_000,
placeholderData: (prev) => prev,
});
if (isLoading || !data) {
return (
<div className="grid grid-cols-2 gap-3 h-full">
{[...Array(4)].map((_, i) => (
<div
key={i}
className="rounded-2xl border border-gray-200 bg-gray-100 p-4 dark:border-gray-700 dark:bg-gray-800"
>
<div className="h-3 w-20 shimmer-skeleton rounded" />
<div className="h-7 w-16 bg-gray-300 dark:bg-gray-600 rounded" />
<div className="h-2 w-24 shimmer-skeleton rounded" />
</div>
))}
</div>
);
}
const budgetPct = data.budgetSummary.avgUtilizationPercent;
const budgetAccent: "red" | "amber" | "green" =
budgetPct > 90 ? "red" : budgetPct >= 70 ? "amber" : "green";
return (
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4 h-full content-start">
<StatCard
label="Total Resources"
value={data.totalResources}
sub={`${data.activeResources} active`}
info="All resources in the system. Sub-line shows active resources only."
delay={0}
/>
<StatCard
label="Active Projects"
value={data.activeProjects}
sub={`${data.totalProjects} total`}
info="Projects with status ACTIVE. Total includes all statuses (DRAFT, ON_HOLD, COMPLETED, CANCELLED)."
delay={0.05}
/>
<StatCard
label="Total Allocations"
value={data.totalAllocations}
sub={`${data.activeAllocations} not cancelled`}
info="All allocation records ever created. 'Not cancelled' excludes allocations with status CANCELLED."
delay={0.1}
/>
<StatCard
label="Budget Utilization"
value={budgetPct}
suffix="%"
sub={`${formatMoney(data.budgetSummary.totalCostCents)} of ${formatMoney(data.budgetSummary.totalBudgetCents)}`}
info="Sum of costs across non-cancelled allocations divided by total project budgets. Cost = resource LCR × booked hours."
accentColor={budgetAccent}
delay={0.15}
ring={{ value: budgetPct, color: ACCENT_COLORS[budgetAccent] }}
/>
</div>
);
}