← back to Norma

app/supervisor/page.tsx

578 lines

'use client';

import { useState, useEffect } from 'react';
import { useRouter } from 'next/navigation';
import { motion, AnimatePresence } from 'framer-motion';
import {
  Shield, Users, Globe, ChevronRight, X, Activity,
  Mail, Newspaper, ScrollText, Search, Target, Building2,
  BarChart3, Settings, Zap, Eye, ArrowRight, Bot,
  UserPlus, FileText, MessageCircle, Database,
} from 'lucide-react';
import GlobalSearch from '@/components/GlobalSearch';

/* ─── Types ────────────────────────────────────────────────────────────── */

interface OrgData {
  id: string;
  name: string;
  city: string | null;
  state: string | null;
  type: string;
}

interface AgentNode {
  id: string;
  label: string;
  tier: 'supervisor' | 'admin' | 'staff' | 'public';
  description: string;
  icon: typeof Shield;
  color: string;
  bg: string;
  status: 'online' | 'idle' | 'offline';
  stats: { label: string; value: string }[];
  path: string;
  orgId?: string;
  children?: string[];
}

/* ─── Agent Registry ───────────────────────────────────────────────────── */

function prioritizeOrgs(orgs: OrgData[]): OrgData[] {
  // SDCC Center first, then NAACP, then rest alphabetical
  const priority = ['Student Debt Crisis Center', 'NAACP', 'Student Debt Crisis'];
  const prioritized = priority
    .map(name => orgs.find(o => o.name.trim() === name))
    .filter(Boolean) as OrgData[];
  const prioritizedIds = new Set(prioritized.map(o => o.id));
  const rest = orgs
    .filter(o => !prioritizedIds.has(o.id))
    .sort((a, b) => a.name.localeCompare(b.name));
  return [...prioritized, ...rest];
}

function buildAgentTree(orgs: OrgData[]): AgentNode[] {
  const sorted = prioritizeOrgs(orgs);
  const agents: AgentNode[] = [
    {
      id: 'supervisor',
      label: 'Supervisor',
      tier: 'supervisor',
      description: 'System overseer. Monitors all tiers, orgs, and agent health.',
      icon: Eye,
      color: '#f59e0b',
      bg: 'rgba(245,158,11,0.10)',
      status: 'online',
      stats: [
        { label: 'Tiers', value: '3' },
        { label: 'Orgs', value: String(orgs.length) },
        { label: 'Agents', value: '6' },
      ],
      path: '/supervisor',
      children: ['norma'],
    },
    {
      id: 'norma',
      label: 'Norma',
      tier: 'admin',
      description: 'Admin tier. Full platform access: settings, credentials, all orgs, analytics.',
      icon: Shield,
      color: '#0a7c59',
      bg: 'rgba(10,124,89,0.10)',
      status: 'online',
      stats: [
        { label: 'Tabs', value: '20+' },
        { label: 'APIs', value: '40+' },
        { label: 'Role', value: 'Admin' },
      ],
      path: '/',
      children: sorted.slice(0, 6).map(o => `nikki-${o.id}`),
    },
  ];

  // Nikki + Pulse instances per org
  for (const org of sorted.slice(0, 6)) {
    agents.push({
      id: `nikki-${org.id}`,
      label: `Nikki`,
      tier: 'staff',
      description: `Staff tier for ${org.name}. Org-locked dashboard, email, petitions, news.`,
      icon: Users,
      color: '#4f46e5',
      bg: 'rgba(79,70,229,0.10)',
      status: 'online',
      stats: [
        { label: 'Org', value: org.name.length > 20 ? org.name.slice(0, 18) + '...' : org.name },
        { label: 'Location', value: org.state || '—' },
        { label: 'Role', value: 'Staff' },
      ],
      path: '/',
      orgId: org.id,
      children: [`pulse-${org.id}`],
    });

    agents.push({
      id: `pulse-${org.id}`,
      label: `Pulse`,
      tier: 'public',
      description: `Public tier for ${org.name}. Petition feed, activity history, community.`,
      icon: Globe,
      color: '#e11d48',
      bg: 'rgba(225,29,72,0.10)',
      status: 'online',
      stats: [
        { label: 'Org', value: org.name.length > 20 ? org.name.slice(0, 18) + '...' : org.name },
        { label: 'Access', value: 'Public' },
        { label: 'Role', value: 'User' },
      ],
      path: '/pulse',
      orgId: org.id,
    });
  }

  return agents;
}

/* ─── Sub-agents (functional modules within each tier) ──────────────── */

const SUB_AGENTS: Record<string, { label: string; icon: typeof Mail; color: string }[]> = {
  norma: [
    { label: 'Email Center', icon: Mail, color: '#0a7c59' },
    { label: 'News Intel', icon: Newspaper, color: '#4f46e5' },
    { label: 'Petitions', icon: ScrollText, color: '#e11d48' },
    { label: 'Grants', icon: Search, color: '#f59e0b' },
    { label: 'Congress', icon: Building2, color: '#8b5cf6' },
    { label: 'Analytics', icon: BarChart3, color: '#06b6d4' },
    { label: 'Settings', icon: Settings, color: '#6b7280' },
    { label: 'AI Chat', icon: Bot, color: '#0a7c59' },
  ],
  staff: [
    { label: 'Email Sends', icon: Mail, color: '#4f46e5' },
    { label: 'Petitions', icon: ScrollText, color: '#e11d48' },
    { label: 'News', icon: Newspaper, color: '#0a7c59' },
    { label: 'Contacts', icon: UserPlus, color: '#f59e0b' },
    { label: 'Gmail CRM', icon: MessageCircle, color: '#06b6d4' },
    { label: 'Congress', icon: Building2, color: '#8b5cf6' },
  ],
  public: [
    { label: 'Petition Feed', icon: ScrollText, color: '#e11d48' },
    { label: 'Activity', icon: Activity, color: '#0a7c59' },
    { label: 'Community', icon: Users, color: '#4f46e5' },
    { label: 'Reports', icon: FileText, color: '#f59e0b' },
  ],
};

/* ─── Flow Connector SVG ────────────────────────────────────────────── */

function FlowLine({ from, to }: { from: string; to: string }) {
  return (
    <div style={{
      width: 2,
      height: 32,
      background: 'var(--color-border)',
      margin: '0 auto',
    }} />
  );
}

/* ─── Agent Card ────────────────────────────────────────────────────── */

function AgentCard({
  agent,
  isSelected,
  onClick,
}: {
  agent: AgentNode;
  isSelected: boolean;
  onClick: () => void;
}) {
  const Icon = agent.icon;
  const statusColor = agent.status === 'online' ? '#22c55e' : agent.status === 'idle' ? '#f59e0b' : '#ef4444';

  return (
    <motion.button
      onClick={onClick}
      whileHover={{ scale: 1.03 }}
      whileTap={{ scale: 0.98 }}
      style={{
        background: isSelected ? agent.bg : 'var(--color-surface)',
        border: `2px solid ${isSelected ? agent.color : 'var(--color-border)'}`,
        borderRadius: 'var(--radius-lg)',
        padding: '16px 20px',
        cursor: 'pointer',
        textAlign: 'left',
        width: agent.tier === 'supervisor' ? 220 : agent.tier === 'admin' ? 200 : 160,
        position: 'relative',
        transition: 'border-color 150ms ease, background 150ms ease',
        boxShadow: isSelected ? `0 0 20px ${agent.color}22` : 'none',
      }}
    >
      {/* Status dot */}
      <div style={{
        position: 'absolute', top: 10, right: 10,
        width: 8, height: 8, borderRadius: '50%',
        background: statusColor,
        boxShadow: `0 0 6px ${statusColor}`,
      }} />

      <div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 8 }}>
        <div style={{
          width: 36, height: 36, borderRadius: 'var(--radius-md)',
          background: agent.bg, display: 'flex', alignItems: 'center', justifyContent: 'center',
        }}>
          <Icon size={20} color={agent.color} />
        </div>
        <div>
          <div style={{ fontWeight: 700, fontSize: 15, color: 'var(--color-text)' }}>
            {agent.label}
          </div>
          <div style={{ fontSize: 11, color: 'var(--color-text-muted)', textTransform: 'uppercase', letterSpacing: '0.05em' }}>
            {agent.tier}
          </div>
        </div>
      </div>

      {/* Stats row */}
      <div style={{ display: 'flex', gap: 12, flexWrap: 'wrap' }}>
        {agent.stats.map(s => (
          <div key={s.label} style={{ fontSize: 11 }}>
            <span style={{ color: 'var(--color-text-muted)' }}>{s.label}: </span>
            <span style={{ color: 'var(--color-text-secondary)', fontWeight: 600 }}>{s.value}</span>
          </div>
        ))}
      </div>
    </motion.button>
  );
}

/* ─── Detail Panel ──────────────────────────────────────────────────── */

function DetailPanel({
  agent,
  onClose,
  onEnter,
}: {
  agent: AgentNode;
  onClose: () => void;
  onEnter: () => void;
}) {
  const Icon = agent.icon;
  const tierKey = agent.tier === 'admin' ? 'norma' : agent.tier === 'staff' ? 'staff' : agent.tier === 'public' ? 'public' : 'norma';
  const subAgents = SUB_AGENTS[tierKey] || [];

  return (
    <motion.div
      initial={{ x: 400, opacity: 0 }}
      animate={{ x: 0, opacity: 1 }}
      exit={{ x: 400, opacity: 0 }}
      transition={{ type: 'spring', damping: 25, stiffness: 300 }}
      style={{
        position: 'fixed', top: 0, right: 0, bottom: 0,
        width: 380, background: 'var(--color-surface)',
        borderLeft: '1px solid var(--color-border)',
        boxShadow: '-8px 0 30px rgba(0,0,0,0.08)',
        zIndex: 50, overflow: 'auto',
        padding: 28,
      }}
    >
      {/* Header */}
      <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: 24 }}>
        <div style={{ display: 'flex', gap: 14, alignItems: 'center' }}>
          <div style={{
            width: 48, height: 48, borderRadius: 'var(--radius-md)',
            background: agent.bg, display: 'flex', alignItems: 'center', justifyContent: 'center',
          }}>
            <Icon size={26} color={agent.color} />
          </div>
          <div>
            <h2 style={{ fontSize: 20, fontWeight: 700, margin: 0, color: 'var(--color-text)' }}>{agent.label}</h2>
            <span style={{
              fontSize: 11, textTransform: 'uppercase', letterSpacing: '0.08em',
              color: agent.color, fontWeight: 600,
            }}>{agent.tier} tier</span>
          </div>
        </div>
        <button onClick={onClose} style={{
          background: 'none', border: 'none', cursor: 'pointer', padding: 4,
          color: 'var(--color-text-muted)',
        }}>
          <X size={20} />
        </button>
      </div>

      {/* Description */}
      <p style={{ fontSize: 14, lineHeight: 1.6, color: 'var(--color-text-secondary)', margin: '0 0 24px' }}>
        {agent.description}
      </p>

      {/* Stats */}
      <div style={{
        display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 10, marginBottom: 24,
      }}>
        {agent.stats.map(s => (
          <div key={s.label} style={{
            background: 'var(--color-surface-el)', borderRadius: 'var(--radius-sm)',
            padding: '10px 12px', textAlign: 'center',
          }}>
            <div style={{ fontSize: 16, fontWeight: 700, color: agent.color }}>{s.value}</div>
            <div style={{ fontSize: 11, color: 'var(--color-text-muted)', marginTop: 2 }}>{s.label}</div>
          </div>
        ))}
      </div>

      {/* Status */}
      <div style={{
        display: 'flex', alignItems: 'center', gap: 8, marginBottom: 24,
        padding: '10px 14px', background: 'rgba(34,197,94,0.08)', borderRadius: 'var(--radius-sm)',
        border: '1px solid rgba(34,197,94,0.2)',
      }}>
        <Activity size={16} color="#22c55e" />
        <span style={{ fontSize: 13, fontWeight: 600, color: '#22c55e' }}>Online</span>
        <span style={{ fontSize: 12, color: 'var(--color-text-muted)', marginLeft: 'auto' }}>Port 7400</span>
      </div>

      {/* Sub-agents / modules */}
      {subAgents.length > 0 && (
        <>
          <h3 style={{ fontSize: 13, fontWeight: 700, color: 'var(--color-text-muted)', textTransform: 'uppercase', letterSpacing: '0.06em', margin: '0 0 12px' }}>
            Modules
          </h3>
          <div style={{ display: 'flex', flexDirection: 'column', gap: 6, marginBottom: 28 }}>
            {subAgents.map(sa => {
              const SaIcon = sa.icon;
              return (
                <div key={sa.label} style={{
                  display: 'flex', alignItems: 'center', gap: 10,
                  padding: '8px 12px', borderRadius: 'var(--radius-sm)',
                  background: 'var(--color-surface-el)',
                }}>
                  <SaIcon size={16} color={sa.color} />
                  <span style={{ fontSize: 13, color: 'var(--color-text)', fontWeight: 500 }}>{sa.label}</span>
                </div>
              );
            })}
          </div>
        </>
      )}

      {/* Enter button */}
      {agent.tier !== 'supervisor' && (
        <button
          onClick={onEnter}
          style={{
            width: '100%', padding: '14px 20px',
            background: agent.color, color: '#fff',
            border: 'none', borderRadius: 'var(--radius-md)',
            fontSize: 15, fontWeight: 700, cursor: 'pointer',
            display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 8,
            transition: 'opacity 150ms ease',
          }}
          onMouseEnter={e => (e.currentTarget.style.opacity = '0.9')}
          onMouseLeave={e => (e.currentTarget.style.opacity = '1')}
        >
          Enter {agent.label}
          <ArrowRight size={18} />
        </button>
      )}
    </motion.div>
  );
}

/* ─── Main Supervisor Page ──────────────────────────────────────────── */

export default function SupervisorPage() {
  const router = useRouter();
  const [orgs, setOrgs] = useState<OrgData[]>([]);
  const [agents, setAgents] = useState<AgentNode[]>([]);
  const [selectedId, setSelectedId] = useState<string | null>(null);
  const [loading, setLoading] = useState(true);

  // Escape key closes panel
  useEffect(() => {
    function handleKeyDown(e: KeyboardEvent) {
      if (e.key === 'Escape') setSelectedId(null);
    }
    document.addEventListener('keydown', handleKeyDown);
    return () => document.removeEventListener('keydown', handleKeyDown);
  }, []);

  useEffect(() => {
    (async () => {
      try {
        const res = await fetch('/api/orgs?limit=200&sort=name&sort_dir=asc');
        if (res.ok) {
          const data = await res.json();
          const orgRows = data.rows || [];
          setOrgs(orgRows);
          setAgents(buildAgentTree(orgRows));
        }
      } catch {
        // fallback with empty orgs
        setAgents(buildAgentTree([]));
      }
      setLoading(false);
    })();
  }, []);

  const selectedAgent = agents.find(a => a.id === selectedId) || null;
  const supervisorAgent = agents.find(a => a.id === 'supervisor');
  const normaAgent = agents.find(a => a.id === 'norma');
  const nikkiAgents = agents.filter(a => a.tier === 'staff');
  const pulseAgents = agents.filter(a => a.tier === 'public');

  function handleEnter(agent: AgentNode) {
    const url = agent.orgId ? `${agent.path}?org=${agent.orgId}` : agent.path;
    router.push(url);
  }

  if (loading) {
    return (
      <div style={{
        minHeight: '100vh', display: 'flex', alignItems: 'center', justifyContent: 'center',
        background: 'var(--color-bg)',
      }}>
        <div className="spinner" />
      </div>
    );
  }

  return (
    <div style={{
      minHeight: '100vh', background: 'var(--color-bg)',
      padding: '40px 32px', position: 'relative',
    }}>
      {/* Top-right global search — supervisors jump to any entity (Cmd+K) */}
      <div style={{
        position: 'absolute', top: 24, right: 32, zIndex: 40,
        display: 'flex', alignItems: 'center', gap: 8,
      }}>
        <GlobalSearch onNavigate={(tab, id) => {
          // Supervisor lives at /supervisor; entity tabs live in Norma admin shell (/).
          // Forward to the admin shell with the target tab + entity id as query params.
          const params = new URLSearchParams({ tab });
          if (id) params.set('id', id);
          router.push(`/?${params.toString()}`);
        }} />
      </div>

      {/* Page header */}
      <div style={{ maxWidth: 1200, margin: '0 auto 40px', textAlign: 'center' }}>
        <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 12, marginBottom: 8 }}>
          <Eye size={28} color="#f59e0b" />
          <h1 style={{ fontSize: 28, fontWeight: 800, color: 'var(--color-text)', margin: 0 }}>
            Supervisor Agent
          </h1>
        </div>
        <p style={{ fontSize: 15, color: 'var(--color-text-secondary)', maxWidth: 500, margin: '0 auto' }}>
          System overview. Click any agent node to inspect, then enter its dashboard.
        </p>
      </div>

      {/* Org Chart Flow */}
      <div style={{ maxWidth: 1200, margin: '0 auto' }}>
        {/* Level 0: Supervisor */}
        <div style={{ display: 'flex', justifyContent: 'center', marginBottom: 0 }}>
          {supervisorAgent && (
            <AgentCard
              agent={supervisorAgent}
              isSelected={selectedId === 'supervisor'}
              onClick={() => setSelectedId(selectedId === 'supervisor' ? null : 'supervisor')}
            />
          )}
        </div>

        {/* Connector */}
        <FlowLine from="supervisor" to="norma" />

        {/* Level 1: Norma (Admin) */}
        <div style={{ display: 'flex', justifyContent: 'center', marginBottom: 0 }}>
          {normaAgent && (
            <AgentCard
              agent={normaAgent}
              isSelected={selectedId === 'norma'}
              onClick={() => setSelectedId(selectedId === 'norma' ? null : 'norma')}
            />
          )}
        </div>

        {/* Connector fan-out */}
        <div style={{ display: 'flex', justifyContent: 'center' }}>
          <div style={{
            width: Math.min(nikkiAgents.length * 180, 1000),
            height: 32,
            borderBottom: '2px solid var(--color-border)',
            borderLeft: '2px solid var(--color-border)',
            borderRight: '2px solid var(--color-border)',
            borderRadius: '0 0 var(--radius-md) var(--radius-md)',
            position: 'relative',
          }}>
            {/* Vertical line from Norma */}
            <div style={{
              position: 'absolute', top: -2, left: '50%', transform: 'translateX(-50%)',
              width: 2, height: 2, background: 'var(--color-border)',
            }} />
          </div>
        </div>

        {/* Level 2+3: Paired Nikki → Pulse columns per org */}
        <div style={{
          display: 'flex', justifyContent: 'center', gap: 20, flexWrap: 'wrap',
        }}>
          {nikkiAgents.map(agent => {
            const pulseAgent = pulseAgents.find(p => p.orgId === agent.orgId);
            return (
              <div key={agent.id} style={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
                {/* Nikki card */}
                <AgentCard
                  agent={agent}
                  isSelected={selectedId === agent.id}
                  onClick={() => setSelectedId(selectedId === agent.id ? null : agent.id)}
                />
                {/* Vertical connector */}
                {pulseAgent && (
                  <>
                    <div style={{
                      width: 2, height: 28,
                      background: `linear-gradient(to bottom, ${agent.color}40, ${pulseAgent.color}40)`,
                    }} />
                    {/* Pulse card */}
                    <AgentCard
                      agent={pulseAgent}
                      isSelected={selectedId === pulseAgent.id}
                      onClick={() => setSelectedId(selectedId === pulseAgent.id ? null : pulseAgent.id)}
                    />
                  </>
                )}
              </div>
            );
          })}
        </div>
      </div>

      {/* Detail panel slideout */}
      <AnimatePresence>
        {selectedAgent && (
          <>
            {/* Backdrop */}
            <motion.div
              initial={{ opacity: 0 }}
              animate={{ opacity: 1 }}
              exit={{ opacity: 0 }}
              onClick={() => setSelectedId(null)}
              style={{
                position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.15)',
                zIndex: 40,
              }}
            />
            <DetailPanel
              agent={selectedAgent}
              onClose={() => setSelectedId(null)}
              onEnter={() => handleEnter(selectedAgent)}
            />
          </>
        )}
      </AnimatePresence>
    </div>
  );
}