import { useState } from 'react';
import { useQuery } from '@tanstack/react-query';
import {
TrendingUp,
DollarSign,
Activity,
Target,
BarChart3,
ArrowUpRight,
ArrowDownRight,
Minus,
ArrowRightLeft,
CheckCircle2,
AlertCircle,
Loader2,
Zap,
Users,
TrendingDown,
} from 'lucide-react';
import { AppLayout } from '../../components/layout/AppLayout';
import { scaleOptimizationApi, type ScaleOverview, type ChannelPerformance, type MatrixResponse, type RecommendationsResponse, type RunRateData } from '../../api/analytics';
const formatCurrency = (val: number) => {
if (val >= 1_000_000) return `$${(val / 1_000_000).toFixed(1)}M`;
if (val >= 1_000) return `$${(val / 1_000).toFixed(1)}K`;
return `$${val.toFixed(2)}`;
};
const formatNumber = (val: number) => {
if (val >= 1_000_000) return `${(val / 1_000_000).toFixed(1)}M`;
if (val >= 1_000) return `${(val / 1_000).toFixed(1)}K`;
return val.toFixed(0);
};
const formatPct = (val: number) => `${(val * 100).toFixed(1)}%`;
// --- Stat Card ---
function StatCard({ title, value, subtitle, icon: Icon, color }: {
title: string;
value: string;
subtitle?: string;
icon: typeof TrendingUp;
color: string;
}) {
return (
<div className="rounded-xl border border-surface-200 bg-white p-4 sm:p-5">
<div className="flex items-start justify-between">
<p className="text-sm font-medium text-surface-500">{title}</p>
<div className={`rounded-lg p-2 ${color}`}>
<Icon className="h-4 w-4" />
</div>
</div>
<p className="mt-2 text-2xl font-bold text-surface-900">{value}</p>
{subtitle && <p className="mt-1 text-xs text-surface-500">{subtitle}</p>}
</div>
);
}
// --- Trend Icon ---
function TrendIcon({ trend }: { trend: 'up' | 'down' | 'flat' }) {
if (trend === 'up') return <ArrowUpRight className="h-4 w-4 text-green-600" />;
if (trend === 'down') return <ArrowDownRight className="h-4 w-4 text-red-500" />;
return <Minus className="h-4 w-4 text-surface-400" />;
}
// --- ROAS Color ---
function roasColor(roas: number) {
if (roas >= 4) return 'bg-green-100 text-green-800';
if (roas >= 2) return 'bg-yellow-100 text-yellow-800';
return 'bg-red-100 text-red-800';
}
function roasTextColor(roas: number) {
if (roas >= 4) return 'text-green-700';
if (roas >= 2) return 'text-yellow-700';
return 'text-red-700';
}
// --- Confidence Badge ---
function ConfidenceBadge({ confidence }: { confidence: 'high' | 'medium' | 'low' }) {
const colors = {
high: 'bg-green-100 text-green-800',
medium: 'bg-yellow-100 text-yellow-800',
low: 'bg-red-100 text-red-800',
};
return (
<span className={`inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium ${colors[confidence]}`}>
{confidence}
</span>
);
}
// --- Channel Performance Table ---
function ChannelTable({ channels }: { channels: ChannelPerformance[] }) {
const [sortKey, setSortKey] = useState<keyof ChannelPerformance>('roas');
const [sortDir, setSortDir] = useState<'asc' | 'desc'>('desc');
const sorted = [...channels].sort((a, b) => {
const aVal = a[sortKey];
const bVal = b[sortKey];
if (typeof aVal === 'number' && typeof bVal === 'number') {
return sortDir === 'asc' ? aVal - bVal : bVal - aVal;
}
return sortDir === 'asc'
? String(aVal).localeCompare(String(bVal))
: String(bVal).localeCompare(String(aVal));
});
const handleSort = (key: keyof ChannelPerformance) => {
if (sortKey === key) {
setSortDir(prev => prev === 'asc' ? 'desc' : 'asc');
} else {
setSortKey(key);
setSortDir('desc');
}
};
const SortIndicator = ({ col }: { col: keyof ChannelPerformance }) => {
if (sortKey !== col) return <span className="ml-1 opacity-30">↕</span>;
return <span className="ml-1">{sortDir === 'asc' ? '↑' : '↓'}</span>;
};
return (
<div className="overflow-x-auto rounded-xl border border-surface-200 bg-white">
<table className="w-full text-left text-sm">
<thead>
<tr className="border-b border-surface-200 bg-surface-50">
{[
{ key: 'channel' as keyof ChannelPerformance, label: 'Channel' },
{ key: 'spend' as keyof ChannelPerformance, label: 'Spend' },
{ key: 'clicks' as keyof ChannelPerformance, label: 'Clicks' },
{ key: 'ctr' as keyof ChannelPerformance, label: 'CTR' },
{ key: 'cpc' as keyof ChannelPerformance, label: 'CPC' },
{ key: 'conversions' as keyof ChannelPerformance, label: 'Conversions' },
{ key: 'attributed_revenue' as keyof ChannelPerformance, label: 'Revenue' },
{ key: 'roas' as keyof ChannelPerformance, label: 'ROAS' },
{ key: 'ltv_cac' as keyof ChannelPerformance, label: 'LTV:CAC' },
].map(col => (
<th
key={col.key}
className="cursor-pointer whitespace-nowrap px-4 py-3 font-semibold text-surface-700 hover:bg-surface-100"
onClick={() => handleSort(col.key)}
>
{col.label}<SortIndicator col={col.key} />
</th>
))}
<th className="whitespace-nowrap px-4 py-3 font-semibold text-surface-700">Trend</th>
</tr>
</thead>
<tbody>
{sorted.map((ch, i) => (
<tr key={ch.source_service} className={`border-b border-surface-100 last:border-0 ${i % 2 === 0 ? 'bg-white' : 'bg-surface-50/50'}`}>
<td className="whitespace-nowrap px-4 py-3 font-medium text-surface-900">{ch.channel}</td>
<td className="whitespace-nowrap px-4 py-3 text-surface-600">{formatCurrency(ch.spend)}</td>
<td className="whitespace-nowrap px-4 py-3 text-surface-600">{formatNumber(ch.clicks)}</td>
<td className="whitespace-nowrap px-4 py-3 text-surface-600">{formatPct(ch.ctr)}</td>
<td className="whitespace-nowrap px-4 py-3 text-surface-600">${ch.cpc.toFixed(2)}</td>
<td className="whitespace-nowrap px-4 py-3 text-surface-600">{formatNumber(ch.conversions)}</td>
<td className="whitespace-nowrap px-4 py-3 text-surface-600">{formatCurrency(ch.attributed_revenue)}</td>
<td className="whitespace-nowrap px-4 py-3">
<span className={`inline-flex items-center rounded-full px-2 py-0.5 text-xs font-semibold ${roasColor(ch.roas)}`}>
{ch.roas.toFixed(2)}x
</span>
</td>
<td className={`whitespace-nowrap px-4 py-3 font-medium ${roasTextColor(ch.ltv_cac)}`}>
{ch.ltv_cac.toFixed(2)}x
</td>
<td className="whitespace-nowrap px-4 py-3">
<TrendIcon trend={ch.trend} />
</td>
</tr>
))}
</tbody>
</table>
</div>
);
}
// --- Matrix Heatmap ---
function MatrixHeatmap({ matrixData }: { matrixData: MatrixResponse }) {
const { rows, channels, channel_names } = matrixData;
if (!rows.length || !channels.length) {
return (
<div className="flex h-40 items-center justify-center rounded-xl border border-dashed border-surface-300 bg-white text-surface-400">
No matrix data available
</div>
);
}
const cellColor = (roas: number) => {
if (roas >= 4) return 'bg-green-200 text-green-900';
if (roas >= 2) return 'bg-yellow-200 text-yellow-900';
if (roas >= 1) return 'bg-orange-100 text-orange-900';
return 'bg-red-100 text-red-900';
};
return (
<div className="overflow-x-auto rounded-xl border border-surface-200 bg-white">
<table className="w-full text-left text-sm">
<thead>
<tr className="border-b border-surface-200 bg-surface-50">
<th className="px-4 py-3 font-semibold text-surface-700">Dimension</th>
<th className="px-2 py-3 font-semibold text-surface-700">Type</th>
{channels.map(ch => (
<th key={ch} className="px-4 py-3 font-semibold text-surface-700">
{channel_names[ch] || ch.replace('_', ' ')}
</th>
))}
</tr>
</thead>
<tbody>
{rows.map((row, i) => (
<tr key={`${row.type}-${row.dimension}`} className={`border-b border-surface-100 last:border-0 ${i % 2 === 0 ? 'bg-white' : 'bg-surface-50/50'}`}>
<td className="whitespace-nowrap px-4 py-3 font-medium text-surface-900">{row.dimension}</td>
<td className="whitespace-nowrap px-2 py-3 text-xs text-surface-500 uppercase">{row.type}</td>
{channels.map(ch => {
const cell = row[ch] as { roas?: number; revenue?: number; spend?: number };
const roas = cell?.roas ?? 0;
return (
<td key={ch} className="whitespace-nowrap px-4 py-3 text-center">
<div className={`inline-flex flex-col items-center rounded-lg px-3 py-2 ${cellColor(roas)}`}>
<span className="text-xs font-bold">{roas.toFixed(2)}x</span>
<span className="text-[10px] opacity-70">{cell?.revenue ? formatCurrency(cell.revenue) : '$0'}</span>
</div>
</td>
);
})}
</tr>
))}
</tbody>
</table>
</div>
);
}
// --- Recommendations ---
function RecommendationsList({ data }: { data: RecommendationsResponse }) {
const applied = useState<string[]>(() => {
try {
const stored = localStorage.getItem('applied_recommendations');
return stored ? JSON.parse(stored) : [];
} catch {
return [];
}
})[0];
const handleApply = (id: string) => {
const updated = [...applied, id];
localStorage.setItem('applied_recommendations', JSON.stringify(updated));
};
if (!data.recommendations.length) {
return (
<div className="rounded-xl border border-dashed border-surface-300 bg-white p-8 text-center">
<Zap className="mx-auto h-8 w-8 text-surface-300" />
<p className="mt-2 text-sm font-medium text-surface-500">No recommendations yet</p>
<p className="text-xs text-surface-400">Add ad spend data to generate budget reallocation suggestions.</p>
</div>
);
}
return (
<div className="space-y-4">
{data.recommendations.map(rec => {
const isApplied = applied.includes(rec.id);
return (
<div key={rec.id} className={`rounded-xl border p-4 sm:p-5 transition-colors ${isApplied ? 'border-green-300 bg-green-50' : 'border-surface-200 bg-white'}`}>
<div className="flex items-start justify-between gap-4">
<div className="flex-1">
<div className="flex items-center gap-2">
<ArrowRightLeft className="h-4 w-4 text-brand-600" />
<h4 className="text-sm font-semibold text-surface-900">{rec.description}</h4>
<ConfidenceBadge confidence={rec.confidence} />
{isApplied && <CheckCircle2 className="h-4 w-4 text-green-600" />}
</div>
<p className="mt-1 text-xs text-surface-500">{rec.reason}</p>
<div className="mt-3 flex flex-wrap gap-4 text-xs">
<div>
<span className="text-surface-400">Current Revenue: </span>
<span className="font-medium text-surface-700">{formatCurrency(rec.current_revenue)}</span>
</div>
<div>
<span className="text-surface-400">Projected: </span>
<span className="font-medium text-green-700">{formatCurrency(rec.projected_revenue)}</span>
</div>
<div>
<span className="text-surface-400">Revenue Lift: </span>
<span className="font-semibold text-green-700">+{formatCurrency(rec.revenue_lift)} ({rec.revenue_lift_pct}%)</span>
</div>
<div className="flex gap-2">
<span className="text-red-600">
{rec.from_channel} ({rec.current_roas_from}x ROAS)
</span>
<ArrowRightRight className="h-3 w-3 text-surface-400" />
<span className="text-green-700">
{rec.to_channel} ({rec.current_roas_to}x ROAS)
</span>
</div>
</div>
</div>
<button
onClick={() => handleApply(rec.id)}
disabled={isApplied}
className={`shrink-0 min-h-[44px] rounded-lg px-4 py-2 text-sm font-medium transition-colors ${
isApplied
? 'cursor-not-allowed bg-green-200 text-green-800'
: 'bg-brand-600 text-white hover:bg-brand-700'
}`}
>
{isApplied ? 'Applied ✓' : 'Apply'}
</button>
</div>
</div>
);
})}
</div>
);
}
function ArrowRightRight(props: React.SVGProps<SVGSVGElement>) {
return (
<svg {...props} xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="m13 17 5-5-5-5" />
<path d="m13 12 5 5-5 5" />
<path d="M2 12h20" />
<path d="m2 12 5 5-5 5" />
<path d="m2 17 5-5-5-5" />
</svg>
);
}
// --- Main Page ---
export default function ScaleOptimizationPage() {
const { data: overview, isLoading: loadingOverview } = useQuery<ScaleOverview>({
queryKey: ['scale-overview'],
queryFn: scaleOptimizationApi.getOverview,
});
const { data: channelsData, isLoading: loadingChannels } = useQuery<{ channels: ChannelPerformance[] }>({
queryKey: ['scale-channels'],
queryFn: scaleOptimizationApi.getChannels,
});
const { data: matrixData, isLoading: loadingMatrix } = useQuery<MatrixResponse>({
queryKey: ['scale-matrix'],
queryFn: scaleOptimizationApi.getMatrix,
});
const { data: recommendationsData, isLoading: loadingRecs } = useQuery<RecommendationsResponse>({
queryKey: ['scale-recommendations'],
queryFn: scaleOptimizationApi.getRecommendations,
});
const { data: runRateData, isLoading: loadingRunRate } = useQuery<RunRateData>({
queryKey: ['scale-run-rate'],
queryFn: scaleOptimizationApi.getRunRate,
});
const isLoading = loadingOverview || loadingChannels || loadingMatrix || loadingRecs || loadingRunRate;
return (
<AppLayout title="Scale Optimization">
<div className="space-y-6">
{/* Header */}
<div>
<h1 className="text-xl sm:text-2xl font-bold text-surface-900">Scale Optimization</h1>
<p className="mt-1 text-sm text-surface-500">
ROAS, LTV:CAC by product × channel × market. AI-powered budget reallocation.
</p>
</div>
{isLoading && !overview && (
<div className="flex items-center justify-center py-12">
<Loader2 className="h-8 w-8 animate-spin text-brand-600" />
<span className="ml-3 text-sm text-surface-500">Loading analytics data...</span>
</div>
)}
{/* A) Overview Cards */}
{overview && (
<div className="grid grid-cols-2 gap-4 lg:grid-cols-3 xl:grid-cols-6">
<StatCard
title="Ad Spend (30d)"
value={formatCurrency(overview.total_spend_30d)}
subtitle={`90d: ${formatCurrency(overview.total_spend_90d)}`}
icon={DollarSign}
color="bg-blue-100 text-blue-700"
/>
<StatCard
title="ROAS"
value={`${overview.overall_roas.toFixed(2)}x`}
subtitle="Overall return on ad spend"
icon={TrendingUp}
color={overview.overall_roas >= 4 ? 'bg-green-100 text-green-700' : overview.overall_roas >= 2 ? 'bg-yellow-100 text-yellow-700' : 'bg-red-100 text-red-700'}
/>
<StatCard
title="LTV:CAC"
value={`${overview.avg_ltv_cac.toFixed(2)}x`}
subtitle="Lifetime value vs acquisition cost"
icon={Target}
color="bg-purple-100 text-purple-700"
/>
<StatCard
title="Conversions"
value={formatNumber(overview.total_conversions)}
subtitle="Last 30 days"
icon={Users}
color="bg-indigo-100 text-indigo-700"
/>
<StatCard
title="Attributed Revenue"
value={formatCurrency(overview.attributed_revenue)}
subtitle="Won deals (90d)"
icon={BarChart3}
color="bg-emerald-100 text-emerald-700"
/>
<StatCard
title="Active Campaigns"
value={String(overview.total_active_campaigns)}
subtitle={`Across ${Object.keys(overview.campaigns_by_channel).length} channels`}
icon={Zap}
color="bg-orange-100 text-orange-700"
/>
</div>
)}
{/* A) Run Rate vs Target */}
{runRateData && (
<div className="rounded-xl border border-surface-200 bg-white p-5 sm:p-6">
<div className="mb-4 flex items-center gap-2">
<TrendingUp className="h-5 w-5 text-surface-700" />
<h2 className="text-lg font-semibold text-surface-900">Run Rate vs Annual Target</h2>
{runRateData.pace && (
<span className={`ml-auto inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium ${
runRateData.pace === 'on_track'
? 'bg-green-100 text-green-800'
: 'bg-red-100 text-red-800'
}`}>
{runRateData.pace === 'on_track' ? '● On Track' : '● Behind Pace'}
</span>
)}
</div>
{runRateData.annual_target ? (
<div>
{/* Progress bar */}
<div className="mb-4">
<div className="mb-1 flex justify-between text-sm">
<span className="text-surface-500">YTD Revenue</span>
<span className="font-semibold text-surface-900">
{formatCurrency(runRateData.ytd_revenue)} / {formatCurrency(runRateData.annual_target)}
</span>
</div>
<div className="h-4 w-full overflow-hidden rounded-full bg-surface-100">
<div
className={`h-full rounded-full transition-all ${
runRateData.pace === 'on_track' ? 'bg-green-500' : 'bg-amber-500'
}`}
style={{
width: `${Math.min(
runRateData.annual_target > 0 ? (runRateData.ytd_revenue / runRateData.annual_target) * 100 : 0,
100
)}%`,
}}
/>
</div>
<div className="mt-1 flex justify-between text-xs text-surface-400">
<span>{runRateData.months_elapsed} mo elapsed</span>
<span>{runRateData.months_remaining} mo remaining</span>
</div>
</div>
{/* Stats grid */}
<div className="grid grid-cols-2 gap-4 sm:grid-cols-4">
<div>
<p className="text-xs text-surface-500">Annualized Run Rate</p>
<p className="mt-1 text-lg font-bold text-surface-900">
{runRateData.run_rate ? formatCurrency(runRateData.run_rate) : '—'}
</p>
</div>
<div>
<p className="text-xs text-surface-500">Pace</p>
<p className={`mt-1 text-lg font-bold ${
runRateData.pace === 'on_track' ? 'text-green-600' : 'text-amber-600'
}`}>
{runRateData.pace === 'on_track' ? 'On Track' : 'Behind'}
</p>
</div>
<div>
<p className="text-xs text-surface-500">Required Monthly</p>
<p className="mt-1 text-lg font-bold text-surface-900">
{runRateData.required_monthly ? formatCurrency(runRateData.required_monthly) : '—'}
</p>
</div>
<div>
<p className="text-xs text-surface-500">Remaining Needed</p>
<p className="mt-1 text-lg font-bold text-surface-900">
{runRateData.remaining_needed ? formatCurrency(runRateData.remaining_needed) : '—'}
</p>
</div>
</div>
</div>
) : (
<div className="rounded-lg bg-amber-50 p-4 text-sm text-amber-800">
<p className="font-medium">No annual target set</p>
<p className="mt-1">Set a target revenue in Company Settings to track your run rate.</p>
</div>
)}
</div>
)}
{/* B) Channel Performance Table */}
<div>
<div className="mb-3 flex items-center gap-2">
<BarChart3 className="h-5 w-5 text-surface-700" />
<h2 className="text-lg font-semibold text-surface-900">Channel Performance</h2>
</div>
{channelsData && channelsData.channels.length > 0 ? (
<ChannelTable channels={channelsData.channels} />
) : (
<div className="flex items-center justify-center rounded-xl border border-dashed border-surface-300 bg-white py-12 text-surface-400">
<div className="text-center">
<BarChart3 className="mx-auto h-8 w-8" />
<p className="mt-2 text-sm font-medium">No channel data</p>
<p className="text-xs">Connect Google Ads or Facebook Ads to see channel performance.</p>
</div>
</div>
)}
</div>
{/* C) Product × Channel Matrix */}
<div>
<div className="mb-3 flex items-center gap-2">
<Target className="h-5 w-5 text-surface-700" />
<h2 className="text-lg font-semibold text-surface-900">Product × Channel Matrix</h2>
</div>
{matrixData && !loadingMatrix ? (
<MatrixHeatmap matrixData={matrixData} />
) : (
<div className="flex items-center justify-center rounded-xl border border-dashed border-surface-300 bg-white py-12 text-surface-400">
<div className="text-center">
<Target className="mx-auto h-8 w-8" />
<p className="mt-2 text-sm font-medium">No matrix data</p>
<p className="text-xs">Product and market data from won deals will appear here.</p>
</div>
</div>
)}
</div>
{/* D) Budget Recommendations */}
<div>
<div className="mb-3 flex items-center gap-2">
<Zap className="h-5 w-5 text-brand-600" />
<h2 className="text-lg font-semibold text-surface-900">Budget Recommendations</h2>
</div>
{recommendationsData && !loadingRecs ? (
<RecommendationsList data={recommendationsData} />
) : (
<div className="flex items-center justify-center rounded-xl border border-dashed border-surface-300 bg-white py-12 text-surface-400">
<div className="text-center">
<Zap className="mx-auto h-8 w-8" />
<p className="mt-2 text-sm font-medium">No recommendations yet</p>
<p className="text-xs">AI generates budget reallocation suggestions from your ad data.</p>
</div>
</div>
)}
</div>
</div>
</AppLayout>
);
}