"use client" import { GitBranch } from 'lucide-react' import type { PipelineDonutData } from '@/lib/dashboard/types' import { EmptyState } from './empty-state' import { Skeleton } from './skeleton' interface PipelineDonutProps { data: PipelineDonutData | null loading: boolean } export function PipelineDonut({ data, loading }: PipelineDonutProps) { return ( Pipeline Value Open deals by stage {loading || !data ? ( ) : data.stages.length === 0 ? ( ) : ( <> {data.stages.map((s) => ( {s.name} {s.dealCount} deal{s.dealCount === 1 ? '' : 's'} {formatCurrencyShort(s.totalValue)} ))} > )} ) } // ------------------------------------------------------------ // SVG ring. 200×200 viewBox, 12px ring width. We draw one // per stage using an SVG arc from startAngle → endAngle. Gaps // between segments are implied by a thin slate-900 stroke between // them for a cleaner look. // ------------------------------------------------------------ function Donut({ data }: { data: PipelineDonutData }) { const size = 200 const r = 80 const ringWidth = 18 const cx = size / 2 const cy = size / 2 // Small slices would render as slivers that disappear into stroke // rounding. We give each stage a floor share purely for rendering, // but keep the labels/legend honest with the actual totals. const totalRaw = data.totalValue || 1 const minFrac = 0.02 const rawShares = data.stages.map((s) => s.totalValue / totalRaw) const floored = rawShares.map((x) => Math.max(x, minFrac)) const floorSum = floored.reduce((a, b) => a + b, 0) const shares = floored.map((x) => x / floorSum) // Build a cumulative-offset array, then map stages → arc paths. Using // a pre-computed offsets array avoids the Next 16 React Compiler's // "Cannot reassign variable after render completes" rule. const offsets: number[] = [0] for (let i = 0; i < shares.length; i++) offsets.push(offsets[i] + shares[i]) const segments = data.stages.map((s, i) => { const start = offsets[i] * Math.PI * 2 - Math.PI / 2 const end = offsets[i + 1] * Math.PI * 2 - Math.PI / 2 return { path: arcPath(cx, cy, r, start, end), color: s.color, id: s.id } }) return ( {/* background ring */} {segments.map((seg) => ( ))} {/* center label */} Total {formatCurrencyShort(data.totalValue)} ) } function arcPath(cx: number, cy: number, r: number, startRad: number, endRad: number): string { const x1 = cx + r * Math.cos(startRad) const y1 = cy + r * Math.sin(startRad) const x2 = cx + r * Math.cos(endRad) const y2 = cy + r * Math.sin(endRad) const largeArc = endRad - startRad > Math.PI ? 1 : 0 return `M ${x1} ${y1} A ${r} ${r} 0 ${largeArc} 1 ${x2} ${y2}` } function formatCurrencyShort(v: number): string { if (v >= 1_000_000) return `$${(v / 1_000_000).toFixed(1)}M` if (v >= 1_000) return `$${(v / 1_000).toFixed(1)}k` return `$${v.toFixed(0)}` }
Open deals by stage