import React from 'react';
import { useQuery } from '@tanstack/react-query';
import { Link } from 'react-router-dom';
import { analyticsApi } from '../../api/analytics';
import type { CrmSummary, PipelineStage, ContactsAnalytics, DealsVelocity, ForecastData } from '../../api/analytics';
import { AppLayout } from '../../components/layout/AppLayout';
import { StatCard } from '../../components/ui/StatCard';
import { PageLoader } from '../../components/ui/LoadingSpinner';
import {
Building2,
Users,
Handshake,
DollarSign,
TrendingUp,
Clock,
Target,
BarChart3,
ArrowRight,
Activity,
} from 'lucide-react';
const currency = (value: number) =>
new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', maximumFractionDigits: 0 }).format(value);
const percent = (value: number) =>
`${value.toFixed(1)}%`;
// Pipeline bar width calculation helper
const maxStages = (stages: PipelineStage[]): number => {
const maxCount = Math.max(...stages.map((s) => s.count), 1);
const maxValue = Math.max(...stages.map((s) => s.value), 1);
return { maxCount, maxValue };
};
export function CrmAnalyticsPage() {
// CRM Summary
const {
data: summary,
isLoading: summaryLoading,
} = useQuery<CrmSummary>({
queryKey: ['analytics-crm-summary'],
queryFn: () => analyticsApi.getCrmSummary(),
});
// Pipeline stages
const {
data: pipeline,
isLoading: pipelineLoading,
} = useQuery<{ stages: PipelineStage[] }>({
queryKey: ['analytics-pipeline'],
queryFn: () => analyticsApi.getPipeline(),
});
// Contacts analytics
const {
data: contacts,
isLoading: contactsLoading,
} = useQuery<ContactsAnalytics>({
queryKey: ['analytics-contacts'],
queryFn: () => analyticsApi.getContacts(),
});
// Deals velocity
const {
data: velocity,
isLoading: velocityLoading,
} = useQuery<DealsVelocity>({
queryKey: ['analytics-deals-velocity'],
queryFn: () => analyticsApi.getDealsVelocity(),
});
// Forecasts
const {
data: forecast,
isLoading: forecastLoading,
} = useQuery<ForecastData>({
queryKey: ['analytics-forecasts'],
queryFn: () => analyticsApi.getForecasts(),
});
const allLoading = summaryLoading || pipelineLoading || contactsLoading || velocityLoading || forecastLoading;
if (allLoading) {
return (
<AppLayout title="CRM Analytics">
<PageLoader />
</AppLayout>
);
}
const stages = pipeline?.stages ?? [];
const { maxCount, maxValue } = maxStages(stages);
// Stage colors for visual differentiation
const stageColors: Record<string, string> = {
lead: 'bg-surface-400',
prospecting: 'bg-surface-500',
qualified: 'bg-brand-300',
proposal: 'bg-brand-400',
negotiation: 'bg-warning',
closed_won: 'bg-success',
closed_lost: 'bg-error',
};
const getStageColor = (name: string): string => {
const lower = name.toLowerCase().replace(/[\s-]/g, '_');
for (const [key, color] of Object.entries(stageColors)) {
if (lower.includes(key)) return color;
}
return 'bg-brand-200';
};
const getStageTextColor = (name: string): string => {
const lower = name.toLowerCase().replace(/[\s-]/g, '_');
if (lower.includes('won') || lower.includes('closed_won')) return 'text-success';
if (lower.includes('lost') || lower.includes('closed_lost')) return 'text-error';
if (lower.includes('negotiation')) return 'text-warning';
return 'text-surface-600';
};
return (
<AppLayout title="CRM Analytics">
{/* 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">CRM Analytics</h1>
<p className="mt-1 text-sm text-surface-500">
Pipeline performance, contact insights, and deal velocity
</p>
</div>
<Link
to="/admin/leads"
className="inline-flex items-center gap-2 rounded-lg bg-brand-600 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-brand-700 min-h-[44px]"
>
<ArrowRight className="h-4 w-4" />
Go to Leads
</Link>
</div>
{/* KPI Cards */}
<div className="mb-8 grid grid-cols-2 gap-4 lg:grid-cols-4">
<StatCard
title="Companies"
value={summary?.companies ?? 0}
icon={Building2}
iconBg="bg-brand-100 text-brand-600"
/>
<StatCard
title="Contacts"
value={summary?.contacts ?? 0}
icon={Users}
iconBg="bg-blue-100 text-blue-600"
/>
<StatCard
title="Active Deals"
value={summary?.deals ?? 0}
icon={Handshake}
iconBg="bg-purple-100 text-purple-600"
/>
<StatCard
title="Pipeline Value"
value={currency(summary?.pipeline_value ?? 0)}
icon={DollarSign}
iconBg="bg-success/10 text-success"
/>
</div>
{/* Pipeline Visualization + Contacts Analytics */}
<div className="mb-8 grid grid-cols-1 gap-6 lg:grid-cols-5">
{/* Pipeline by Stage */}
<div className="lg:col-span-3 rounded-xl border border-surface-200 bg-white 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">Pipeline by Stage</h3>
</div>
{stages.length === 0 ? (
<p className="text-sm text-surface-500">No pipeline data available</p>
) : (
<div className="space-y-3">
{stages.map((stage) => {
const countWidth = Math.round((stage.count / maxCount) * 100);
const valueWidth = Math.round((stage.value / maxValue) * 100);
return (
<div key={stage.name}>
<div className="mb-1 flex items-center justify-between">
<span className="text-sm font-medium text-surface-700">{stage.name}</span>
<div className="flex items-center gap-3 text-xs text-surface-500">
<span>{stage.count} deals</span>
<span className="font-medium text-surface-700">{currency(stage.value)}</span>
</div>
</div>
<div className="h-3 w-full overflow-hidden rounded-full bg-surface-100">
<div
className={`h-full rounded-full transition-all duration-500 ${getStageColor(stage.name)}`}
style={{ width: `${countWidth}%` }}
/>
</div>
{/* Value bar overlay */}
<div className="mt-1 h-2 w-full overflow-hidden rounded-full bg-surface-100">
<div
className={`h-full rounded-full transition-all duration-500 ${getStageColor(stage.name)} opacity-50`}
style={{ width: `${valueWidth}%` }}
title={`Value: ${currency(stage.value)}`}
/>
</div>
</div>
);
})}
</div>
)}
</div>
{/* Contacts Analytics */}
<div className="lg:col-span-2 space-y-4">
<div className="rounded-xl border border-surface-200 bg-white p-5">
<div className="mb-4 flex items-center gap-2">
<Users className="h-4 w-4 text-blue-600" />
<h3 className="text-sm font-semibold text-surface-700">Contacts Overview</h3>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<p className="text-xs text-surface-500">Total Contacts</p>
<p className="mt-1 text-xl font-semibold text-surface-900">
{contacts?.total?.toLocaleString() ?? 0}
</p>
</div>
<div>
<p className="text-xs text-surface-500">30-Day Growth</p>
<p className="mt-1 text-xl font-semibold text-success">
{contacts?.growth_30d ?? 'N/A'}
</p>
</div>
<div>
<p className="text-xs text-surface-500">Avg Lifecycle</p>
<p className="mt-1 text-xl font-semibold text-surface-900">
{contacts?.avg_lifecycle_days ?? 0} days
</p>
</div>
<div>
<p className="text-xs text-surface-500">Sources</p>
<p className="mt-1 text-xl font-semibold text-surface-900">
{contacts?.source_breakdown ? Object.keys(contacts.source_breakdown).length : 0}
</p>
</div>
</div>
</div>
{/* Source Breakdown */}
{contacts?.source_breakdown && Object.keys(contacts.source_breakdown).length > 0 && (
<div className="rounded-xl border border-surface-200 bg-white p-5">
<h3 className="mb-3 text-sm font-semibold text-surface-700">Contact Sources</h3>
<div className="space-y-2">
{Object.entries(contacts.source_breakdown)
.sort(([, a], [, b]) => b - a)
.map(([source, count]) => {
const maxSource = Math.max(...Object.values(contacts.source_breakdown), 1);
const pct = Math.round((count / maxSource) * 100);
return (
<div key={source}>
<div className="mb-1 flex items-center justify-between text-xs">
<span className="font-medium text-surface-600">{source}</span>
<span className="text-surface-500">{count}</span>
</div>
<div className="h-2 w-full overflow-hidden rounded-full bg-surface-100">
<div
className="h-full rounded-full bg-brand-400 transition-all duration-500"
style={{ width: `${pct}%` }}
/>
</div>
</div>
);
})}
</div>
</div>
)}
</div>
</div>
{/* Deals Velocity + Forecasts */}
<div className="mb-8 grid grid-cols-1 gap-6 lg:grid-cols-2">
{/* Deals Velocity */}
<div className="rounded-xl border border-surface-200 bg-white p-5">
<div className="mb-4 flex items-center gap-2">
<Activity className="h-4 w-4 text-purple-600" />
<h3 className="text-sm font-semibold text-surface-700">Deals Velocity</h3>
</div>
<div className="grid grid-cols-2 gap-4 sm:grid-cols-4">
<div>
<p className="text-xs text-surface-500">Avg Days to Close</p>
<div className="mt-1 flex items-center gap-1">
<Clock className="h-4 w-4 text-surface-400" />
<p className="text-lg font-semibold text-surface-900">
{velocity?.avg_days_to_close ?? 0}
</p>
</div>
</div>
<div>
<p className="text-xs text-surface-500">Monthly Wins</p>
<div className="mt-1 flex items-center gap-1">
<Target className="h-4 w-4 text-success" />
<p className="text-lg font-semibold text-success">
{velocity?.monthly_wins ?? 0}
</p>
</div>
</div>
<div>
<p className="text-xs text-surface-500">Win Rate</p>
<div className="mt-1 flex items-center gap-1">
<TrendingUp className="h-4 w-4 text-brand-600" />
<p className="text-lg font-semibold text-brand-700">
{percent(velocity?.win_rate ?? 0)}
</p>
</div>
</div>
<div>
<p className="text-xs text-surface-500">Velocity Score</p>
<div className="mt-1 flex items-center gap-1">
<BarChart3 className="h-4 w-4 text-warning" />
<p className="text-lg font-semibold text-surface-900">
{velocity?.pipeline_velocity_score ?? 0}
</p>
</div>
</div>
</div>
{/* Win rate progress bar */}
<div className="mt-4">
<div className="mb-1 flex items-center justify-between text-xs">
<span className="text-surface-500">Win Rate Progress</span>
<span className="font-medium text-surface-700">{percent(velocity?.win_rate ?? 0)}</span>
</div>
<div className="h-3 w-full overflow-hidden rounded-full bg-surface-100">
<div
className="h-full rounded-full bg-gradient-to-r from-brand-300 to-brand-600 transition-all duration-500"
style={{ width: `${Math.min(velocity?.win_rate ?? 0, 100)}%` }}
/>
</div>
</div>
</div>
{/* Revenue Forecasts */}
<div className="rounded-xl border border-surface-200 bg-white p-5">
<div className="mb-4 flex items-center gap-2">
<TrendingUp className="h-4 w-4 text-success" />
<h3 className="text-sm font-semibold text-surface-700">Revenue Forecast</h3>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<p className="text-xs text-surface-500">Monthly Revenue</p>
<p className="mt-1 text-lg font-semibold text-surface-900">
{currency(forecast?.monthly_revenue ?? 0)}
</p>
</div>
<div>
<p className="text-xs text-surface-500">Monthly Deals</p>
<p className="mt-1 text-lg font-semibold text-surface-900">
{forecast?.monthly_deals ?? 0}
</p>
</div>
<div>
<p className="text-xs text-surface-500">Quarterly Revenue</p>
<p className="mt-1 text-lg font-semibold text-surface-900">
{currency(forecast?.quarterly_revenue ?? 0)}
</p>
</div>
<div>
<p className="text-xs text-surface-500">Quarterly Deals</p>
<p className="mt-1 text-lg font-semibold text-surface-900">
{forecast?.quarterly_deals ?? 0}
</p>
</div>
</div>
{/* Monthly vs Quarterly visual comparison */}
<div className="mt-4 space-y-2">
<div>
<div className="mb-1 flex items-center justify-between text-xs">
<span className="text-surface-500">Monthly Revenue Target</span>
<span className="font-medium text-surface-700">
{forecast ? `${Math.round((forecast.monthly_revenue / (forecast.quarterly_revenue || 1)) * 100)}% of quarterly` : '—'}
</span>
</div>
<div className="h-2.5 w-full overflow-hidden rounded-full bg-surface-100">
<div
className="h-full rounded-full bg-brand-500 transition-all duration-500"
style={{
width: `${forecast ? Math.min((forecast.monthly_revenue / (forecast.quarterly_revenue || 1)) * 100, 100) : 0}%`,
}}
/>
</div>
</div>
<div>
<div className="mb-1 flex items-center justify-between text-xs">
<span className="text-surface-500">Monthly Deals vs Quarterly</span>
<span className="font-medium text-surface-700">
{forecast && forecast.quarterly_deals > 0
? `${Math.round((forecast.monthly_deals / forecast.quarterly_deals) * 100)}% of quarterly`
: '—'}
</span>
</div>
<div className="h-2.5 w-full overflow-hidden rounded-full bg-surface-100">
<div
className="h-full rounded-full bg-purple-500 transition-all duration-500"
style={{
width: `${forecast && forecast.quarterly_deals > 0 ? Math.min((forecast.monthly_deals / forecast.quarterly_deals) * 100, 100) : 0}%`,
}}
/>
</div>
</div>
</div>
</div>
</div>
</AppLayout>
);
}