← 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>
  );
}