"use client"; import { useMemo } from "react"; import type { Deal, PipelineStage } from "@/types"; import { DollarSign, TrendingUp, Target, BarChart3, Trophy, XCircle, Info, } from "lucide-react"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from "@/components/ui/tooltip"; import { useAuth } from "@/hooks/use-auth"; import { formatCurrency } from "@/lib/currency"; interface PipelineAnalyticsProps { stages: PipelineStage[]; deals: Deal[]; } /** * Weighted pipeline value: value × per-stage probability. * First stage ≈ 10%, stages interpolate up to 90% before the final stage, * final stage (Won) = 100%. Lost deals excluded. */ function computeStageProbability( stage: PipelineStage, sortedStages: PipelineStage[], ): number { const n = sortedStages.length; if (n <= 1) return 1; const index = sortedStages.findIndex((s) => s.id === stage.id); if (index < 0) return 0; if (index === n - 1) return 1; const slots = n - 1; if (slots <= 1) return 0.1; const t = index / (slots - 1); return 0.1 + t * (0.9 - 0.1); } export function PipelineAnalytics({ stages, deals }: PipelineAnalyticsProps) { const { defaultCurrency } = useAuth(); const sortedStages = useMemo( () => [...stages].sort((a, b) => a.position - b.position), [stages], ); const stats = useMemo(() => { const active = deals.filter((d) => d.status !== "lost"); const openDeals = active.filter((d) => d.status !== "won"); const totalCount = active.length; const totalValue = active.reduce((sum, d) => sum + Number(d.value || 0), 0); const avgValue = totalCount > 0 ? totalValue / totalCount : 0; const stageById = new Map(sortedStages.map((s) => [s.id, s])); const weightedValue = openDeals.reduce((sum, d) => { const stage = stageById.get(d.stage_id); if (!stage) return sum; const prob = computeStageProbability(stage, sortedStages); return sum + Number(d.value || 0) * prob; }, 0); const now = new Date(); const monthStart = new Date(now.getFullYear(), now.getMonth(), 1); const thisMonth = (d: Deal) => { const ts = d.updated_at ?? d.created_at; return ts ? new Date(ts) >= monthStart : false; }; const wonThisMonth = deals.filter( (d) => d.status === "won" && thisMonth(d), ).length; const lostThisMonth = deals.filter( (d) => d.status === "lost" && thisMonth(d), ).length; return { totalCount, totalValue, avgValue, weightedValue, wonThisMonth, lostThisMonth, }; }, [deals, sortedStages]); return (
} label="Total Deals" value={String(stats.totalCount)} tooltip="Count of every deal in this pipeline that isn't marked as Lost. Won deals are still included." /> } label="Pipeline Value" value={formatCurrency(stats.totalValue, defaultCurrency)} tooltip="Sum of the dollar values of all deals in this pipeline, excluding deals marked as Lost." /> } label="Avg Deal Size" value={formatCurrency(stats.avgValue, defaultCurrency)} tooltip="Pipeline Value divided by Total Deals — the average value of a single non-lost deal." /> } label="Weighted Value" value={formatCurrency(stats.weightedValue, defaultCurrency)} tooltip="Expected revenue: each open deal's value × its stage probability. First stage ≈ 10%, stages progress up to 90%, Won = 100%. Lost deals are excluded." /> } label="Won This Month" value={String(stats.wonThisMonth)} tooltip="Deals marked as Won since the first day of the current month." /> } label="Lost This Month" value={String(stats.lostThisMonth)} tooltip="Deals marked as Lost since the first day of the current month." />
); } function Metric({ icon, label, value, tooltip, }: { icon: React.ReactNode; label: string; value: string; tooltip: string; }) { return (
{icon} {label} } > {tooltip}

{value}

); }