feat: Sprint 2 — data storytelling and visual richness

Timeline project color system:
- 16-color deterministic palette (same project = same color always)
- Resource panel: allocation blocks colored by project instead of uniform green
- Project panel: colored left border + dot on project headers
- ProjectColorLegend: floating strip showing color-to-project mapping
- Utilization intensity tint: subtle background gradient on resource rows

Table visual enhancements:
- Resources: inline 3px utilization bar below chargeability percentage
- Resources: 32px avatar circles with initials + role-derived colors
- Projects: animated budget bars, styled resource count badges
- Allocations: 3px left border colored by status (green/amber/blue/gray/red)

KPI progress rings:
- Budget utilization: ProgressRing wrapping AnimatedNumber on dashboard
- Chargeability report: ring on average chargeability summary card
- Resource detail: rings on chargeability target + actual metrics
- Vacation balance: ring showing remaining days with color thresholds
- Demand widget: mini rings on FTE fill rate per project
- Resource detail: FadeIn on SkillRadarChart

Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
2026-03-19 00:58:06 +01:00
parent ae92923c28
commit a97597093f
13 changed files with 399 additions and 53 deletions
@@ -5,8 +5,15 @@ 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,
@@ -15,6 +22,7 @@ function StatCard({
info,
accentColor,
delay = 0,
ring,
}: {
label: string;
value: number;
@@ -23,6 +31,7 @@ function StatCard({
info?: React.ReactNode;
accentColor?: "green" | "amber" | "red";
delay?: number;
ring?: { value: number; color: string };
}) {
const accentBorder = accentColor === "red"
? "border-l-red-500"
@@ -43,9 +52,17 @@ function StatCard({
{label}
{info && <InfoTooltip content={info} />}
</span>
<span className="mt-2 text-2xl font-semibold text-gray-900 dark:text-gray-50">
<AnimatedNumber value={value} suffix={suffix} />
</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>
) : (
<span className="mt-2 text-2xl font-semibold text-gray-900 dark:text-gray-50">
<AnimatedNumber value={value} suffix={suffix} />
</span>
)}
{sub && <span className="mt-1 text-xs text-gray-500 dark:text-gray-400">{sub}</span>}
</div>
</FadeIn>
@@ -111,6 +128,7 @@ export function StatCardsWidget(_props: Partial<WidgetProps> = {}) {
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>
);