import React, { useState, useEffect } from 'react';
import { useQuery } from '@tanstack/react-query';
import { forecastApi } from '../../api/analytics';
import type { RevenueForecastResponse, PipelineForecastResponse, KPIsResponse, KPIMetric } from '../../api/analytics';
import { AppLayout } from '../../components/layout/AppLayout';
import { StatCard } from '../../components/ui/StatCard';
import { PageLoader } from '../../components/ui/LoadingSpinner';
import {
AreaChart,
Area,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
Legend,
ResponsiveContainer,
BarChart,
Bar,
ReferenceLine,
} from 'recharts';
import {
TrendingUp,
TrendingDown,
Minus,
DollarSign,
Activity,
Target,
AlertTriangle,
BarChart3,
Users,
Clock,
Handshake,
CalendarDays,
ArrowRight,
Gauge,
RefreshCw,
} from 'lucide-react';
// -- Helpers --
const currency = (value: number) =>
new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', maximumFractionDigits: 0 }).format(value);
const formatCompactCurrency = (value: number) => {
if (value >= 1_000_000) return `$${(value / 1_000_000).toFixed(1)}M`;
if (value >= 1_000) return `$${(value / 1_000).toFixed(1)}K`;
return `$${value.toFixed(0)}`;
};
const formatDate = (date: Date) =>
date.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' });
// -- LocalStorage helpers for revenue target --
const STORAGE_KEY_REVENUE_TARGET = 'cs-revenue-target-annual';
function getRevenueTarget(): number | null {
try {
const val = localStorage.getItem(STORAGE_KEY_REVENUE_TARGET);
if (val) {
const parsed = parseFloat(val);
if (!isNaN(parsed) && parsed > 0) return parsed;
}
} catch {
// ignore
}
return null;
}
function setRevenueTarget(target: number): void {
try {
localStorage.setItem(STORAGE_KEY_REVENUE_TARGET, String(target));
} catch {
// ignore
}
}
// -- Revenue Target Input Component --
interface RevenueTargetInputProps {
currentTarget: number | null;
onSet: (target: number) => void;
onClear: () => void;
}
function RevenueTargetInput({ currentTarget, onSet, onClear }: RevenueTargetInputProps) {
const [value, setValue] = useState('');
const [error, setError] = useState('');
const handleSet = () => {
const parsed = parseFloat(value.replace(/[^0-9.]/g, ''));
if (isNaN(parsed) || parsed <= 0) {
setError('Enter a valid target amount');
return;
}
if (parsed > 1_000_000_000) {
setError('Target seems unusually large — double-check your input');
return;
}
setError('');
onSet(parsed);
setValue('');
};
const handleClear = () => {
onClear();
};
return (
<div className="rounded-xl border border-surface-200 bg-white p-4 sm:p-5">
<div className="mb-3 flex items-center gap-2">
<Target className="h-5 w-5 text-brand-600" />
<h3 className="text-sm font-semibold text-surface-900">Annual Revenue Target</h3>
</div>
{currentTarget !== null ? (
<div className="flex flex-col sm:flex-row sm:items-center gap-3">
<div className="flex items-center gap-2 rounded-lg bg-green-50 px-3 py-2.5 min-h-[44px]">
<DollarSign className="h-4 w-4 text-green-600" />
<span className="text-lg font-bold text-green-700">{currency(currentTarget)}</span>
<span className="text-xs text-green-600">/ year</span>
</div>
<button
onClick={handleClear}
className="inline-flex items-center gap-1.5 self-start rounded-lg border border-red-200 bg-red-50 px-3 py-2 text-xs font-medium text-red-700 hover:bg-red-100 min-h-[44px]"
>
<RefreshCw className="h-3.5 w-3.5" />
Clear Target
</button>
</div>
) : null}
<div className="mt-3 flex flex-col sm:flex-row gap-2">
<div className="relative flex-1">
<span className="absolute inset-y-0 left-0 flex items-center pl-3 text-surface-400">$</span>
<input
type="text"
value={value}
onChange={(e) => { setValue(e.target.value); setError(''); }}
placeholder="e.g. 500000"
className="w-full rounded-lg border border-surface-200 bg-surface-50 py-2.5 pl-7 pr-3 text-sm text-surface-900 placeholder:text-surface-400 focus:border-brand-600 focus:outline-none focus:ring-1 focus:ring-brand-600 min-h-[44px]"
/>
</div>
<button
onClick={handleSet}
className="inline-flex items-center justify-center gap-1.5 rounded-lg bg-brand-600 px-5 py-2.5 text-sm font-medium text-white hover:bg-brand-700 min-h-[44px]"
>
<Target className="h-4 w-4" />
Set Target
</button>
</div>
{error && <p className="mt-2 text-xs text-red-600">{error}</p>}
</div>
);
}
// -- KPI Card Component --
interface KPICardProps {
kpi: KPIMetric;
}
function KPICard({ kpi }: KPICardProps) {
const isPositive = kpi.trend === 'up' || (kpi.invert_trend && kpi.trend === 'down');
const isNegative = kpi.trend === 'down' || (kpi.invert_trend && kpi.trend === 'up');
const isFlat = kpi.trend === 'flat';
const formatValue = (v: number) => {
if (kpi.unit === 'currency') return currency(v);
if (kpi.unit === 'percent') return `${v.toFixed(1)}%`;
return v.toLocaleString();
};
const TrendIcon = isFlat ? Minus : isPositive ? TrendingUp : TrendingDown;
const trendColor = isFlat
? 'text-surface-500'
: isPositive
? 'text-green-600'
: 'text-red-600';
return (
<div className="rounded-xl border border-surface-200 bg-white p-4 sm:p-5">
<p className="text-xs font-medium text-surface-500">{kpi.label}</p>
<div className="mt-2 flex items-end justify-between">
<p className="text-xl font-bold text-surface-900">{formatValue(kpi.value)}</p>
{!isFlat && (
<div className={`flex items-center gap-1 text-xs font-medium ${trendColor}`}>
<TrendIcon className="h-3.5 w-3.5" />
<span>{Math.abs(kpi.pct_change).toFixed(1)}%</span>
</div>
)}
</div>
{!isFlat && (
<p className="mt-1 text-xs text-surface-500">
vs prev period ({formatValue(kpi.previous_value)})
</p>
)}
</div>
);
}
// -- Health Score Gauge --
interface HealthGaugeProps {
score: number;
}
function HealthGauge({ score }: HealthGaugeProps) {
const radius = 54;
const circumference = 2 * Math.PI * radius;
const progress = Math.max(0, Math.min(score, 100));
const offset = circumference - (progress / 100) * circumference;
let color: string;
if (progress >= 70) color = '#059669'; // green
else if (progress >= 40) color = '#F59E0B'; // amber
else color = '#EF4444'; // red
return (
<div className="relative flex h-32 w-32 items-center justify-center">
<svg className="h-full w-full -rotate-90" viewBox="0 0 120 120">
<circle
cx="60"
cy="60"
r={radius}
fill="none"
stroke="#E5E7EB"
strokeWidth="8"
/>
<circle
cx="60"
cy="60"
r={radius}
fill="none"
stroke={color}
strokeWidth="8"
strokeLinecap="round"
strokeDasharray={circumference}
strokeDashoffset={offset}
className="transition-all duration-700 ease-out"
/>
</svg>
<div className="absolute flex flex-col items-center">
<span className="text-2xl font-bold text-surface-900">{Math.round(progress)}</span>
<span className="text-xs text-surface-500">/ 100</span>
</div>
</div>
);
}
// -- Main Page --
export function ForecastingPage() {
// Revenue forecast
const {
data: revenueData,
isLoading: revenueLoading,
} = useQuery<RevenueForecastResponse>({
queryKey: ['forecast-revenue'],
queryFn: () => forecastApi.getRevenueForecast(),
});
// Pipeline health
const {
data: pipelineData,
isLoading: pipelineLoading,
} = useQuery<PipelineForecastResponse>({
queryKey: ['forecast-pipeline'],
queryFn: () => forecastApi.getPipelineForecast(),
});
// KPIs
const {
data: kpisData,
isLoading: kpisLoading,
} = useQuery<KPIsResponse>({
queryKey: ['analytics-kpis', 90],
queryFn: () => forecastApi.getKPIs(90),
});
// Revenue target (localStorage)
const [revenueTarget, setRevenueTargetState] = useState<number | null>(getRevenueTarget);
const handleSetTarget = (target: number) => {
setRevenueTargetState(target);
setRevenueTarget(target);
};
const handleClearTarget = () => {
setRevenueTargetState(null);
try { localStorage.removeItem(STORAGE_KEY_REVENUE_TARGET); } catch { /* ignore */ }
};
// Required pace calculation
const forecast = revenueData?.forecast;
const avgMonthlyActual = forecast?.avg_monthly_actual ?? 0;
const totalWonRevenue = (forecast?.trailing_actuals ?? []).reduce((sum, m) => sum + m.actual, 0);
const currentMonth = new Date().getMonth() + 1; // 1-indexed
const remainingMonths = Math.max(12 - currentMonth + 1, 1); // include current month
const monthsElapsed = currentMonth - 1;
let requiredMonthlyPace = 0;
let projectedYearEnd = 0;
let isOnTrack = false;
if (revenueTarget) {
requiredMonthlyPace = (revenueTarget - totalWonRevenue) / remainingMonths;
projectedYearEnd = totalWonRevenue + avgMonthlyActual * remainingMonths;
isOnTrack = avgMonthlyActual >= requiredMonthlyPace && requiredMonthlyPace > 0;
}
const allLoading = revenueLoading || pipelineLoading || kpisLoading;
if (allLoading) {
return (
<AppLayout title="Revenue Forecasting">
<PageLoader />
</AppLayout>
);
}
const pipelineHealth = pipelineData?.pipeline_health;
const atRiskDeals = pipelineData?.at_risk_deals ?? [];
const stageDistribution = pipelineData?.stage_distribution ?? [];
const kpis = kpisData?.kpis ?? [];
// -- Prepare chart data for revenue forecast --
const allMonths = [
...(forecast?.trailing_actuals ?? []),
...(forecast?.forecast_months ?? []),
];
const chartData = allMonths.map((m) => ({
...m,
isActual: m.actual > 0,
}));
// -- Custom tooltip for revenue chart --
const RevenueTooltip = ({ active, payload }: any) => {
if (!active || !payload || !payload.length) return null;
const data = payload[0].payload;
return (
<div className="rounded-lg border border-surface-200 bg-white p-3 shadow-lg">
<p className="text-sm font-semibold text-surface-900">{data.label}</p>
{data.actual > 0 && (
<p className="text-xs text-surface-600">Actual: {currency(data.actual)}</p>
)}
<p className="text-xs text-green-600">Likely: {currency(data.likely)}</p>
<p className="text-xs text-surface-400">Best: {currency(data.best)}</p>
<p className="text-xs text-surface-400">Conservative: {currency(data.conservative)}</p>
</div>
);
};
// -- Custom tooltip for stage chart --
const StageTooltip = ({ active, payload }: any) => {
if (!active || !payload || !payload.length) return null;
const data = payload[0].payload;
return (
<div className="rounded-lg border border-surface-200 bg-white p-3 shadow-lg">
<p className="text-sm font-semibold text-surface-900">{data.stage}</p>
<p className="text-xs text-surface-600">{data.count} deals</p>
<p className="text-xs text-surface-600">Value: {currency(data.value)}</p>
</div>
);
};
// -- Filter KPIs for display --
const revenueKpis = kpis.filter((k) => k.category === 'revenue');
const dealsKpis = kpis.filter((k) => k.category === 'deals');
const efficiencyKpis = kpis.filter((k) => k.category === 'efficiency');
const customerKpis = kpis.filter((k) => k.category === 'customers');
// Top 6 KPIs for the summary grid
const summaryKpis = [
kpis.find((k) => k.label === 'Win Rate'),
kpis.find((k) => k.label === 'Avg Deal Size'),
kpis.find((k) => k.label === 'Avg Days to Close'),
kpis.find((k) => k.label === 'New Contacts'),
kpis.find((k) => k.label === 'Total Revenue'),
kpis.find((k) => k.label === 'Deals Created/Month'),
].filter(Boolean) as KPIMetric[];
return (
<AppLayout title="Revenue Forecasting">
{/* Header */}
<div className="mb-6 flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3">
<div>
<h1 className="text-xl sm:text-2xl font-bold text-surface-900">Revenue Forecasting</h1>
<p className="mt-1 text-sm text-surface-500">
Pipeline analysis, revenue projections, and key performance metrics
</p>
</div>
<div className="flex items-center gap-2 rounded-lg border border-surface-200 bg-white px-3 py-2 min-h-[44px]">
<CalendarDays className="h-4 w-4 text-surface-400" />
<span className="text-sm text-surface-600">
Last 90 days • Updated {formatDate(new Date())}
</span>
</div>
</div>
{/* ========== Section 0: Revenue Target ========== */}
<div className="mb-8">
<RevenueTargetInput
currentTarget={revenueTarget}
onSet={handleSetTarget}
onClear={handleClearTarget}
/>
</div>
{/* ========== Section 0b: Pace Status ========== */}
{revenueTarget && requiredMonthlyPace > 0 && (
<div className="mb-8">
<div className={`rounded-xl border p-4 sm:p-6 ${
isOnTrack
? 'border-green-200 bg-gradient-to-r from-green-50 to-emerald-50'
: 'border-red-200 bg-gradient-to-r from-red-50 to-orange-50'
}`}>
<div className="flex flex-col sm:flex-row sm:items-center gap-4">
{/* Status indicator */}
<div className="flex items-center gap-3">
<div className={`flex h-12 w-12 shrink-0 items-center justify-center rounded-full text-xl ${
isOnTrack ? 'bg-green-100' : 'bg-red-100'
}`}>
{isOnTrack ? '🟢' : '🔴'}
</div>
<div>
<h3 className={`text-base font-bold ${
isOnTrack ? 'text-green-800' : 'text-red-800'
}`}>
{isOnTrack ? 'On Track' : 'Behind Pace'}
</h3>
<p className={`text-xs ${
isOnTrack ? 'text-green-600' : 'text-red-600'
}`}>
Based on your {currency(revenueTarget)} annual target
</p>
</div>
</div>
{/* Stats */}
<div className="flex flex-1 flex-wrap items-center gap-4 sm:gap-6">
<div className="flex items-center gap-2">
<span className="text-xs text-surface-500">Avg Pace</span>
<span className="text-sm font-bold text-surface-900">{currency(avgMonthlyActual)}/mo</span>
</div>
<ArrowRight className="h-4 w-4 shrink-0 text-surface-300" />
<div className="flex items-center gap-2">
<span className="text-xs text-surface-500">Required</span>
<span className={`text-sm font-bold ${
isOnTrack ? 'text-green-700' : 'text-red-700'
}`}>{currency(requiredMonthlyPace)}/mo</span>
</div>
<div className="hidden sm:block h-6 w-px bg-surface-200" />
<div className="flex items-center gap-2">
<CalendarDays className="h-4 w-4 text-surface-400" />
<span className="text-xs text-surface-500">
Projected YE: <span className="font-bold text-surface-900">{currency(projectedYearEnd)}</span>
</span>
</div>
</div>
</div>
{/* Detail row */}
<div className="mt-3 flex flex-wrap items-center gap-x-6 gap-y-1 pt-3 border-t border-surface-200/50">
<p className="text-xs text-surface-500">
Won so far: <span className="font-medium text-surface-700">{currency(totalWonRevenue)}</span> (last 3 months)
</p>
<p className="text-xs text-surface-500">
Remaining: <span className="font-medium text-surface-700">{remainingMonths} months</span>
</p>
<p className="text-xs text-surface-500">
Gap to close: <span className={`font-medium ${
(revenueTarget - projectedYearEnd) >= 0 ? 'text-green-600' : 'text-red-600'
}`}>{currency(Math.max(0, revenueTarget - projectedYearEnd))}</span>
</p>
</div>
</div>
</div>
)}
{/* ========== Section 1: Revenue Forecast ========== */}
<div className="mb-8">
<div className="mb-4 flex items-center gap-2">
<TrendingUp className="h-5 w-5 text-brand-600" />
<h2 className="text-lg font-semibold text-surface-900">Revenue Forecast</h2>
</div>
{/* Stat cards */}
<div className="mb-6 grid grid-cols-2 gap-4 lg:grid-cols-4">
<StatCard
title="Pipeline Value"
value={formatCompactCurrency(forecast?.total_pipeline_value ?? 0)}
icon={DollarSign}
iconBg="bg-brand-100 text-brand-600"
/>
<StatCard
title="Weighted Pipeline"
value={formatCompactCurrency(forecast?.total_weighted_value ?? 0)}
icon={Activity}
iconBg="bg-blue-100 text-blue-600"
/>
<StatCard
title="Avg Monthly Revenue"
value={formatCompactCurrency(forecast?.avg_monthly_actual ?? 0)}
icon={Target}
iconBg="bg-purple-100 text-purple-600"
/>
<div className="rounded-xl border border-surface-200 bg-white p-4 sm:p-5">
<div className="flex items-center gap-2">
<div className="flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-surface-100 text-surface-600">
<Gauge className="h-4 w-4" />
</div>
<div className="min-w-0">
<p className="text-xs text-surface-500">Confidence</p>
<p className="text-lg font-bold text-surface-900">
{forecast?.confidence_score ?? 0}%
</p>
</div>
</div>
</div>
</div>
{/* Revenue forecast chart */}
<div className="rounded-xl border border-surface-200 bg-white p-4 sm:p-5">
<div className="mb-4 flex items-center gap-2">
<BarChart3 className="h-4 w-4 text-brand-600" />
<h3 className="text-sm font-semibold text-surface-700">
6-Month Revenue Projection
</h3>
</div>
{chartData.length === 0 ? (
<div className="flex h-64 items-center justify-center">
<p className="text-sm text-surface-500">No forecast data available</p>
</div>
) : (
<ResponsiveContainer width="100%" height={320}>
<AreaChart data={chartData} margin={{ top: 10, right: 10, left: 0, bottom: 0 }}>
<defs>
<linearGradient id="colorLikely" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="#00A846" stopOpacity={0.3} />
<stop offset="95%" stopColor="#00A846" stopOpacity={0.05} />
</linearGradient>
<linearGradient id="colorBest" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="#9CA3AF" stopOpacity={0.15} />
<stop offset="95%" stopColor="#9CA3AF" stopOpacity={0.02} />
</linearGradient>
<linearGradient id="colorConservative" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="#D1D5DB" stopOpacity={0.1} />
<stop offset="95%" stopColor="#D1D5DB" stopOpacity={0.02} />
</linearGradient>
</defs>
<CartesianGrid strokeDasharray="3 3" stroke="#F3F4F6" />
<XAxis
dataKey="label"
tick={{ fontSize: 12, fill: '#6B7280' }}
axisLine={{ stroke: '#E5E7EB' }}
tickLine={false}
/>
<YAxis
tickFormatter={(v: number) => formatCompactCurrency(v)}
tick={{ fontSize: 12, fill: '#6B7280' }}
axisLine={false}
tickLine={false}
/>
<Tooltip content={<RevenueTooltip />} />
<Legend
wrapperStyle={{ fontSize: '12px', paddingTop: '8px' }}
formatter={(value: string) => value.charAt(0).toUpperCase() + value.slice(1)}
/>
{/* Required pace reference line */}
{revenueTarget && requiredMonthlyPace > 0 && (
<ReferenceLine
y={requiredMonthlyPace}
stroke={isOnTrack ? '#059669' : '#EF4444'}
strokeWidth={2}
strokeDasharray="8 4"
label={{
value: `Required pace: ${formatCompactCurrency(requiredMonthlyPace)}/mo`,
position: 'right',
fill: isOnTrack ? '#059669' : '#EF4444',
fontSize: 11,
fontWeight: 600,
className: 'text-shadow',
}}
/>
)}
<Area
type="monotone"
dataKey="best"
name="Best Case"
stroke="#9CA3AF"
fill="url(#colorBest)"
strokeWidth={1.5}
dot={false}
/>
<Area
type="monotone"
dataKey="likely"
name="Likely"
stroke="#00A846"
fill="url(#colorLikely)"
strokeWidth={2}
dot={false}
/>
<Area
type="monotone"
dataKey="conservative"
name="Conservative"
stroke="#D1D5DB"
fill="url(#colorConservative)"
strokeWidth={1.5}
dot={false}
/>
{chartData.map((d, i) =>
d.actual > 0 ? (
<Area
key={`actual-${i}`}
type="monotone"
dataKey="actual"
name="Actual"
stroke="#00A846"
fill="none"
strokeWidth={2.5}
strokeDasharray="6 3"
connectNulls
dot={
d.isActual
? { r: 4, fill: '#00A846', stroke: '#fff', strokeWidth: 2 }
: false
}
/>
) : null
)}
</AreaChart>
</ResponsiveContainer>
)}
{/* Legend note */}
<div className="mt-3 flex flex-wrap items-center gap-4 text-xs text-surface-500">
<div className="flex items-center gap-1.5">
<div className="h-2.5 w-2.5 rounded-full bg-brand-600" />
<span>Actual (trailing 3 months)</span>
</div>
<div className="flex items-center gap-1.5">
<div className="h-2.5 w-2.5 rounded-full bg-brand-600 opacity-70" />
<span>Likely (weighted by probability)</span>
</div>
<div className="flex items-center gap-1.5">
<div className="h-2.5 w-2.5 rounded-full bg-surface-400" />
<span>Best (100% of all deals)</span>
</div>
<div className="flex items-center gap-1.5">
<div className="h-2.5 w-2.5 rounded-full bg-surface-300" />
<span>Conservative (50% of weighted)</span>
</div>
{revenueTarget && requiredMonthlyPace > 0 && (
<div className="flex items-center gap-1.5">
<div className={`h-0.5 w-3 ${isOnTrack ? 'bg-green-600' : 'bg-red-600'}`} style={{ borderTop: '2px dashed', borderColor: isOnTrack ? '#059669' : '#EF4444', background: 'none' }} />
<span>Required pace ({formatCompactCurrency(requiredMonthlyPace)}/mo)</span>
</div>
)}
</div>
</div>
</div>
{/* ========== Section 2: Pipeline Health ========== */}
<div className="mb-8">
<div className="mb-4 flex items-center gap-2">
<Gauge className="h-5 w-5 text-brand-600" />
<h2 className="text-lg font-semibold text-surface-900">Pipeline Health</h2>
</div>
<div className="grid grid-cols-1 gap-6 lg:grid-cols-3">
{/* Health Score + Coverage */}
<div className="rounded-xl border border-surface-200 bg-white p-4 sm:p-5">
<h3 className="mb-4 text-sm font-semibold text-surface-700">Health Score</h3>
<div className="flex justify-center">
<HealthGauge score={pipelineHealth?.health_score ?? 0} />
</div>
<div className="mt-4 space-y-2">
<div className="flex items-center justify-between text-xs">
<span className="text-surface-500">Coverage Ratio</span>
<span className="font-medium text-surface-700">
{pipelineHealth?.coverage_ratio
? `${pipelineHealth.coverage_ratio.toFixed(1)}x`
: 'N/A'}
</span>
</div>
<div className="flex items-center justify-between text-xs">
<span className="text-surface-500">Growth Rate</span>
<span className={`font-medium ${
(pipelineHealth?.growth_rate_pct ?? 0) >= 0
? 'text-green-600'
: 'text-red-600'
}`}>
{(pipelineHealth?.growth_rate_pct ?? 0) >= 0 ? '+' : ''}
{(pipelineHealth?.growth_rate_pct ?? 0).toFixed(1)}%
</span>
</div>
<div className="flex items-center justify-between text-xs">
<span className="text-surface-500">Open Deals</span>
<span className="font-medium text-surface-700">
{pipelineHealth?.open_deals_count ?? 0}
</span>
</div>
</div>
</div>
{/* Stage Distribution Chart */}
<div className="lg:col-span-2 rounded-xl border border-surface-200 bg-white p-4 sm:p-5">
<h3 className="mb-4 text-sm font-semibold text-surface-700">Stage Distribution</h3>
{stageDistribution.length === 0 ? (
<div className="flex h-48 items-center justify-center">
<p className="text-sm text-surface-500">No pipeline data available</p>
</div>
) : (
<ResponsiveContainer width="100%" height={200}>
<BarChart
data={stageDistribution}
layout="vertical"
margin={{ top: 5, right: 20, left: 100, bottom: 5 }}
>
<CartesianGrid strokeDasharray="3 3" stroke="#F3F4F6" />
<XAxis
type="number"
tickFormatter={(v: number) => formatCompactCurrency(v)}
tick={{ fontSize: 11, fill: '#6B7280' }}
axisLine={false}
tickLine={false}
/>
<YAxis
type="category"
dataKey="stage"
tick={{ fontSize: 11, fill: '#6B7280' }}
axisLine={false}
tickLine={false}
width={90}
/>
<Tooltip content={<StageTooltip />} />
<Bar
dataKey="value"
name="Value"
fill="#00A846"
radius={[0, 4, 4, 0]}
barSize={20}
/>
</BarChart>
</ResponsiveContainer>
)}
</div>
</div>
{/* At-risk deals */}
{atRiskDeals.length > 0 && (
<div className="mt-6 rounded-xl border border-red-200 bg-red-50 p-4 sm:p-5">
<div className="mb-3 flex items-center gap-2">
<AlertTriangle className="h-4 w-4 text-red-600" />
<h3 className="text-sm font-semibold text-red-900">
At-Risk Deals ({atRiskDeals.length})
</h3>
</div>
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-red-200">
<th className="pb-2 text-left text-xs font-medium text-red-800">Deal</th>
<th className="pb-2 text-left text-xs font-medium text-red-800">Stage</th>
<th className="pb-2 text-right text-xs font-medium text-red-800">Amount</th>
<th className="pb-2 text-right text-xs font-medium text-red-800">Due Date</th>
<th className="pb-2 text-right text-xs font-medium text-red-800">Overdue</th>
</tr>
</thead>
<tbody>
{atRiskDeals.slice(0, 5).map((deal) => (
<tr key={deal.deal_name} className="border-b border-red-100 last:border-b-0">
<td className="py-2 font-medium text-red-900 truncate max-w-[200px]">
{deal.deal_name}
</td>
<td className="py-2 text-red-700">{deal.stage}</td>
<td className="py-2 text-right text-red-900">{currency(deal.amount)}</td>
<td className="py-2 text-right text-red-700">{deal.expected_close_date ?? 'N/A'}</td>
<td className="py-2 text-right font-medium text-red-700">
{deal.days_overdue} days
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
)}
</div>
{/* ========== Section 3: KPI Dashboard ========== */}
<div>
<div className="mb-4 flex items-center gap-2">
<Target className="h-5 w-5 text-brand-600" />
<h2 className="text-lg font-semibold text-surface-900">Key Performance Indicators</h2>
</div>
{summaryKpis.length === 0 ? (
<div className="rounded-xl border border-surface-200 bg-white p-8 text-center">
<Activity className="mx-auto h-8 w-8 text-surface-300" />
<p className="mt-2 text-sm text-surface-500">No KPI data available yet</p>
<p className="text-xs text-surface-400">KPIs will appear once CRM data is synced</p>
</div>
) : (
<div className="grid grid-cols-2 gap-4 lg:grid-cols-3">
{summaryKpis.map((kpi) => (
<KPICard key={kpi.label} kpi={kpi} />
))}
</div>
)}
{/* Detailed KPI breakdowns */}
{kpis.length > 0 && (
<div className="mt-6 grid grid-cols-1 gap-6 lg:grid-cols-2">
{/* Revenue & Deals */}
<div className="rounded-xl border border-surface-200 bg-white p-4 sm:p-5">
<h3 className="mb-3 text-sm font-semibold text-surface-700 flex items-center gap-2">
<Handshake className="h-4 w-4 text-brand-600" />
Revenue & Deal Metrics
</h3>
<div className="space-y-3">
{[...revenueKpis, ...dealsKpis].map((kpi) => (
<div
key={kpi.label}
className="flex items-center justify-between rounded-lg bg-surface-50 p-3"
>
<span className="text-sm text-surface-600">{kpi.label}</span>
<div className="flex items-center gap-2">
<span className="text-sm font-semibold text-surface-900">
{kpi.unit === 'currency'
? currency(kpi.value)
: kpi.unit === 'percent'
? `${kpi.value.toFixed(1)}%`
: kpi.value.toLocaleString()}
</span>
{kpi.trend !== 'flat' && (
<span className={`inline-flex items-center gap-0.5 rounded-full px-2 py-0.5 text-xs font-medium ${
kpi.trend === 'up'
? 'bg-green-100 text-green-700'
: 'bg-red-100 text-red-700'
}`}>
{kpi.trend === 'up' ? '↑' : '↓'}
{Math.abs(kpi.pct_change).toFixed(1)}%
</span>
)}
</div>
</div>
))}
</div>
</div>
{/* Efficiency & Customers */}
<div className="rounded-xl border border-surface-200 bg-white p-4 sm:p-5">
<h3 className="mb-3 text-sm font-semibold text-surface-700 flex items-center gap-2">
<Users className="h-4 w-4 text-blue-600" />
Efficiency & Customer Metrics
</h3>
<div className="space-y-3">
{[...efficiencyKpis, ...customerKpis].map((kpi) => (
<div
key={kpi.label}
className="flex items-center justify-between rounded-lg bg-surface-50 p-3"
>
<span className="text-sm text-surface-600">{kpi.label}</span>
<div className="flex items-center gap-2">
<span className="text-sm font-semibold text-surface-900">
{kpi.unit === 'currency'
? currency(kpi.value)
: kpi.unit === 'percent'
? `${kpi.value.toFixed(1)}%`
: kpi.value.toLocaleString()}
</span>
{kpi.trend !== 'flat' && (
<span className={`inline-flex items-center gap-0.5 rounded-full px-2 py-0.5 text-xs font-medium ${
(kpi.invert_trend && kpi.trend === 'down') || kpi.trend === 'up'
? 'bg-green-100 text-green-700'
: 'bg-red-100 text-red-700'
}`}>
{kpi.trend === 'up' ? '↑' : '↓'}
{Math.abs(kpi.pct_change).toFixed(1)}%
</span>
)}
</div>
</div>
))}
</div>
</div>
</div>
)}
</div>
</AppLayout>
);
}