+
{label}
+
+ {data.map((d, i) => (
+
+
{d.value || ''}
+
0 ? 4 : 2 }}
+ />
+
+ {d.label}
+
+
+ ))}
+
+
+ );
+}
+
+function EngagementRing({ summary }: { summary: EngagementData['summary'] }) {
+ const total = summary.engaged + summary.warm + summary.cooling + summary.cold;
+ if (total === 0) return null;
+ const segments = [
+ { label: 'Engaged', count: summary.engaged, color: 'bg-emerald-500', textColor: 'text-emerald-600', icon: Flame },
+ { label: 'Warm', count: summary.warm, color: 'bg-amber-400', textColor: 'text-amber-600', icon: ThermometerSun },
+ { label: 'Cooling', count: summary.cooling, color: 'bg-blue-400', textColor: 'text-blue-600', icon: Activity },
+ { label: 'Cold', count: summary.cold, color: 'bg-slate-300', textColor: 'text-slate-500', icon: Snowflake },
+ ];
+
+ return (
+
+
Engagement Breakdown
+ {/* Stacked bar */}
+
+ {segments.map(s => (
+ s.count > 0 && (
+
+ )
+ ))}
+
+
+ {segments.map(s => (
+
+
+
+ {s.count}
+ {s.label}
+
+
+ ))}
+
+
+ Engaged = contacted in last 14 days • Warm = 15-30 days • Cooling = 31-60 days • Cold = 60+ days or never
+
+
+ );
+}
+
+function TopList({ title, items, icon: Icon }: {
+ title: string;
+ items: { label: string; value: number }[];
+ icon: typeof Tag;
+}) {
+ const max = Math.max(...items.map(i => i.value), 1);
+ return (
+
+
+
+
{title}
+
+ {items.length === 0 && (
+
No data yet
+ )}
+
+ {items.slice(0, 8).map((item, i) => (
+
+
+ {item.label}
+ {item.value}
+
+
+
+ ))}
+
+
+ );
+}
+
+function AtRiskList({ title, clients: clientList }: {
+ title: string;
+ clients: { id: string; name: string; company: string | null; lastContacted: string | null }[];
+}) {
+ if (clientList.length === 0) return null;
+ return (
+
+
+
+ {clientList.map(c => (
+
+
+
{c.name}
+ {c.company &&
{c.company}
}
+
+
+ {c.lastContacted
+ ? `${Math.floor((Date.now() - new Date(c.lastContacted).getTime()) / (1000 * 60 * 60 * 24))}d ago`
+ : 'Never'}
+
+
+ ))}
+
+
+ );
+}
+
+// Fill in missing months with 0
+function fillMonths(data: { month: string; count: number }[]): { label: string; value: number }[] {
+ const now = new Date();
+ const months: { label: string; value: number }[] = [];
+ for (let i = 11; i >= 0; i--) {
+ const d = new Date(now.getFullYear(), now.getMonth() - i, 1);
+ const key = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}`;
+ const label = d.toLocaleString('default', { month: 'short' });
+ const match = data.find(m => m.month === key);
+ months.push({ label, value: match?.count || 0 });
+ }
+ return months;
+}
+
+export default function ReportsPage() {
+ const [overview, setOverview] = useState
(null);
+ const [growth, setGrowth] = useState(null);
+ const [industries, setIndustries] = useState([]);
+ const [tags, setTags] = useState([]);
+ const [engagement, setEngagement] = useState(null);
+ const [loading, setLoading] = useState(true);
+ const [exporting, setExporting] = useState(false);
+
+ useEffect(() => {
+ Promise.all([
+ api.getReportsOverview().catch(() => null),
+ api.getReportsGrowth().catch(() => null),
+ api.getReportsIndustries().catch(() => []),
+ api.getReportsTags().catch(() => []),
+ api.getReportsEngagement().catch(() => null),
+ ]).then(([ov, gr, ind, tg, eng]) => {
+ setOverview(ov as Overview | null);
+ setGrowth(gr as GrowthData | null);
+ setIndustries(ind as IndustryData[]);
+ setTags(tg as TagData[]);
+ setEngagement(eng as EngagementData | null);
+ setLoading(false);
+ });
+ }, []);
+
+ const handleExport = async () => {
+ setExporting(true);
+ try {
+ await api.exportClientsCSV();
+ } finally {
+ setExporting(false);
+ }
+ };
+
+ if (loading) return ;
+
+ return (
+
+
+
+
Reports & Analytics
+
Overview of your CRM performance
+
+
+
+
+ {/* Overview Stats */}
+ {overview && (
+
+
+
+
+
+
+ )}
+
+ {/* Growth Charts */}
+ {growth && (
+
+
+
+
+ )}
+
+ {/* Engagement + Distributions */}
+
+ {engagement && }
+ ({ label: i.industry || 'Unknown', value: i.count }))} icon={Building2} />
+ ({ label: t.tag, value: t.count }))} icon={Tag} />
+
+
+ {/* At-risk clients */}
+ {engagement && (
+
+ )}
+
+ );
+}