← back to Hub

components/CommandCenterTab.tsx

479 lines

'use client';

import { useState, useEffect, useCallback } from 'react';
import { SkeletonList, SkeletonStats } from './Skeleton';
import {
  LayoutDashboard,
  Mail,
  Award,
  Megaphone,
  DollarSign,
  Activity,
  Database,
  Server,
  ExternalLink,
  RefreshCw,
  Clock,
  FileText,
  Handshake,
  Users,
  Zap,
} from 'lucide-react';
import { useToast } from './ToastProvider';

interface AppStatus {
  name: string;
  port: number;
  color: string;
  status: 'online' | 'offline';
  responseTime: number;
}

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

interface ActivityItem {
  id: string;
  app: string;
  type: string;
  title: string;
  description: string;
  created_at: string;
}

const APP_ICONS: Record<string, React.ElementType> = {
  SDCC: Mail,
  Grant: Award,
  Patty: Megaphone,
  Freddy: DollarSign,
};

const TYPE_ICONS: Record<string, React.ElementType> = {
  draft: FileText,
  collaboration: Handshake,
  grant: Award,
  proposal: FileText,
  petition: Megaphone,
  campaign: Mail,
  match: Zap,
  contact: Users,
};

function formatTime(ts: string) {
  const d = new Date(ts);
  return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }) +
    ' ' + d.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit', timeZone: 'America/Los_Angeles' });
}

export default function CommandCenterTab() {
  const { addToast } = useToast();
  const [statuses, setStatuses] = useState<AppStatus[]>([]);
  const [dashboard, setDashboard] = useState<DashboardData | null>(null);
  const [activities, setActivities] = useState<ActivityItem[]>([]);
  const [loading, setLoading] = useState(true);
  const [refreshing, setRefreshing] = useState(false);

  const fetchAll = useCallback(async (isRefresh = false) => {
    if (isRefresh) setRefreshing(true);
    try {
      const [statusRes, dashRes, actRes] = await Promise.all([
        fetch('/api/apps/status', { credentials: 'include' }),
        fetch('/api/dashboard', { credentials: 'include' }),
        fetch('/api/activity', { credentials: 'include' }),
      ]);
      if (statusRes.ok) {
        const s = await statusRes.json();
        setStatuses(s.apps || []);
      }
      if (dashRes.ok) {
        const d = await dashRes.json();
        setDashboard(d);
      }
      if (actRes.ok) {
        const a = await actRes.json();
        setActivities(a.items || []);
      }
      if (isRefresh) addToast('Dashboard refreshed', 'success');
    } catch (err) {
      console.error('Fetch error:', err);
      if (isRefresh) addToast('Refresh failed', 'error');
    } finally {
      setLoading(false);
      setRefreshing(false);
    }
  }, []);

  useEffect(() => { fetchAll(); }, [fetchAll]);

  if (loading) {
    return (
      <div className="p-5 space-y-6">
        <SkeletonStats count={4} />
        <SkeletonList count={3} />
      </div>
    );
  }

  const onlineCount = statuses.filter(s => s.status === 'online').length;

  return (
    <div className="p-5 space-y-6">
      {/* Header */}
      <div className="flex items-center justify-between">
        <div>
          <h2 className="text-xl font-bold" style={{ color: 'var(--color-text)' }}>
            Command Center
          </h2>
          <p className="text-sm mt-0.5" style={{ color: 'var(--color-text-muted)' }}>
            SDCC Ecosystem Overview
          </p>
        </div>
        <button
          onClick={() => fetchAll(true)}
          disabled={refreshing}
          className="btn btn-secondary btn-sm"
        >
          <RefreshCw size={14} className={refreshing ? 'animate-spin' : ''} />
          Refresh
        </button>
      </div>

      {/* App Status Grid */}
      <div className="grid grid-cols-4 gap-4">
        {statuses.map((app) => (
          <div
            key={app.name}
            className="card"
            style={{ borderLeft: `3px solid ${app.color}` }}
          >
            <div className="flex items-center justify-between mb-2">
              <div className="flex items-center gap-2">
                {(() => {
                  const Icon = APP_ICONS[app.name] || Server;
                  return <Icon size={16} style={{ color: app.color }} />;
                })()}
                <span className="font-semibold text-sm" style={{ color: 'var(--color-text)' }}>
                  {app.name}
                </span>
              </div>
              <div className="flex items-center gap-1.5">
                <div
                  className={app.status === 'online' ? 'pulse-online' : ''}
                  style={{
                    width: 8,
                    height: 8,
                    borderRadius: '50%',
                    backgroundColor: app.status === 'online' ? '#22c55e' : '#ef4444',
                  }}
                />
                <span
                  className="text-xs font-medium"
                  style={{ color: app.status === 'online' ? '#22c55e' : '#ef4444' }}
                >
                  {app.status === 'online' ? 'Online' : 'Offline'}
                </span>
              </div>
            </div>
            <div className="flex items-center justify-between">
              <span className="text-xs" style={{ color: 'var(--color-text-muted)' }}>
                Port {app.port}
              </span>
              <span className="text-xs" style={{ color: 'var(--color-text-muted)' }}>
                {app.responseTime}ms
              </span>
            </div>
          </div>
        ))}
      </div>

      {/* Aggregate Stats Row */}
      <div className="grid grid-cols-4 gap-4">
        <div className="stat-card">
          <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</span>
          </div>
          <div className="text-2xl font-bold" style={{ color: 'var(--color-text)' }}>
            {dashboard?.system.total_records?.toLocaleString() || '0'}
          </div>
        </div>
        <div className="stat-card">
          <div className="flex items-center gap-2 mb-1">
            <Server size={14} style={{ color: 'var(--color-accent)' }} />
            <span className="text-xs font-medium" style={{ color: 'var(--color-text-muted)' }}>Apps Online</span>
          </div>
          <div className="text-2xl font-bold" style={{ color: 'var(--color-text)' }}>
            {onlineCount}<span className="text-sm font-normal" style={{ color: 'var(--color-text-muted)' }}> / 4</span>
          </div>
        </div>
        <div className="stat-card">
          <div className="flex items-center gap-2 mb-1">
            <LayoutDashboard size={14} style={{ color: 'var(--color-accent)' }} />
            <span className="text-xs font-medium" style={{ color: 'var(--color-text-muted)' }}>Databases</span>
          </div>
          <div className="text-2xl font-bold" style={{ color: 'var(--color-text)' }}>4</div>
        </div>
        <div className="stat-card">
          <div className="flex items-center gap-2 mb-1">
            <Activity size={14} style={{ color: 'var(--color-accent)' }} />
            <span className="text-xs font-medium" style={{ color: 'var(--color-text-muted)' }}>Recent Activity</span>
          </div>
          <div className="text-2xl font-bold" style={{ color: 'var(--color-text)' }}>
            {activities.length}
          </div>
        </div>
      </div>

      {/* App Cards — 2x2 grid */}
      <div className="grid grid-cols-2 gap-4">
        {/* SDCC Card */}
        <AppCard
          name="SDCC"
          subtitle="Email Draft System"
          color="#3b82f6"
          port={7400}
          Icon={Mail}
          status={statuses.find(s => s.name === 'SDCC')?.status || 'offline'}
          stats={dashboard ? [
            { label: 'Drafts', value: dashboard.sdcc.drafts },
            { label: 'News Items', value: dashboard.sdcc.news },
            { label: 'Petitions', value: dashboard.sdcc.petitions },
            { label: 'Collaborations', value: dashboard.sdcc.collaborations },
          ] : []}
        />

        {/* Grant Card */}
        <AppCard
          name="Grant"
          subtitle="Grant Discovery"
          color="#059669"
          port={7450}
          Icon={Award}
          status={statuses.find(s => s.name === 'Grant')?.status || 'offline'}
          stats={dashboard ? [
            { label: 'Grants', value: dashboard.grant.grants },
            { label: 'Proposals', value: dashboard.grant.proposals },
            { label: 'News', value: dashboard.grant.news },
            { label: 'Collabs', value: dashboard.grant.collaborations },
          ] : []}
        />

        {/* Patty Card */}
        <AppCard
          name="Patty"
          subtitle="Petition Platform"
          color="#7c3aed"
          port={7460}
          Icon={Megaphone}
          status={statuses.find(s => s.name === 'Patty')?.status || 'offline'}
          stats={dashboard ? [
            { label: 'Petitions', value: dashboard.patty.petitions },
            { label: 'Signatures', value: dashboard.patty.signatures },
            { label: 'Campaigns', value: dashboard.patty.campaigns },
            { label: 'Subscribers', value: dashboard.patty.subscribers },
          ] : []}
        />

        {/* Freddy Card */}
        <AppCard
          name="Freddy"
          subtitle="Fundraising Engine"
          color="#d97706"
          port={7470}
          Icon={DollarSign}
          status={statuses.find(s => s.name === 'Freddy')?.status || 'offline'}
          stats={dashboard ? [
            { label: 'Causes', value: dashboard.freddy.causes },
            { label: 'Organizations', value: dashboard.freddy.organizations },
            { label: 'Matches', value: dashboard.freddy.matches },
            { label: 'Contacts', value: dashboard.freddy.contacts },
          ] : []}
        />
      </div>

      {/* Activity Feed */}
      <div className="card">
        <div className="flex items-center gap-2 mb-4">
          <Activity size={16} style={{ color: 'var(--color-accent)' }} />
          <h3 className="text-sm font-semibold" style={{ color: 'var(--color-text)' }}>
            Recent Activity
          </h3>
        </div>
        <div className="space-y-2">
          {activities.slice(0, 10).map((item) => {
            const TypeIcon = TYPE_ICONS[item.type] || Activity;
            const appColor = item.app === 'sdcc' ? '#3b82f6' : item.app === 'grant' ? '#059669' : item.app === 'patty' ? '#7c3aed' : '#d97706';
            return (
              <div
                key={item.id}
                className="flex items-center gap-3 p-2 rounded-lg"
                style={{ backgroundColor: 'var(--color-surface-el)' }}
              >
                <div
                  className="flex items-center justify-center shrink-0"
                  style={{
                    width: 28,
                    height: 28,
                    borderRadius: 6,
                    backgroundColor: `${appColor}18`,
                  }}
                >
                  <TypeIcon size={14} style={{ color: appColor }} />
                </div>
                <div className="flex-1 min-w-0">
                  <div className="flex items-center gap-2">
                    <span
                      className="badge"
                      style={{
                        backgroundColor: `${appColor}18`,
                        color: appColor,
                        border: `1px solid ${appColor}40`,
                        fontSize: 10,
                        padding: '1px 6px',
                      }}
                    >
                      {item.app.toUpperCase()}
                    </span>
                    <span className="text-sm font-medium truncate" style={{ color: 'var(--color-text)' }}>
                      {item.title}
                    </span>
                  </div>
                  <span className="text-xs" style={{ color: 'var(--color-text-muted)' }}>
                    {item.description}
                  </span>
                </div>
                <div className="flex items-center gap-1 shrink-0">
                  <Clock size={11} style={{ color: 'var(--color-text-muted)' }} />
                  <span className="text-xs" style={{ color: 'var(--color-text-muted)' }}>
                    {formatTime(item.created_at)}
                  </span>
                </div>
              </div>
            );
          })}
          {activities.length === 0 && (
            <p className="text-sm text-center py-4" style={{ color: 'var(--color-text-muted)' }}>
              No recent activity
            </p>
          )}
        </div>
      </div>

      {/* Quick Actions */}
      <div className="card">
        <h3 className="text-sm font-semibold mb-3" style={{ color: 'var(--color-text)' }}>
          Quick Actions
        </h3>
        <div className="flex gap-3">
          {[
            { name: 'SDCC', port: 7400, color: '#3b82f6', Icon: Mail },
            { name: 'Grant', port: 7450, color: '#059669', Icon: Award },
            { name: 'Patty', port: 7460, color: '#7c3aed', Icon: Megaphone },
            { name: 'Freddy', port: 7470, color: '#d97706', Icon: DollarSign },
          ].map((app) => (
            <a
              key={app.name}
              href={`http://45.61.58.125:${app.port}`}
              target="_blank"
              rel="noopener noreferrer"
              className="btn btn-secondary flex-1"
              style={{ borderColor: `${app.color}40` }}
            >
              <app.Icon size={14} style={{ color: app.color }} />
              Open {app.name}
              <ExternalLink size={12} style={{ color: 'var(--color-text-muted)' }} />
            </a>
          ))}
        </div>
      </div>
    </div>
  );
}

/* ---- AppCard component --------------------------------------------------- */
function AppCard({
  name,
  subtitle,
  color,
  port,
  Icon,
  status,
  stats,
}: {
  name: string;
  subtitle: string;
  color: string;
  port: number;
  Icon: React.ElementType;
  status: 'online' | 'offline';
  stats: { label: string; value: number }[];
}) {
  return (
    <div
      className="card"
      style={{ borderTop: `3px solid ${color}` }}
    >
      <div className="flex items-center justify-between mb-4">
        <div className="flex items-center gap-3">
          <div
            style={{
              width: 36,
              height: 36,
              borderRadius: 10,
              backgroundColor: `${color}18`,
              display: 'flex',
              alignItems: 'center',
              justifyContent: 'center',
            }}
          >
            <Icon size={18} style={{ color }} />
          </div>
          <div>
            <div className="font-semibold text-sm" style={{ color: 'var(--color-text)' }}>{name}</div>
            <div className="text-xs" style={{ color: 'var(--color-text-muted)' }}>{subtitle}</div>
          </div>
        </div>
        <div className="flex items-center gap-2">
          <div
            className={status === 'online' ? 'pulse-online' : ''}
            style={{
              width: 8,
              height: 8,
              borderRadius: '50%',
              backgroundColor: status === 'online' ? '#22c55e' : '#ef4444',
            }}
          />
          <a
            href={`http://45.61.58.125:${port}`}
            target="_blank"
            rel="noopener noreferrer"
            className="btn btn-ghost btn-sm"
            style={{ color }}
          >
            Open <ExternalLink size={12} />
          </a>
        </div>
      </div>
      <div className="grid grid-cols-2 gap-3">
        {stats.map((stat) => (
          <div
            key={stat.label}
            className="rounded-lg p-2.5"
            style={{ backgroundColor: 'var(--color-surface-el)' }}
          >
            <div className="text-xs" style={{ color: 'var(--color-text-muted)' }}>{stat.label}</div>
            <div className="text-lg font-bold mt-0.5" style={{ color: 'var(--color-text)' }}>
              {stat.value.toLocaleString()}
            </div>
          </div>
        ))}
      </div>
    </div>
  );
}