"use client"; import { useState } from "react"; import { trpc } from "~/lib/trpc/client.js"; // Stable country colors — deterministic from code const COUNTRY_COLORS: Record = { DE: "#3b82f6", ES: "#f59e0b", IN: "#10b981", PL: "#ef4444", PT: "#8b5cf6", RO: "#ec4899", CZ: "#06b6d4", HU: "#f97316", BG: "#14b8a6", US: "#6366f1", UK: "#a855f7", FR: "#84cc16", IT: "#e11d48", UNKNOWN: "#9ca3af", }; function getCountryColor(code: string): string { if (COUNTRY_COLORS[code]) return COUNTRY_COLORS[code]; // Deterministic fallback based on char codes let hash = 0; for (let i = 0; i < code.length; i++) { hash = code.charCodeAt(i) + ((hash << 5) - hash); } const hue = Math.abs(hash) % 360; return `hsl(${hue}, 65%, 50%)`; } function getSeverity(offshoreRatio: number, threshold: number): "green" | "yellow" | "red" { // Higher offshore = better (cost-efficient). Threshold is the MINIMUM target. if (offshoreRatio >= threshold) return "green"; // Target met if (offshoreRatio >= threshold - 10) return "yellow"; // Close to target return "red"; // Too little offshore } const SEVERITY_BADGE: Record = { green: "bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-300", yellow: "bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-300", red: "bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-300", }; const SEVERITY_DOT: Record = { green: "bg-green-500", yellow: "bg-yellow-500", red: "bg-red-500", }; // ─── Mini badge for list views ──────────────────────────────────────────────── export function ShoringBadge({ projectId }: { projectId: string }) { const { data, isLoading } = trpc.project.getShoringRatio.useQuery( { projectId }, { staleTime: 60_000 }, ); if (isLoading) { return ; } if (!data || data.totalHours === 0) { return --; } const severity = getSeverity(data.offshoreRatio, data.threshold); return ( {data.offshoreRatio}% ); } // ─── Full indicator for detail views ────────────────────────────────────────── export function ShoringIndicator({ projectId }: { projectId: string }) { const [tooltipOpen, setTooltipOpen] = useState(false); const { data, isLoading } = trpc.project.getShoringRatio.useQuery( { projectId }, { staleTime: 30_000 }, ); if (isLoading) { return (
); } if (!data || data.totalHours === 0) { return (

Nearshore Ratio

No assignments

); } const severity = getSeverity(data.offshoreRatio, data.threshold); // Build sorted country segments for the bar const segments = Object.entries(data.byCountry) .filter(([code]) => code !== "UNKNOWN") .sort((a, b) => b[1].pct - a[1].pct); if (data.byCountry["UNKNOWN"]) { segments.push(["UNKNOWN", data.byCountry["UNKNOWN"]]); } return (

Nearshore Ratio

{data.offshoreRatio}% offshore {severity === "green" ? " — Target met" : severity === "red" ? ` — Below ${data.threshold}% target` : ""}
{/* Stacked horizontal bar */}
setTooltipOpen(true)} onMouseLeave={() => setTooltipOpen(false)} >
{segments.map(([code, info]) => (
0 ? "2px" : "0", }} > {info.pct > 10 ? `${code} ${info.pct}%` : ""}
))}
{/* Tooltip overlay */} {tooltipOpen && (
Country Breakdown
{segments.map(([code, info]) => (
{code === "UNKNOWN" ? "Unknown" : code}
{info.pct}% ({info.resourceCount} {info.resourceCount === 1 ? "person" : "people"})
))}
{data.unknownCount > 0 && (
{data.unknownCount} resource{data.unknownCount !== 1 ? "s" : ""} without country
)}
)}
{/* Summary text */}
{data.onshoreRatio}% onshore ({data.onshoreCountryCode}) | {data.offshoreRatio}% offshore | Threshold: {data.threshold}%
); }