diff --git a/src/app/(dashboard)/pipelines/page.tsx b/src/app/(dashboard)/pipelines/page.tsx index 4f30901..80d7ee8 100644 --- a/src/app/(dashboard)/pipelines/page.tsx +++ b/src/app/(dashboard)/pipelines/page.tsx @@ -380,6 +380,7 @@ export default function PipelinesPage() { ) : ( <> + - )} diff --git a/src/components/pipelines/deal-card.tsx b/src/components/pipelines/deal-card.tsx index d1d23d4..5c523c4 100644 --- a/src/components/pipelines/deal-card.tsx +++ b/src/components/pipelines/deal-card.tsx @@ -47,10 +47,10 @@ export function DealCard({ deal, stage, onEdit, isOverlay }: DealCardProps) { e.stopPropagation(); onEdit(deal); }} - className={`group relative w-full cursor-pointer rounded-xl bg-white pl-4 pr-3 py-3 text-left shadow-sm transition-all ${ + className={`group relative w-full cursor-pointer rounded-xl border border-slate-700/50 bg-slate-800/70 pl-4 pr-3 py-3 text-left shadow-sm transition-all ${ isOverlay ? "shadow-xl" - : "hover:-translate-y-0.5 hover:shadow-md" + : "hover:-translate-y-0.5 hover:border-slate-600 hover:bg-slate-800 hover:shadow-lg" }`} > {/* 4px left accent bar using stage color */} @@ -61,17 +61,17 @@ export function DealCard({ deal, stage, onEdit, isOverlay }: DealCardProps) { />
-

+

{deal.title}

{deal.status === "won" && ( - + Won )} {deal.status === "lost" && ( - + Lost @@ -80,18 +80,18 @@ export function DealCard({ deal, stage, onEdit, isOverlay }: DealCardProps) { {/* Contact row */}
- + {initials(deal.contact?.name, deal.contact?.phone)} - {contactLabel} + {contactLabel}
- + {formatCurrency(deal.value, deal.currency)} {deal.expected_close_date && ( - + {formatDate(deal.expected_close_date)} @@ -102,7 +102,7 @@ export function DealCard({ deal, stage, onEdit, isOverlay }: DealCardProps) {
{initials(assigneeLabel)} diff --git a/src/components/pipelines/pipeline-analytics.tsx b/src/components/pipelines/pipeline-analytics.tsx index 00e2f94..769ce2f 100644 --- a/src/components/pipelines/pipeline-analytics.tsx +++ b/src/components/pipelines/pipeline-analytics.tsx @@ -2,7 +2,21 @@ import { useMemo } from "react"; import type { Deal, PipelineStage } from "@/types"; -import { DollarSign, TrendingUp, Target, BarChart3, Trophy, XCircle } from "lucide-react"; +import { + DollarSign, + TrendingUp, + Target, + BarChart3, + Trophy, + XCircle, + Info, +} from "lucide-react"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip"; interface PipelineAnalyticsProps { stages: PipelineStage[]; @@ -20,9 +34,8 @@ function formatCurrency(value: number) { /** * Weighted pipeline value: value × per-stage probability. - * Spec rule: first stage = 10%, last stage before Won = 90%, Won = 100%. - * Stages between are interpolated linearly. - * Won deals count at 100%; Lost deals are excluded. + * First stage ≈ 10%, stages interpolate up to 90% before the final stage, + * final stage (Won) = 100%. Lost deals excluded. */ function computeStageProbability( stage: PipelineStage, @@ -32,11 +45,10 @@ function computeStageProbability( 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; // final stage (Won) - // First stage → 0.10, second-to-last → 0.90. Evenly spread. - const slots = n - 1; // number of non-final slots + if (index === n - 1) return 1; + const slots = n - 1; if (slots <= 1) return 0.1; - const t = index / (slots - 1); // 0..1 + const t = index / (slots - 1); return 0.1 + t * (0.9 - 0.1); } @@ -62,7 +74,6 @@ export function PipelineAnalytics({ stages, deals }: PipelineAnalyticsProps) { return sum + Number(d.value || 0) * prob; }, 0); - // This-month won/lost counts based on updated_at (falls back to created_at). const now = new Date(); const monthStart = new Date(now.getFullYear(), now.getMonth(), 1); const thisMonth = (d: Deal) => { @@ -87,38 +98,46 @@ export function PipelineAnalytics({ stages, deals }: PipelineAnalyticsProps) { }, [deals, sortedStages]); return ( -
- } - label="Total Deals" - value={String(stats.totalCount)} - /> - } - label="Pipeline Value" - value={formatCurrency(stats.totalValue)} - /> - } - label="Avg Deal Size" - value={formatCurrency(stats.avgValue)} - /> - } - label="Weighted Value" - value={formatCurrency(stats.weightedValue)} - /> - } - label="Won This Month" - value={String(stats.wonThisMonth)} - /> - } - label="Lost This Month" - value={String(stats.lostThisMonth)} - /> -
+ +
+ } + 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)} + 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)} + tooltip="Pipeline Value divided by Total Deals — the average value of a single non-lost deal." + /> + } + label="Weighted Value" + value={formatCurrency(stats.weightedValue)} + 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." + /> +
+
); } @@ -126,16 +145,34 @@ function Metric({ icon, label, value, + tooltip, }: { icon: React.ReactNode; label: string; value: string; + tooltip: string; }) { return (
{icon} - {label} + {label} + + + } + > + + + + {tooltip} + +

{value}

diff --git a/src/components/pipelines/pipeline-board.tsx b/src/components/pipelines/pipeline-board.tsx index 27b3b72..dee50ca 100644 --- a/src/components/pipelines/pipeline-board.tsx +++ b/src/components/pipelines/pipeline-board.tsx @@ -172,35 +172,32 @@ function StageColumn({ const { setNodeRef, isOver } = useDroppable({ id: stage.id }); return ( -
- {/* 3px colored top border (spec) — sits above the column's padding */} +
+ {/* 3px colored top border — sits above the column's padding */}
-

+

{stage.name}

- + {deals.length}
-

{formatCurrency(totalValue)}

+

{formatCurrency(totalValue)}

{deals.length === 0 ? ( -
+
Drop a deal here
) : ( @@ -219,7 +216,7 @@ function StageColumn({ variant="ghost" size="sm" onClick={() => onAddDeal(stage.id)} - className="mt-3 w-full justify-start border border-dashed border-gray-300 bg-transparent text-gray-500 hover:border-gray-400 hover:bg-white hover:text-gray-700" + className="mt-3 w-full justify-start border border-dashed border-slate-700 bg-transparent text-slate-400 hover:border-slate-600 hover:bg-slate-800 hover:text-white" > Add Deal