import type {
StrategicOverview,
WaterfallData,
RootCauseTree,
ContributionAnalysis,
RootCauseNode,
WaterfallStep,
TrendPoint,
} from '../../api/analytics';
import { strategicApi } from '../../api/analytics';
import { Bar, BarChart, Cell, Legend, Line, LineChart, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recharts';
import { AlertCircle, ArrowDown, ArrowUp, BarChart3, ChevronDown, ChevronRight, FileText, Layers, TrendingDown, TrendingUp } from 'lucide-react';
import { useQuery } from '@tanstack/react-query';
import { useState } from 'react';
import { AppLayout } from '../../components/layout/AppLayout';
const BRAND = '#00A846';
const GREEN = '#22C55E';
const RED = '#EF4444';
function formatCurrency(value: number): string {
const abs = Math.abs(value);
if (abs >= 1_000_000) {
return `${value >= 0 ? '+' : '-'}$${(abs / 1_000_000).toFixed(1)}M`;
}
if (abs >= 1_000) {
return `${value >= 0 ? '+' : '-'}$${(abs / 1_000).toFixed(1)}K`;
}
return `${value >= 0 ? '+' : ''}$${abs.toFixed(0)}`;
}
function formatFullCurrency(value: number): string {
return '$' + value.toLocaleString(undefined, { minimumFractionDigits: 0, maximumFractionDigits: 0 });
}
/* ── Trend Chart ─────────────────────────────────────────────────────── */
function TrendChart({ data }: { data: TrendPoint[] }) {
return (
<ResponsiveContainer width="100%" height={280}>
<LineChart data={data}>
<XAxis dataKey="label" tick={{ fontSize: 12 }} />
<YAxis tick={{ fontSize: 12 }} tickFormatter={(v: number) => `$${v >= 1_000 ? (v / 1_000).toFixed(0) + 'K' : v}`} />
<Tooltip
formatter={(value: number) => ['$' + value.toLocaleString(), '']}
contentStyle={{ borderRadius: 8, fontSize: 13 }}
/>
<Legend wrapperStyle={{ fontSize: 13 }} />
<Line type="monotone" dataKey="actual" name="Actual" stroke={BRAND} strokeWidth={2} dot={{ r: 4 }} />
<Line type="monotone" dataKey="budget" name="Budget" stroke="#94A3B8" strokeWidth={2} strokeDasharray="6 4" dot={{ r: 3 }} />
</LineChart>
</ResponsiveContainer>
);
}
/* ── Waterfall Chart ─────────────────────────────────────────────────── */
function WaterfallChart({ steps }: { steps: WaterfallStep[] }) {
const maxVal = Math.max(...steps.map((s) => Math.abs(s.cumulative)), 1);
// Build chart data: each bar has a baseline and height
const chartData = steps.map((step, i) => {
const isStart = i === 0;
const isEnd = i === steps.length - 1;
if (isStart || isEnd) {
// Full bars: budget and actual
return {
label: step.label,
baseline: 0,
value: step.value,
color: step.color,
type: 'total',
};
}
// Intermediate bars: floating
const prevCumulative = steps[i - 1].cumulative;
const val = step.value;
const baseline = val >= 0 ? prevCumulative : prevCumulative + val;
return {
label: step.label,
baseline,
value: Math.abs(val),
color: step.color,
type: 'delta',
};
});
return (
<ResponsiveContainer width="100%" height={320}>
<BarChart data={chartData} barGap={2}>
<XAxis dataKey="label" tick={{ fontSize: 11 }} interval={0} angle={-30} textAnchor="end" height={60} />
<YAxis
tick={{ fontSize: 12 }}
domain={[-maxVal * 0.1, maxVal * 1.1]}
tickFormatter={(v: number) => `$${Math.abs(v) >= 1_000 ? (Math.abs(v) / 1_000).toFixed(0) + 'K' : Math.abs(v)}`}
/>
<Tooltip
formatter={(value: number, name: string) => {
if (name === 'value') return ['$' + value.toLocaleString(), 'Amount'];
return [value, name];
}}
contentStyle={{ borderRadius: 8, fontSize: 13 }}
/>
<Bar dataKey="value" stackId="waterfall">
{chartData.map((entry, index) => (
<Cell key={`cell-${index}`} fill={entry.color} />
))}
</Bar>
</BarChart>
</ResponsiveContainer>
);
}
/* ── Root Cause Tree ─────────────────────────────────────────────────── */
function TreeNode({ node, depth = 0, totalVariance }: { node: RootCauseNode; depth?: number; totalVariance: number }) {
const [expanded, setExpanded] = useState(depth < 1);
const hasChildren = node.children.length > 0;
const pct = totalVariance !== 0 ? Math.abs((node.value / totalVariance) * 100) : 0;
const isPositive = node.value >= 0;
return (
<div className="ml-4">
<div
className={`flex items-center gap-2 min-h-[44px] px-2 rounded-lg cursor-pointer transition-colors hover:bg-gray-50`}
style={{ paddingLeft: `${depth * 16 + 8}px` }}
onClick={() => hasChildren && setExpanded((e) => !e)}
role="button"
tabIndex={0}
onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); hasChildren && setExpanded((v) => !v); } }}
>
{hasChildren ? (
expanded ? <ChevronDown className="w-4 h-4 text-gray-400 flex-shrink-0" /> : <ChevronRight className="w-4 h-4 text-gray-400 flex-shrink-0" />
) : (
<div className="w-4 flex-shrink-0" />
)}
<span className={`text-sm font-medium flex-1`}>
{node.label}
</span>
<span className={`text-sm font-semibold ${isPositive ? 'text-green-600' : 'text-red-600'}`}>
{formatCurrency(node.value)}
</span>
<span className="text-xs text-gray-400 w-12 text-right">
{pct.toFixed(1)}%
</span>
</div>
{expanded && hasChildren && (
<div className="ml-2 border-l-2 border-gray-100 pl-2">
{node.children.map((child, i) => (
<TreeNode key={`${child.label}-${i}`} node={child} depth={depth + 1} totalVariance={totalVariance} />
))}
</div>
)}
{expanded && node.details && (
<div className="ml-8 mt-1 mb-2 p-2 bg-gray-50 rounded-md">
{Object.entries(node.details).map(([key, val]) => (
<div key={key} className="text-xs text-gray-500">
<span className="font-medium text-gray-600">
{key.replace(/_/g, ' ')}:
</span>{' '}
{typeof val === 'number' ? (val >= 0 && key.includes('rate') ? `${(val * 100).toFixed(1)}%` : val.toLocaleString()) : String(val)}
</div>
))}
</div>
)}
</div>
);
}
/* ── Contribution Card ───────────────────────────────────────────────── */
function ContributionCard({ factor, index }: { factor: { label: string; value: number; direction: string; description: string; recommendation: string; percentage: number }; index: number }) {
const isPositive = factor.value >= 0;
const isNeutral = factor.direction === 'neutral';
const colors = [BRAND, '#3B82F6', '#F59E0B', '#8B5CF6'];
const color = colors[index % colors.length];
return (
<div className="bg-white rounded-xl border border-gray-100 p-4 flex flex-col gap-3 hover:shadow-sm transition-shadow">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<div className="w-3 h-3 rounded-full" style={{ backgroundColor: color }} />
<span className="text-sm font-semibold text-gray-800">{factor.label}</span>
</div>
{isNeutral ? (
<AlertCircle className="w-4 h-4 text-gray-400" />
) : isPositive ? (
<TrendingUp className="w-4 h-4 text-green-500" />
) : (
<TrendingDown className="w-4 h-4 text-red-500" />
)}
</div>
<div className={`text-2xl font-bold ${isPositive ? 'text-green-600' : isNeutral ? 'text-gray-400' : 'text-red-600'}`}>
{formatCurrency(factor.value)}
</div>
<div className="text-xs text-gray-500">
{factor.percentage.toFixed(1)}% of total • {factor.description}
</div>
<div className="text-xs text-gray-400 border-t pt-2 mt-auto">
💡 {factor.recommendation}
</div>
</div>
);
}
/* ── Page ────────────────────────────────────────────────────────────── */
function StrategicIntelligenceContent() {
const { data: overview, isLoading: loadingOverview } = useQuery({
queryKey: ['strategic-overview'],
queryFn: strategicApi.getOverview,
staleTime: 60_000,
});
const { data: waterfall, isLoading: loadingWaterfall } = useQuery({
queryKey: ['strategic-waterfall'],
queryFn: strategicApi.getWaterfall,
staleTime: 60_000,
});
const { data: rootCause, isLoading: loadingRootCause } = useQuery({
queryKey: ['strategic-root-cause'],
queryFn: strategicApi.getRootCause,
staleTime: 60_000,
});
const { data: contribution, isLoading: loadingContribution } = useQuery({
queryKey: ['strategic-contribution'],
queryFn: strategicApi.getContribution,
staleTime: 60_000,
});
const isLoading = loadingOverview || loadingWaterfall || loadingRootCause || loadingContribution;
if (isLoading) {
return (
<div className="flex items-center justify-center h-96">
<div className="animate-spin rounded-full h-8 w-8 border-2 border-gray-200 border-t-brand-600" />
</div>
);
}
const variancePositive = overview?.variance >= 0;
return (
<div className="space-y-6">
{/* Header */}
<div>
<h1 className="text-xl sm:text-2xl font-bold text-gray-900">Strategic Intelligence</h1>
<p className="text-sm text-gray-500 mt-1">
FP&A-grade waterfall analysis and drillable root-cause trees. See every dollar explained.
{overview?.period_label && ` — ${overview.period_label}`}
</p>
</div>
{/* ── Variance Summary Card ───────────────────────────────────── */}
<div className="bg-white rounded-xl border border-gray-100 p-6">
<div className="grid grid-cols-2 sm:grid-cols-4 gap-4">
<div>
<div className="text-xs text-gray-400 uppercase tracking-wider">Budget</div>
<div className="text-2xl font-bold text-gray-800 mt-1">
{overview?.budget ? formatFullCurrency(overview.budget) : '$0'}
</div>
</div>
<div>
<div className="text-xs text-gray-400 uppercase tracking-wider">Actual</div>
<div className="text-2xl font-bold text-gray-800 mt-1">
{overview?.actual ? formatFullCurrency(overview.actual) : '$0'}
</div>
</div>
<div>
<div className="text-xs text-gray-400 uppercase tracking-wider">Forecast</div>
<div className="text-2xl font-bold text-gray-800 mt-1">
{overview?.forecast ? formatFullCurrency(overview.forecast) : '$0'}
</div>
</div>
<div>
<div className="text-xs text-gray-400 uppercase tracking-wider">Variance</div>
<div className={`text-2xl font-bold mt-1 flex items-center gap-1 ${variancePositive ? 'text-green-600' : 'text-red-600'}`}>
{variancePositive ? <ArrowUp className="w-5 h-5" /> : <ArrowDown className="w-5 h-5" />}
{formatCurrency(overview?.variance ?? 0)}
</div>
<div className={`text-xs mt-1 ${variancePositive ? 'text-green-500' : 'text-red-500'}`}>
{formatCurrency(overview?.variance_pct ?? 0)}
</div>
</div>
</div>
</div>
{/* ── Revenue Waterfall Chart ─────────────────────────────────── */}
<div className="bg-white rounded-xl border border-gray-100 p-6">
<div className="flex items-center gap-2 mb-4">
<BarChart3 className="w-5 h-5 text-brand-600" />
<h2 className="text-lg font-semibold text-gray-800">Revenue Waterfall</h2>
</div>
{waterfall?.waterfall && waterfall.waterfall.length > 0 ? (
<WaterfallChart steps={waterfall.waterfall} />
) : (
<div className="text-center text-gray-400 py-12">
No waterfall data available. Add CRM deals to see the revenue breakdown.
</div>
)}
{waterfall?.market_mix && waterfall.market_mix.length > 0 && (
<div className="mt-4 pt-4 border-t border-gray-100">
<h3 className="text-sm font-medium text-gray-600 mb-2">Revenue by Source</h3>
<div className="flex flex-wrap gap-2">
{waterfall.market_mix.map((mm) => (
<span key={mm.source} className="inline-flex items-center gap-1 px-2 py-1 bg-gray-50 rounded-md text-xs text-gray-600">
{mm.source}: <span className="font-semibold">{formatFullCurrency(mm.value)}</span>
</span>
))}
</div>
</div>
)}
</div>
{/* ── Contribution Analysis ───────────────────────────────────── */}
<div className="bg-white rounded-xl border border-gray-100 p-6">
<div className="flex items-center gap-2 mb-4">
<Layers className="w-5 h-5 text-brand-600" />
<h2 className="text-lg font-semibold text-gray-800">Contribution Analysis</h2>
</div>
{contribution?.factors && contribution.factors.length > 0 ? (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
{contribution.factors.map((factor, i) => (
<ContributionCard key={factor.name} factor={factor} index={i} />
))}
</div>
) : (
<div className="text-center text-gray-400 py-8">
No contribution data available.
</div>
)}
{contribution?.source_breakdown && contribution.source_breakdown.length > 0 && (
<div className="mt-4 pt-4 border-t border-gray-100">
<h3 className="text-sm font-medium text-gray-600 mb-2">Source Breakdown</h3>
<div className="overflow-x-auto">
<table className="w-full text-xs text-gray-600">
<thead>
<tr className="border-b">
<th className="text-left py-1 px-2">Source</th>
<th className="text-right py-1 px-2">Current</th>
<th className="text-right py-1 px-2">Previous</th>
<th className="text-right py-1 px-2">Change</th>
</tr>
</thead>
<tbody>
{contribution.source_breakdown.map((sb) => (
<tr key={sb.source} className="border-b border-gray-50">
<td className="py-1 px-2">{sb.source}</td>
<td className="text-right py-1 px-2">{formatFullCurrency(sb.current)}</td>
<td className="text-right py-1 px-2">{formatFullCurrency(sb.previous)}</td>
<td className={`text-right py-1 px-2 font-semibold ${sb.change >= 0 ? 'text-green-600' : 'text-red-600'}`}>
{formatCurrency(sb.change)}
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
)}
</div>
{/* ── Root Cause Tree ─────────────────────────────────────────── */}
<div className="bg-white rounded-xl border border-gray-100 p-6">
<div className="flex items-center gap-2 mb-4">
<FileText className="w-5 h-5 text-brand-600" />
<h2 className="text-lg font-semibold text-gray-800">Root Cause Drill-Down</h2>
</div>
{rootCause?.root && rootCause.root.children.length > 0 ? (
<div className="border rounded-lg p-3">
{/* Root node */}
<div className="flex items-center gap-2 px-2 py-1 bg-gray-50 rounded-md mb-2">
<span className="text-sm font-bold text-gray-700">{rootCause.root.label}</span>
<span className={`text-sm font-bold ${rootCause.root.value >= 0 ? 'text-green-600' : 'text-red-600'}`}>
{formatCurrency(rootCause.root.value)}
</span>
<span className="text-xs text-gray-400">
Budget: {formatFullCurrency(rootCause.root.budget)} → Actual: {formatFullCurrency(rootCause.root.actual)}
</span>
</div>
{rootCause.root.children.map((child, i) => (
<TreeNode key={`${child.label}-${i}`} node={child} depth={0} totalVariance={rootCause.root.value} />
))}
</div>
) : (
<div className="text-center text-gray-400 py-8">
No root cause data available. Add CRM deals to see variance drill-down.
</div>
)}
</div>
{/* ── Trend Chart ─────────────────────────────────────────────── */}
<div className="bg-white rounded-xl border border-gray-100 p-6">
<div className="flex items-center gap-2 mb-4">
<TrendingUp className="w-5 h-5 text-brand-600" />
<h2 className="text-lg font-semibold text-gray-800">6-Month Trend</h2>
</div>
{overview?.trend && overview.trend.length > 0 ? (
<TrendChart data={overview.trend} />
) : (
<div className="text-center text-gray-400 py-12">
No trend data available yet.
</div>
)}
</div>
</div>
);
}
export default function StrategicIntelligencePage() {
return (
<AppLayout>
<StrategicIntelligenceContent />
</AppLayout>
);
}