← back to Hub
components/AnalyticsTab.tsx
178 lines
'use client';
import { useState, useEffect } from 'react';
import { SkeletonList, SkeletonStats } from './Skeleton';
import { BarChart3, Database, Mail, Award, Megaphone, DollarSign, RefreshCw } from 'lucide-react';
interface DashboardData {
sdcc: { drafts: number; news: number; petitions: number; collaborations: number; partners: number; leads: number; library: number };
grant: { grants: number; proposals: number; news: number; collaborations: number; total_funding: number };
patty: { petitions: number; signatures: number; campaigns: number; subscribers: number; templates: number };
freddy: { causes: number; organizations: number; granters: number; matches: number; contacts: number; revenue_total: number };
system: { apps_online: number; total_records: number };
}
function calcAppTotal(data: DashboardData, app: string): number {
switch (app) {
case 'sdcc':
return data.sdcc.drafts + data.sdcc.news + data.sdcc.petitions + data.sdcc.collaborations + data.sdcc.partners + data.sdcc.leads + data.sdcc.library;
case 'grant':
return data.grant.grants + data.grant.proposals + data.grant.news + data.grant.collaborations;
case 'patty':
return data.patty.petitions + data.patty.campaigns + data.patty.subscribers + data.patty.templates;
case 'freddy':
return data.freddy.causes + data.freddy.organizations + data.freddy.granters + data.freddy.matches + data.freddy.contacts;
default:
return 0;
}
}
export default function AnalyticsTab() {
const [data, setData] = useState<DashboardData | null>(null);
const [loading, setLoading] = useState(true);
async function fetchData() {
try {
const res = await fetch('/api/dashboard', { credentials: 'include' });
if (res.ok) setData(await res.json());
} catch (err) {
console.error('Analytics fetch error:', err);
} finally {
setLoading(false);
}
}
useEffect(() => { fetchData(); }, []);
if (loading || !data) {
return <div className="p-5 space-y-6"><SkeletonStats count={4} /><SkeletonList count={3} /></div>;
}
const appTotals = [
{ name: 'SDCC', total: calcAppTotal(data, 'sdcc'), color: '#3b82f6', Icon: Mail },
{ name: 'Grant', total: calcAppTotal(data, 'grant'), color: '#059669', Icon: Award },
{ name: 'Patty', total: calcAppTotal(data, 'patty'), color: '#7c3aed', Icon: Megaphone },
{ name: 'Freddy', total: calcAppTotal(data, 'freddy'), color: '#d97706', Icon: DollarSign },
];
const maxTotal = Math.max(...appTotals.map(a => a.total), 1);
return (
<div className="p-5 space-y-6">
{/* Header */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<BarChart3 size={20} style={{ color: 'var(--color-accent)' }} />
<div>
<h2 className="text-xl font-bold" style={{ color: 'var(--color-text)' }}>Analytics</h2>
<p className="text-sm" style={{ color: 'var(--color-text-muted)' }}>Aggregate view across all apps</p>
</div>
</div>
<button onClick={() => { setLoading(true); fetchData(); }} className="btn btn-secondary btn-sm">
<RefreshCw size={14} /> Refresh
</button>
</div>
{/* Total Records */}
<div className="stat-card" style={{ borderLeft: '3px solid var(--color-accent)' }}>
<div className="flex items-center gap-2 mb-1">
<Database size={14} style={{ color: 'var(--color-accent)' }} />
<span className="text-xs font-medium" style={{ color: 'var(--color-text-muted)' }}>Total Records Across All Apps</span>
</div>
<div className="text-3xl font-bold" style={{ color: 'var(--color-text)' }}>
{data.system.total_records.toLocaleString()}
</div>
</div>
{/* Per-App Distribution */}
<div className="card">
<h3 className="text-sm font-semibold mb-4" style={{ color: 'var(--color-text)' }}>Record Distribution by App</h3>
<div className="space-y-4">
{appTotals.map((app) => {
const pct = maxTotal > 0 ? (app.total / maxTotal) * 100 : 0;
return (
<div key={app.name}>
<div className="flex items-center justify-between mb-1.5">
<div className="flex items-center gap-2">
<app.Icon size={14} style={{ color: app.color }} />
<span className="text-sm font-medium" style={{ color: 'var(--color-text)' }}>{app.name}</span>
</div>
<span className="text-sm font-bold" style={{ color: 'var(--color-text)' }}>
{app.total.toLocaleString()}
</span>
</div>
<div
style={{
width: '100%',
height: 8,
borderRadius: 4,
backgroundColor: 'var(--color-surface-el)',
overflow: 'hidden',
}}
>
<div
style={{
width: `${pct}%`,
height: '100%',
borderRadius: 4,
backgroundColor: app.color,
transition: 'width 0.5s ease',
}}
/>
</div>
</div>
);
})}
</div>
</div>
{/* Key Metrics Summary Table */}
<div className="card">
<h3 className="text-sm font-semibold mb-4" style={{ color: 'var(--color-text)' }}>Key Metrics Summary</h3>
<div style={{ overflowX: 'auto' }}>
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
<thead>
<tr>
<th style={{ textAlign: 'left', padding: '8px 12px', borderBottom: '1px solid var(--color-border)', color: 'var(--color-text-muted)', fontSize: 12, fontWeight: 600 }}>App</th>
<th style={{ textAlign: 'left', padding: '8px 12px', borderBottom: '1px solid var(--color-border)', color: 'var(--color-text-muted)', fontSize: 12, fontWeight: 600 }}>Metric</th>
<th style={{ textAlign: 'right', padding: '8px 12px', borderBottom: '1px solid var(--color-border)', color: 'var(--color-text-muted)', fontSize: 12, fontWeight: 600 }}>Count</th>
</tr>
</thead>
<tbody>
{[
{ app: 'SDCC', color: '#3b82f6', metrics: [['Drafts', data.sdcc.drafts], ['News', data.sdcc.news], ['Petitions', data.sdcc.petitions], ['Collaborations', data.sdcc.collaborations], ['Partners', data.sdcc.partners], ['Leads', data.sdcc.leads], ['Library', data.sdcc.library]] },
{ app: 'Grant', color: '#059669', metrics: [['Grants', data.grant.grants], ['Proposals', data.grant.proposals], ['News', data.grant.news], ['Collaborations', data.grant.collaborations]] },
{ app: 'Patty', color: '#7c3aed', metrics: [['Petitions', data.patty.petitions], ['Signatures', data.patty.signatures], ['Campaigns', data.patty.campaigns], ['Subscribers', data.patty.subscribers], ['Templates', data.patty.templates]] },
{ app: 'Freddy', color: '#d97706', metrics: [['Causes', data.freddy.causes], ['Organizations', data.freddy.organizations], ['Granters', data.freddy.granters], ['Matches', data.freddy.matches], ['Contacts', data.freddy.contacts]] },
].map(({ app, color, metrics }) =>
metrics.map(([label, value], idx) => (
<tr key={`${app}-${label}`}>
{idx === 0 && (
<td
rowSpan={metrics.length}
style={{
padding: '8px 12px',
borderBottom: '1px solid var(--color-border)',
verticalAlign: 'top',
fontWeight: 600,
fontSize: 13,
color,
}}
>
{app}
</td>
)}
<td style={{ padding: '6px 12px', borderBottom: '1px solid var(--color-border)', fontSize: 13, color: 'var(--color-text-secondary)' }}>{label as string}</td>
<td style={{ padding: '6px 12px', borderBottom: '1px solid var(--color-border)', fontSize: 13, color: 'var(--color-text)', textAlign: 'right', fontWeight: 600 }}>
{(value as number).toLocaleString()}
</td>
</tr>
))
)}
</tbody>
</table>
</div>
</div>
</div>
);
}