← back to Norma

components/contacts/ContactsTab.tsx

1230 lines

'use client';

import { useState, useEffect, useCallback, useRef } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import {
  Search, Upload, Sparkles, ChevronRight, Briefcase, GraduationCap,
  Users, Globe, MapPin, Tag, ExternalLink, RefreshCw, X, Building2,
  Clock, UserCircle, FileText, Loader2, CheckCircle2, AlertCircle,
  ChevronDown, ChevronUp, Link2, ArrowRight,
} from 'lucide-react';
import { useToast } from '../ToastProvider';
import ResizableModal from '../shared/ResizableModal';

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

interface Contact {
  id: string;
  first_name: string;
  last_name: string;
  email: string | null;
  company: string | null;
  position: string | null;
  headline: string | null;
  location: string | null;
  linkedin_url: string | null;
  website: string | null;
  summary: string | null;
  skills: string[];
  groups: string[];
  tags: string[];
  enrichment_status: string;
  enrichment_source: string | null;
  enriched_at: string | null;
  matched_org_id: string | null;
  matched_org_name: string | null;
  relevance_score: number | null;
  connected_on: string | null;
  created_at: string;
}

interface WorkHistory {
  id: string;
  company_name: string;
  title: string | null;
  start_date: string | null;
  end_date: string | null;
  is_current: boolean;
  description: string | null;
  location: string | null;
  matched_org_name: string | null;
}

interface Education {
  id: string;
  school: string;
  degree: string | null;
  field_of_study: string | null;
  start_year: number | null;
  end_year: number | null;
}

interface ContactGroup {
  id: string;
  group_name: string;
  group_type: string;
}

interface Coworker {
  id: string;
  first_name: string;
  last_name: string;
  company: string | null;
  headline: string | null;
}

interface ContactDetail {
  contact: Contact;
  work_history: WorkHistory[];
  education: Education[];
  groups: ContactGroup[];
  coworkers: Coworker[];
  links: Array<{ linked_type: string; linked_id: string; link_type: string; weight: number }>;
}

interface EnrichStats {
  total: string;
  pending: string;
  enriched: string;
  failed: string;
  org_matched: string;
  total_work_records: string;
  total_edu_records: string;
  total_groups: string;
}

interface RelatedContact {
  id: string;
  first_name: string;
  last_name: string;
  headline: string | null;
  company: string | null;
  location: string | null;
  linkedin_url: string | null;
  // for coworkers
  role_at_org?: string | null;
  start_date?: string | null;
  end_date?: string | null;
  is_current?: boolean;
  // for alumni
  degree?: string | null;
  field_of_study?: string | null;
  start_year?: number | null;
  end_year?: number | null;
  // for skills
  skills?: string[];
}

/* ─── Helpers ────────────────────────────────────────────────────────────── */

function formatDate(d: string | null): string {
  if (!d) return '';
  try {
    return new Date(d).toLocaleDateString('en-US', { year: 'numeric', month: 'short' });
  } catch { return ''; }
}

function yearFromDate(d: string | null): string {
  if (!d) return '';
  try { return new Date(d).getFullYear().toString(); } catch { return ''; }
}

const STATUS_COLORS: Record<string, { bg: string; text: string }> = {
  pending:  { bg: 'rgba(234, 179, 8, 0.15)',  text: '#eab308' },
  enriched: { bg: 'rgba(34, 197, 94, 0.15)',   text: '#22c55e' },
  failed:   { bg: 'rgba(239, 68, 68, 0.15)',   text: '#ef4444' },
};

function avatarGradient(seed: string) {
  const gradients = [
    'linear-gradient(135deg, #f472b6, #ec4899)',
    'linear-gradient(135deg, #a78bfa, #7c3aed)',
    'linear-gradient(135deg, #34d399, #059669)',
    'linear-gradient(135deg, #60a5fa, #2563eb)',
    'linear-gradient(135deg, #fb923c, #ea580c)',
    'linear-gradient(135deg, #f87171, #dc2626)',
  ];
  const idx = (seed.charCodeAt(0) + seed.charCodeAt(1)) % gradients.length;
  return gradients[idx];
}

/* ─── Slide Panel (co-workers / related) ─────────────────────────────────── */

interface SlidePanelProps {
  title: string;
  subtitle?: string;
  items: RelatedContact[];
  total: number;
  loading: boolean;
  onClose: () => void;
  onSelectContact: (id: string) => void;
  mode: 'coworkers' | 'skill' | 'school' | 'group';
}

function SlidePanel({ title, subtitle, items, total, loading, onClose, onSelectContact, mode }: SlidePanelProps) {
  const modeColors: Record<string, { accent: string; bg: string }> = {
    coworkers: { accent: '#10b981', bg: 'rgba(16, 185, 129, 0.12)' },
    skill:     { accent: '#6366f1', bg: 'rgba(99, 102, 241, 0.12)' },
    school:    { accent: '#a855f7', bg: 'rgba(168, 85, 247, 0.12)' },
    group:     { accent: '#f59e0b', bg: 'rgba(245, 158, 11, 0.12)' },
  };
  const { accent, bg } = modeColors[mode] || modeColors.coworkers;

  return (
    <motion.div
      initial={{ x: '100%', opacity: 0 }}
      animate={{ x: 0, opacity: 1 }}
      exit={{ x: '100%', opacity: 0 }}
      transition={{ type: 'spring', stiffness: 340, damping: 30 }}
      style={{
        position: 'fixed',
        right: 0, top: 0, bottom: 0,
        width: 360,
        backgroundColor: 'var(--color-surface)',
        borderLeft: `1px solid var(--color-border)`,
        zIndex: 200,
        display: 'flex',
        flexDirection: 'column',
        boxShadow: '-8px 0 32px rgba(0,0,0,0.4)',
      }}
    >
      {/* Header */}
      <div style={{
        padding: '16px 20px',
        borderBottom: '1px solid var(--color-border)',
        display: 'flex',
        alignItems: 'flex-start',
        gap: 12,
      }}>
        <div style={{
          width: 36, height: 36, borderRadius: 8,
          backgroundColor: bg,
          display: 'flex', alignItems: 'center', justifyContent: 'center',
          flexShrink: 0,
        }}>
          {mode === 'coworkers' && <Building2 size={16} style={{ color: accent }} />}
          {mode === 'skill' && <Tag size={16} style={{ color: accent }} />}
          {mode === 'school' && <GraduationCap size={16} style={{ color: accent }} />}
          {mode === 'group' && <Users size={16} style={{ color: accent }} />}
        </div>
        <div style={{ flex: 1, minWidth: 0 }}>
          <div style={{ fontSize: 13, fontWeight: 700, color: 'var(--color-text)', lineHeight: 1.2 }}>{title}</div>
          {subtitle && <div style={{ fontSize: 11, color: 'var(--color-text-muted)', marginTop: 2, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{subtitle}</div>}
          {!loading && (
            <div style={{ fontSize: 10, color: accent, fontWeight: 600, marginTop: 3 }}>
              {total} {total === 1 ? 'match' : 'matches'}
            </div>
          )}
        </div>
        <button
          onClick={onClose}
          style={{
            width: 28, height: 28, borderRadius: 6, border: 'none',
            backgroundColor: 'var(--color-surface-el)', cursor: 'pointer',
            display: 'flex', alignItems: 'center', justifyContent: 'center',
            color: 'var(--color-text-muted)', flexShrink: 0,
          }}
        >
          <X size={14} />
        </button>
      </div>

      {/* Content */}
      <div style={{ flex: 1, overflowY: 'auto', padding: '8px 0' }}>
        {loading ? (
          <div style={{ padding: 32, textAlign: 'center', color: 'var(--color-text-muted)' }}>
            <Loader2 size={20} className="spinner" style={{ margin: '0 auto 8px' }} />
            <div style={{ fontSize: 12 }}>Searching...</div>
          </div>
        ) : items.length === 0 ? (
          <div style={{ padding: 32, textAlign: 'center', color: 'var(--color-text-muted)' }}>
            <Users size={28} style={{ margin: '0 auto 8px', opacity: 0.4 }} />
            <div style={{ fontSize: 13, fontWeight: 500 }}>No matches found</div>
            <div style={{ fontSize: 11, marginTop: 4 }}>No other contacts match this criteria</div>
          </div>
        ) : (
          items.map(person => (
            <div
              key={person.id}
              onClick={() => { onSelectContact(person.id); onClose(); }}
              style={{
                display: 'flex', alignItems: 'center', gap: 10,
                padding: '10px 20px', cursor: 'pointer', transition: 'background-color 0.15s',
              }}
              onMouseEnter={e => { e.currentTarget.style.backgroundColor = 'var(--color-surface-el)'; }}
              onMouseLeave={e => { e.currentTarget.style.backgroundColor = 'transparent'; }}
            >
              <div style={{
                width: 32, height: 32, borderRadius: '50%', flexShrink: 0,
                background: avatarGradient(person.first_name + person.last_name),
                display: 'flex', alignItems: 'center', justifyContent: 'center',
                color: '#fff', fontSize: 11, fontWeight: 700,
              }}>
                {person.first_name[0]}{person.last_name[0]}
              </div>
              <div style={{ flex: 1, minWidth: 0 }}>
                <div style={{ fontSize: 12, fontWeight: 600, color: 'var(--color-text)' }}>
                  {person.first_name} {person.last_name}
                </div>
                {person.headline && (
                  <div style={{ fontSize: 10, color: 'var(--color-text-muted)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
                    {person.headline}
                  </div>
                )}
                {/* Coworker overlap dates */}
                {mode === 'coworkers' && (person.start_date || person.end_date || person.is_current) && (
                  <div style={{ fontSize: 10, color: accent, marginTop: 1, display: 'flex', alignItems: 'center', gap: 3 }}>
                    <Clock size={9} />
                    {person.role_at_org && <span>{person.role_at_org} · </span>}
                    {yearFromDate(person.start_date || null)}
                    {person.start_date ? ' – ' : ''}
                    {person.is_current ? 'Present' : yearFromDate(person.end_date || null)}
                  </div>
                )}
                {/* Alumni graduation year */}
                {mode === 'school' && person.end_year && (
                  <div style={{ fontSize: 10, color: accent, marginTop: 1 }}>
                    {[person.degree, person.field_of_study].filter(Boolean).join(' in ')}
                    {person.end_year ? ` · Class of ${person.end_year}` : ''}
                  </div>
                )}
              </div>
              <ChevronRight size={13} style={{ color: 'var(--color-text-muted)', flexShrink: 0 }} />
            </div>
          ))
        )}
      </div>

      {/* Footer */}
      {!loading && items.length > 0 && total > items.length && (
        <div style={{
          padding: '10px 20px',
          borderTop: '1px solid var(--color-border)',
          fontSize: 11, color: 'var(--color-text-muted)', textAlign: 'center',
        }}>
          Showing {items.length} of {total}
        </div>
      )}
    </motion.div>
  );
}

/* ─── Clickable Tag with count badge ─────────────────────────────────────── */

interface ClickableTagProps {
  label: string;
  count: number | undefined;
  accentColor: string;
  accentBg: string;
  onClick: () => void;
}

function ClickableTag({ label, count, accentColor, accentBg, onClick }: ClickableTagProps) {
  const [hovered, setHovered] = useState(false);

  return (
    <button
      onClick={onClick}
      onMouseEnter={() => setHovered(true)}
      onMouseLeave={() => setHovered(false)}
      style={{
        display: 'inline-flex', alignItems: 'center', gap: 5,
        padding: '3px 8px 3px 8px',
        borderRadius: 6,
        border: `1px solid ${hovered ? accentColor : 'var(--color-border)'}`,
        backgroundColor: hovered ? accentBg : 'var(--color-surface-el)',
        color: hovered ? accentColor : 'var(--color-text-secondary)',
        fontSize: 11, fontWeight: 500, cursor: 'pointer',
        transition: 'all 0.15s ease',
        textDecoration: hovered ? 'underline' : 'none',
      }}
    >
      {label}
      {count !== undefined && count > 0 && (
        <span style={{
          fontSize: 9, fontWeight: 700, padding: '1px 4px', borderRadius: 10,
          backgroundColor: hovered ? accentColor : 'rgba(113,113,122,0.3)',
          color: hovered ? '#fff' : 'var(--color-text-muted)',
          minWidth: 16, textAlign: 'center',
          transition: 'all 0.15s ease',
        }}>
          {count}
        </span>
      )}
    </button>
  );
}

/* ─── Clickable Employer Row ─────────────────────────────────────────────── */

interface ClickableEmployerProps {
  wh: WorkHistory;
  coworkerCount: number | undefined;
  onClickEmployer: () => void;
}

function ClickableEmployer({ wh, coworkerCount, onClickEmployer }: ClickableEmployerProps) {
  const [hovered, setHovered] = useState(false);

  return (
    <div
      style={{ display: 'flex', alignItems: 'center', gap: 4, cursor: 'pointer' }}
      onClick={onClickEmployer}
      onMouseEnter={() => setHovered(true)}
      onMouseLeave={() => setHovered(false)}
      title={`See who else worked at ${wh.company_name}`}
    >
      <Building2 size={11} style={{ color: hovered ? '#10b981' : 'var(--color-text-muted)', transition: 'color 0.15s' }} />
      <span style={{
        fontSize: 12,
        color: hovered ? '#10b981' : 'var(--color-text-secondary)',
        textDecoration: hovered ? 'underline' : 'none',
        transition: 'color 0.15s',
        fontWeight: 500,
      }}>
        {wh.company_name}
      </span>
      {wh.matched_org_name && (
        <span style={{
          fontSize: 9, padding: '1px 5px', borderRadius: 3,
          backgroundColor: 'rgba(99,102,241,0.15)', color: '#6366f1', fontWeight: 600,
        }}>
          ORG LINKED
        </span>
      )}
      {coworkerCount !== undefined && coworkerCount > 0 && (
        <span style={{
          fontSize: 9, fontWeight: 700, padding: '1px 5px', borderRadius: 10,
          backgroundColor: hovered ? 'rgba(16,185,129,0.2)' : 'rgba(113,113,122,0.2)',
          color: hovered ? '#10b981' : 'var(--color-text-muted)',
          transition: 'all 0.15s ease',
        }}>
          {coworkerCount} colleague{coworkerCount !== 1 ? 's' : ''}
        </span>
      )}
    </div>
  );
}

/* ─── ContactsTab ────────────────────────────────────────────────────────── */

export default function ContactsTab() {
  const { addToast } = useToast();
  const [contacts, setContacts] = useState<Contact[]>([]);
  const [total, setTotal] = useState(0);
  const [loading, setLoading] = useState(true);
  const [search, setSearch] = useState('');
  const [statusFilter, setStatusFilter] = useState('');
  const [selectedId, setSelectedId] = useState<string | null>(null);
  const [detail, setDetail] = useState<ContactDetail | null>(null);
  const [detailLoading, setDetailLoading] = useState(false);
  const [stats, setStats] = useState<EnrichStats | null>(null);
  const [showImport, setShowImport] = useState(false);
  const [importing, setImporting] = useState(false);
  const [enriching, setEnriching] = useState(false);
  const fileRef = useRef<HTMLInputElement>(null);

  // Slide panel state
  const [slidePanel, setSlidePanel] = useState<{
    open: boolean;
    title: string;
    subtitle?: string;
    mode: 'coworkers' | 'skill' | 'school' | 'group';
    items: RelatedContact[];
    total: number;
    loading: boolean;
  }>({ open: false, title: '', mode: 'coworkers', items: [], total: 0, loading: false });

  /* ─── Fetch contacts ─────────────────────────────────────────────────── */
  const fetchContacts = useCallback(async () => {
    setLoading(true);
    try {
      const params = new URLSearchParams({ limit: '100' });
      if (search) params.set('q', search);
      if (statusFilter) params.set('enrichment_status', statusFilter);
      const res = await fetch(`/api/contacts?${params}`);
      const json = await res.json();
      setContacts(json.contacts || []);
      setTotal(json.total || 0);
    } catch { addToast('Failed to load contacts', 'error'); }
    setLoading(false);
  }, [search, statusFilter, addToast]);

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

  /* ─── Fetch stats ────────────────────────────────────────────────────── */
  useEffect(() => {
    fetch('/api/contacts/enrich')
      .then(r => r.json())
      .then(j => { if (j.stats) setStats(j.stats); })
      .catch(() => {});
  }, []);

  /* ─── Fetch detail ───────────────────────────────────────────────────── */
  const loadDetail = useCallback(async (id: string) => {
    setSelectedId(id);
    setDetailLoading(true);
    setSlidePanel(p => ({ ...p, open: false }));
    try {
      const res = await fetch(`/api/contacts/${id}`);
      const json = await res.json();
      setDetail(json);
    } catch { addToast('Failed to load contact detail', 'error'); }
    setDetailLoading(false);
  }, [addToast]);

  /* ─── Slide panel openers ────────────────────────────────────────────── */
  const openCoworkers = useCallback(async (wh: WorkHistory, contactId: string) => {
    const startYear = wh.start_date ? yearFromDate(wh.start_date) : '';
    const endYear = wh.is_current ? '' : (wh.end_date ? yearFromDate(wh.end_date) : '');

    setSlidePanel({
      open: true,
      title: `Colleagues at ${wh.company_name}`,
      subtitle: `Who else worked there${startYear ? ` ${startYear}–${endYear || 'Present'}` : ''}`,
      mode: 'coworkers',
      items: [],
      total: 0,
      loading: true,
    });

    try {
      const params = new URLSearchParams({ org: wh.company_name, exclude_id: contactId });
      if (startYear) params.set('start', startYear);
      if (endYear) params.set('end', endYear);
      const res = await fetch(`/api/contacts/coworkers?${params}`);
      const json = await res.json();
      setSlidePanel(p => ({ ...p, items: json.coworkers || [], total: json.total || 0, loading: false }));
    } catch {
      setSlidePanel(p => ({ ...p, loading: false }));
    }
  }, []);

  const openRelated = useCallback(async (type: 'skill' | 'school' | 'group', value: string, contactId: string) => {
    const titleMap = { skill: `Contacts with skill`, school: `Alumni of`, group: `Members of` };
    const modeMap: Record<string, 'skill' | 'school' | 'group'> = { skill: 'skill', school: 'school', group: 'group' };

    setSlidePanel({
      open: true,
      title: `${titleMap[type]}: ${value}`,
      subtitle: undefined,
      mode: modeMap[type],
      items: [],
      total: 0,
      loading: true,
    });

    try {
      const params = new URLSearchParams({ type, value, exclude_id: contactId });
      const res = await fetch(`/api/contacts/related?${params}`);
      const json = await res.json();
      setSlidePanel(p => ({ ...p, items: json.contacts || [], total: json.total || 0, loading: false }));
    } catch {
      setSlidePanel(p => ({ ...p, loading: false }));
    }
  }, []);

  /* ─── Import CSV ─────────────────────────────────────────────────────── */
  const handleImport = async (file: File) => {
    setImporting(true);
    try {
      const fd = new FormData();
      fd.append('file', file);
      const res = await fetch('/api/contacts/import', { method: 'POST', body: fd });
      const json = await res.json();
      if (res.ok) {
        addToast(`Imported ${json.imported} contacts (${json.skipped} skipped, ${json.org_matches} org matches)`, 'success');
        setShowImport(false);
        fetchContacts();
      } else {
        addToast(json.error || 'Import failed', 'error');
      }
    } catch { addToast('Import failed', 'error'); }
    setImporting(false);
  };

  /* ─── Enrich batch ───────────────────────────────────────────────────── */
  const handleEnrichBatch = async () => {
    setEnriching(true);
    try {
      const res = await fetch('/api/contacts/enrich', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ batch: true, limit: 10 }),
      });
      const json = await res.json();
      if (res.ok) {
        addToast(`Enriched ${json.enriched}/${json.total} contacts`, 'success');
        fetchContacts();
      } else {
        addToast(json.error || 'Enrichment failed', 'error');
      }
    } catch { addToast('Enrichment failed', 'error'); }
    setEnriching(false);
  };

  /* ─── Enrich single ──────────────────────────────────────────────────── */
  const handleEnrichSingle = async (contactId: string) => {
    try {
      const res = await fetch('/api/contacts/enrich', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ contact_id: contactId }),
      });
      const json = await res.json();
      if (res.ok && json.results?.[0]?.enriched) {
        addToast('Contact enriched successfully', 'success');
        loadDetail(contactId);
        fetchContacts();
      } else {
        addToast('Enrichment found no data', 'warning');
      }
    } catch { addToast('Enrichment failed', 'error'); }
  };

  /* ─── Render ─────────────────────────────────────────────────────────── */
  return (
    <div style={{ display: 'flex', height: '100%', overflow: 'hidden', position: 'relative' }}>
      {/* ── Left: Contact List ─────────────────────────────────────────── */}
      <div style={{
        width: 380, minWidth: 380, display: 'flex', flexDirection: 'column',
        borderRight: '1px solid var(--color-border)',
        backgroundColor: 'var(--color-surface)',
      }}>
        {/* Stats bar */}
        {stats && (
          <div style={{
            padding: '8px 16px', display: 'flex', gap: 16,
            borderBottom: '1px solid var(--color-border)',
            fontSize: 11, color: 'var(--color-text-muted)',
          }}>
            <span>{stats.total} total</span>
            <span style={{ color: '#eab308' }}>{stats.pending} pending</span>
            <span style={{ color: '#22c55e' }}>{stats.enriched} enriched</span>
            <span style={{ color: '#ef4444' }}>{stats.failed} failed</span>
          </div>
        )}

        {/* Search + actions */}
        <div style={{ padding: '12px 16px', display: 'flex', flexDirection: 'column', gap: 8 }}>
          <div style={{ display: 'flex', gap: 8 }}>
            <div style={{ flex: 1, position: 'relative' }}>
              <Search size={14} style={{ position: 'absolute', left: 10, top: '50%', transform: 'translateY(-50%)', color: 'var(--color-text-muted)' }} />
              <input
                className="input"
                placeholder="Search contacts..."
                value={search}
                onChange={e => setSearch(e.target.value)}
                style={{ paddingLeft: 32, fontSize: 13, height: 34 }}
              />
            </div>
            <select
              className="input"
              value={statusFilter}
              onChange={e => setStatusFilter(e.target.value)}
              style={{ width: 90, fontSize: 12, height: 34 }}
            >
              <option value="">All</option>
              <option value="pending">Pending</option>
              <option value="enriched">Enriched</option>
              <option value="failed">Failed</option>
            </select>
          </div>
          <div style={{ display: 'flex', gap: 8 }}>
            <button className="btn btn-primary btn-sm" onClick={() => setShowImport(true)} style={{ flex: 1, gap: 6 }}>
              <Upload size={13} /> Import CSV
            </button>
            <button
              className="btn btn-secondary btn-sm"
              onClick={handleEnrichBatch}
              disabled={enriching}
              style={{ flex: 1, gap: 6 }}
            >
              {enriching ? <Loader2 size={13} className="spinner" /> : <Sparkles size={13} />}
              Enrich All
            </button>
          </div>
        </div>

        {/* Contact list */}
        <div style={{ flex: 1, overflowY: 'auto', padding: '0 8px 8px' }}>
          {loading ? (
            <div style={{ padding: 32, textAlign: 'center', color: 'var(--color-text-muted)' }}>
              <Loader2 size={20} className="spinner" style={{ margin: '0 auto 8px' }} />
              Loading contacts...
            </div>
          ) : contacts.length === 0 ? (
            <div style={{ padding: 32, textAlign: 'center', color: 'var(--color-text-muted)' }}>
              <UserCircle size={32} style={{ margin: '0 auto 8px', opacity: 0.5 }} />
              <div style={{ fontSize: 13 }}>No contacts found</div>
              <div style={{ fontSize: 11, marginTop: 4 }}>Import a LinkedIn CSV to get started</div>
            </div>
          ) : (
            contacts.map(c => (
              <motion.div
                key={c.id}
                initial={{ opacity: 0, y: 4 }}
                animate={{ opacity: 1, y: 0 }}
                onClick={() => loadDetail(c.id)}
                style={{
                  padding: '10px 12px',
                  borderRadius: 8,
                  cursor: 'pointer',
                  marginBottom: 2,
                  backgroundColor: selectedId === c.id ? 'rgba(16, 185, 129, 0.12)' : 'transparent',
                  borderLeft: selectedId === c.id ? '3px solid #10b981' : '3px solid transparent',
                  transition: 'background-color 0.15s',
                }}
                onMouseEnter={e => { if (selectedId !== c.id) e.currentTarget.style.backgroundColor = 'var(--color-surface-el)'; }}
                onMouseLeave={e => { if (selectedId !== c.id) e.currentTarget.style.backgroundColor = 'transparent'; }}
              >
                <div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
                  <div style={{
                    width: 36, height: 36, borderRadius: '50%',
                    background: avatarGradient(c.first_name + c.last_name),
                    display: 'flex', alignItems: 'center', justifyContent: 'center',
                    color: '#fff', fontSize: 13, fontWeight: 700, flexShrink: 0,
                  }}>
                    {c.first_name[0]}{c.last_name[0]}
                  </div>
                  <div style={{ flex: 1, minWidth: 0 }}>
                    <div style={{ fontSize: 13, fontWeight: 600, color: 'var(--color-text)', lineHeight: 1.3, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
                      {c.first_name} {c.last_name}
                    </div>
                    {c.headline ? (
                      <div style={{ fontSize: 11, color: 'var(--color-text-secondary)', lineHeight: 1.3, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
                        {c.headline}
                      </div>
                    ) : c.company ? (
                      <div style={{ fontSize: 11, color: 'var(--color-text-secondary)', lineHeight: 1.3 }}>
                        {c.position ? `${c.position} at ` : ''}{c.company}
                      </div>
                    ) : null}
                  </div>
                  <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-end', gap: 4 }}>
                    <span style={{
                      fontSize: 9, fontWeight: 600, padding: '2px 6px', borderRadius: 4,
                      backgroundColor: STATUS_COLORS[c.enrichment_status]?.bg || 'rgba(113,113,122,0.15)',
                      color: STATUS_COLORS[c.enrichment_status]?.text || '#71717a',
                      textTransform: 'uppercase', letterSpacing: '0.04em',
                    }}>
                      {c.enrichment_status}
                    </span>
                    {c.matched_org_name && (
                      <span style={{ fontSize: 9, color: '#6366f1', display: 'flex', alignItems: 'center', gap: 2 }}>
                        <Building2 size={9} /> Org matched
                      </span>
                    )}
                  </div>
                </div>
              </motion.div>
            ))
          )}
          {total > contacts.length && (
            <div style={{ padding: '8px 12px', fontSize: 11, color: 'var(--color-text-muted)', textAlign: 'center' }}>
              Showing {contacts.length} of {total}
            </div>
          )}
        </div>
      </div>

      {/* ── Right: Detail Panel ────────────────────────────────────────── */}
      <div style={{ flex: 1, overflowY: 'auto', backgroundColor: 'var(--color-bg)' }}>
        {!selectedId ? (
          <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '100%', color: 'var(--color-text-muted)' }}>
            <div style={{ textAlign: 'center' }}>
              <Users size={40} style={{ margin: '0 auto 12px', opacity: 0.3 }} />
              <div style={{ fontSize: 14, fontWeight: 500 }}>Select a contact</div>
              <div style={{ fontSize: 12, marginTop: 4 }}>Choose from the list or import a CSV</div>
            </div>
          </div>
        ) : detailLoading ? (
          <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '100%' }}>
            <Loader2 size={24} className="spinner" style={{ color: 'var(--color-primary)' }} />
          </div>
        ) : detail ? (
          <ContactDetailPanel
            detail={detail}
            onEnrich={() => handleEnrichSingle(detail.contact.id)}
            onSelectContact={(id) => loadDetail(id)}
            onOpenCoworkers={(wh) => openCoworkers(wh, detail.contact.id)}
            onOpenRelated={(type, value) => openRelated(type, value, detail.contact.id)}
          />
        ) : null}
      </div>

      {/* ── Slide Panel Overlay ─────────────────────────────────────────── */}
      <AnimatePresence>
        {slidePanel.open && (
          <>
            {/* Backdrop */}
            <motion.div
              initial={{ opacity: 0 }}
              animate={{ opacity: 1 }}
              exit={{ opacity: 0 }}
              onClick={() => setSlidePanel(p => ({ ...p, open: false }))}
              style={{
                position: 'fixed', inset: 0, zIndex: 199,
                backgroundColor: 'rgba(0,0,0,0.4)',
              }}
            />
            <SlidePanel
              title={slidePanel.title}
              subtitle={slidePanel.subtitle}
              items={slidePanel.items}
              total={slidePanel.total}
              loading={slidePanel.loading}
              onClose={() => setSlidePanel(p => ({ ...p, open: false }))}
              onSelectContact={(id) => loadDetail(id)}
              mode={slidePanel.mode}
            />
          </>
        )}
      </AnimatePresence>

      {/* ── Import Modal ───────────────────────────────────────────────── */}
      {showImport && (
        <ResizableModal
          modalId="contacts-import-csv"
          title="Import LinkedIn CSV"
          onClose={() => setShowImport(false)}
          defaultWidth={520}
        >
          <div
            onClick={() => fileRef.current?.click()}
            style={{
              border: '2px dashed var(--color-border)',
              borderRadius: 12, padding: 32, textAlign: 'center', cursor: 'pointer',
              transition: 'border-color 0.15s',
            }}
            onMouseEnter={e => { e.currentTarget.style.borderColor = 'var(--color-primary)'; }}
            onMouseLeave={e => { e.currentTarget.style.borderColor = 'var(--color-border)'; }}
          >
            <Upload size={32} style={{ margin: '0 auto 8px', color: 'var(--color-text-muted)' }} />
            <div style={{ fontSize: 13, fontWeight: 500, color: 'var(--color-text)' }}>Click to upload CSV</div>
            <div style={{ fontSize: 11, color: 'var(--color-text-muted)', marginTop: 4 }}>
              LinkedIn Connections.csv (First Name, Last Name, Email, Company, Position)
            </div>
          </div>
          <input
            ref={fileRef}
            type="file"
            accept=".csv"
            style={{ display: 'none' }}
            onChange={e => {
              const file = e.target.files?.[0];
              if (file) handleImport(file);
            }}
          />
          {importing && (
            <div style={{ marginTop: 16, display: 'flex', alignItems: 'center', gap: 8, justifyContent: 'center', color: 'var(--color-primary)' }}>
              <Loader2 size={16} className="spinner" /> Importing...
            </div>
          )}
        </ResizableModal>
      )}
    </div>
  );
}

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

interface ContactCounts {
  [key: string]: number;
}

function ContactDetailPanel({
  detail,
  onEnrich,
  onSelectContact,
  onOpenCoworkers,
  onOpenRelated,
}: {
  detail: ContactDetail;
  onEnrich: () => void;
  onSelectContact: (id: string) => void;
  onOpenCoworkers: (wh: WorkHistory) => void;
  onOpenRelated: (type: 'skill' | 'school' | 'group', value: string) => void;
}) {
  const c = detail.contact;
  const [counts, setCounts] = useState<ContactCounts>({});
  const [countsLoading, setCountsLoading] = useState(false);

  // Load counts for all clickable elements
  useEffect(() => {
    const skills = c.skills || [];
    const schools = detail.education.map(e => e.school);
    const groups = detail.groups.map(g => g.group_name);
    const employers = detail.work_history.map(wh => ({
      company_name: wh.company_name,
      start_date: wh.start_date,
      end_date: wh.is_current ? null : wh.end_date,
    }));

    if (!skills.length && !schools.length && !groups.length && !employers.length) return;

    setCountsLoading(true);
    fetch('/api/contacts/counts', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ contact_id: c.id, skills, schools, groups, employers }),
    })
      .then(r => r.json())
      .then(j => { if (j.counts) setCounts(j.counts); })
      .catch(() => {})
      .finally(() => setCountsLoading(false));
  }, [c.id, c.skills, detail.education, detail.groups, detail.work_history]);

  return (
    <div style={{ padding: 24, maxWidth: 720 }}>
      {/* Header */}
      <div style={{ display: 'flex', alignItems: 'flex-start', gap: 16, marginBottom: 24 }}>
        <div style={{
          width: 56, height: 56, borderRadius: '50%',
          background: avatarGradient(c.first_name + c.last_name),
          display: 'flex', alignItems: 'center', justifyContent: 'center',
          color: '#fff', fontSize: 20, fontWeight: 700, flexShrink: 0,
        }}>
          {c.first_name[0]}{c.last_name[0]}
        </div>
        <div style={{ flex: 1 }}>
          <h2 style={{ fontSize: 20, fontWeight: 700, color: 'var(--color-text)', lineHeight: 1.2 }}>
            {c.first_name} {c.last_name}
          </h2>
          {c.headline && (
            <div style={{ fontSize: 13, color: 'var(--color-text-secondary)', marginTop: 2 }}>{c.headline}</div>
          )}
          <div style={{ display: 'flex', gap: 12, marginTop: 8, flexWrap: 'wrap' }}>
            {c.location && (
              <span style={{ fontSize: 11, color: 'var(--color-text-muted)', display: 'flex', alignItems: 'center', gap: 4 }}>
                <MapPin size={11} /> {c.location}
              </span>
            )}
            {c.company && (
              <span style={{ fontSize: 11, color: 'var(--color-text-muted)', display: 'flex', alignItems: 'center', gap: 4 }}>
                <Building2 size={11} /> {c.company}
              </span>
            )}
            {c.connected_on && (
              <span style={{ fontSize: 11, color: 'var(--color-text-muted)', display: 'flex', alignItems: 'center', gap: 4 }}>
                <Clock size={11} /> Connected {formatDate(c.connected_on)}
              </span>
            )}
          </div>
        </div>
        <div style={{ display: 'flex', gap: 8 }}>
          {c.linkedin_url && (
            <a href={c.linkedin_url} target="_blank" rel="noopener noreferrer" className="btn btn-ghost btn-sm" title="LinkedIn">
              <ExternalLink size={14} />
            </a>
          )}
          {c.enrichment_status !== 'enriched' && (
            <button className="btn btn-primary btn-sm" onClick={onEnrich} style={{ gap: 4 }}>
              <Sparkles size={13} /> Enrich
            </button>
          )}
        </div>
      </div>

      {/* Summary */}
      {c.summary && (
        <div className="card" style={{ padding: 16, marginBottom: 16 }}>
          <div style={{ fontSize: 12, fontWeight: 600, color: 'var(--color-text-muted)', marginBottom: 6 }}>SUMMARY</div>
          <div style={{ fontSize: 13, color: 'var(--color-text-secondary)', lineHeight: 1.5 }}>{c.summary}</div>
        </div>
      )}

      {/* Org Match */}
      {c.matched_org_name && (
        <div className="card" style={{ padding: 12, marginBottom: 16, borderLeft: '3px solid #6366f1' }}>
          <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
            <Building2 size={16} style={{ color: '#6366f1' }} />
            <div style={{ flex: 1 }}>
              <div style={{ fontSize: 12, fontWeight: 600, color: '#6366f1' }}>Matched Organization</div>
              <div style={{ fontSize: 13, color: 'var(--color-text)' }}>{c.matched_org_name}</div>
            </div>
            <span style={{
              fontSize: 9, fontWeight: 700, padding: '2px 8px', borderRadius: 4,
              backgroundColor: 'rgba(99,102,241,0.2)', color: '#6366f1',
              textTransform: 'uppercase', letterSpacing: '0.06em',
            }}>
              ORG LINKED
            </span>
          </div>
        </div>
      )}

      {/* Skills — clickable */}
      {c.skills && c.skills.length > 0 && (
        <div style={{ marginBottom: 16 }}>
          <div style={{
            fontSize: 12, fontWeight: 600, color: 'var(--color-text-muted)', marginBottom: 8,
            display: 'flex', alignItems: 'center', gap: 6,
          }}>
            <Tag size={12} /> SKILLS
            <span style={{ fontSize: 10, color: 'var(--color-text-muted)', fontWeight: 400 }}>
              — click to find contacts with same skill
            </span>
          </div>
          <div style={{ display: 'flex', flexWrap: 'wrap', gap: 6 }}>
            {c.skills.map((s, i) => (
              <ClickableTag
                key={i}
                label={s}
                count={counts[`skill:${s}`]}
                accentColor="#6366f1"
                accentBg="rgba(99,102,241,0.12)"
                onClick={() => onOpenRelated('skill', s)}
              />
            ))}
          </div>
        </div>
      )}

      {/* Work History Timeline — clickable employers */}
      {detail.work_history.length > 0 && (
        <div style={{ marginBottom: 16 }}>
          <div style={{
            fontSize: 12, fontWeight: 600, color: 'var(--color-text-muted)', marginBottom: 12,
            display: 'flex', alignItems: 'center', gap: 6,
          }}>
            <Briefcase size={12} /> WORK HISTORY ({detail.work_history.length})
            <span style={{ fontSize: 10, color: 'var(--color-text-muted)', fontWeight: 400 }}>
              — click employer to find colleagues
            </span>
          </div>
          <div style={{ position: 'relative', paddingLeft: 20 }}>
            {/* Timeline line */}
            <div style={{
              position: 'absolute', left: 7, top: 6, bottom: 6,
              width: 2, backgroundColor: 'var(--color-border)',
            }} />
            {detail.work_history.map((wh, i) => (
              <div key={wh.id} style={{ position: 'relative', marginBottom: i < detail.work_history.length - 1 ? 16 : 0 }}>
                {/* Dot */}
                <div style={{
                  position: 'absolute', left: -16, top: 5,
                  width: 10, height: 10, borderRadius: '50%',
                  backgroundColor: wh.is_current ? '#22c55e' : 'var(--color-surface-el)',
                  border: `2px solid ${wh.is_current ? '#22c55e' : 'var(--color-border)'}`,
                }} />
                <div className="card" style={{ padding: 12 }}>
                  <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>
                    <div>
                      <div style={{ fontSize: 13, fontWeight: 600, color: 'var(--color-text)' }}>
                        {wh.title || 'Role'}
                      </div>
                      <ClickableEmployer
                        wh={wh}
                        coworkerCount={counts[`employer:${wh.company_name}`]}
                        onClickEmployer={() => onOpenCoworkers(wh)}
                      />
                    </div>
                    <div style={{ fontSize: 11, color: 'var(--color-text-muted)', whiteSpace: 'nowrap' }}>
                      {yearFromDate(wh.start_date)}{wh.start_date ? ' – ' : ''}{wh.is_current ? 'Present' : yearFromDate(wh.end_date)}
                    </div>
                  </div>
                  {wh.description && (
                    <div style={{ fontSize: 11, color: 'var(--color-text-muted)', marginTop: 6, lineHeight: 1.4 }}>
                      {wh.description}
                    </div>
                  )}
                  {wh.location && (
                    <div style={{ fontSize: 10, color: 'var(--color-text-muted)', marginTop: 4, display: 'flex', alignItems: 'center', gap: 3 }}>
                      <MapPin size={9} /> {wh.location}
                    </div>
                  )}
                </div>
              </div>
            ))}
          </div>
        </div>
      )}

      {/* Education — clickable schools */}
      {detail.education.length > 0 && (
        <div style={{ marginBottom: 16 }}>
          <div style={{
            fontSize: 12, fontWeight: 600, color: 'var(--color-text-muted)', marginBottom: 8,
            display: 'flex', alignItems: 'center', gap: 6,
          }}>
            <GraduationCap size={12} /> EDUCATION ({detail.education.length})
            <span style={{ fontSize: 10, color: 'var(--color-text-muted)', fontWeight: 400 }}>
              — click school to find alumni
            </span>
          </div>
          {detail.education.map(edu => (
            <ClickableEducationCard
              key={edu.id}
              edu={edu}
              count={counts[`school:${edu.school}`]}
              onClick={() => onOpenRelated('school', edu.school)}
            />
          ))}
        </div>
      )}

      {/* Groups — clickable */}
      {detail.groups.length > 0 && (
        <div style={{ marginBottom: 16 }}>
          <div style={{
            fontSize: 12, fontWeight: 600, color: 'var(--color-text-muted)', marginBottom: 8,
            display: 'flex', alignItems: 'center', gap: 6,
          }}>
            <Users size={12} /> GROUPS ({detail.groups.length})
            <span style={{ fontSize: 10, color: 'var(--color-text-muted)', fontWeight: 400 }}>
              — click to find other members
            </span>
          </div>
          <div style={{ display: 'flex', flexWrap: 'wrap', gap: 6 }}>
            {detail.groups.map(g => (
              <ClickableTag
                key={g.id}
                label={g.group_name}
                count={counts[`group:${g.group_name}`]}
                accentColor="#f59e0b"
                accentBg="rgba(245,158,11,0.12)"
                onClick={() => onOpenRelated('group', g.group_name)}
              />
            ))}
          </div>
        </div>
      )}

      {/* Existing Coworkers (from contact record) */}
      {detail.coworkers.length > 0 && (
        <div style={{ marginBottom: 16 }}>
          <div style={{ fontSize: 12, fontWeight: 600, color: 'var(--color-text-muted)', marginBottom: 8, display: 'flex', alignItems: 'center', gap: 6 }}>
            <Users size={12} /> CONNECTIONS ({detail.coworkers.length})
          </div>
          <div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
            {detail.coworkers.map(cw => (
              <div
                key={cw.id}
                onClick={() => onSelectContact(cw.id)}
                style={{
                  display: 'flex', alignItems: 'center', gap: 10, padding: '8px 12px',
                  borderRadius: 8, cursor: 'pointer', transition: 'background-color 0.15s',
                }}
                onMouseEnter={e => { e.currentTarget.style.backgroundColor = 'var(--color-surface-el)'; }}
                onMouseLeave={e => { e.currentTarget.style.backgroundColor = 'transparent'; }}
              >
                <div style={{
                  width: 28, height: 28, borderRadius: '50%',
                  background: avatarGradient(cw.first_name + cw.last_name),
                  display: 'flex', alignItems: 'center', justifyContent: 'center',
                  color: '#fff', fontSize: 10, fontWeight: 700, flexShrink: 0,
                }}>
                  {cw.first_name[0]}{cw.last_name[0]}
                </div>
                <div style={{ flex: 1, minWidth: 0 }}>
                  <div style={{ fontSize: 12, fontWeight: 600, color: 'var(--color-text)' }}>
                    {cw.first_name} {cw.last_name}
                  </div>
                  {(cw.headline || cw.company) && (
                    <div style={{ fontSize: 10, color: 'var(--color-text-muted)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
                      {cw.headline || cw.company}
                    </div>
                  )}
                </div>
                <ChevronRight size={14} style={{ color: 'var(--color-text-muted)' }} />
              </div>
            ))}
          </div>
        </div>
      )}

      {/* Mind Map Links */}
      {detail.links && detail.links.length > 0 && (
        <div>
          <div style={{ fontSize: 12, fontWeight: 600, color: 'var(--color-text-muted)', marginBottom: 8, display: 'flex', alignItems: 'center', gap: 6 }}>
            <Globe size={12} /> MIND MAP CONNECTIONS ({detail.links.length})
          </div>
          <div style={{ display: 'flex', flexWrap: 'wrap', gap: 6 }}>
            {detail.links.map((l, i) => (
              <span key={i} className="badge" style={{
                fontSize: 10,
                backgroundColor: l.link_type === 'employed_at' ? 'rgba(99,102,241,0.15)' :
                  l.link_type === 'co_workers' ? 'rgba(244,114,182,0.15)' :
                  l.link_type === 'alumni' ? 'rgba(168,85,247,0.15)' :
                  'rgba(113,113,122,0.15)',
                color: l.link_type === 'employed_at' ? '#6366f1' :
                  l.link_type === 'co_workers' ? '#f472b6' :
                  l.link_type === 'alumni' ? '#a855f7' :
                  '#71717a',
              }}>
                {l.link_type.replace(/_/g, ' ')} → {l.linked_type}
              </span>
            ))}
          </div>
        </div>
      )}
    </div>
  );
}

/* ─── Clickable Education Card ───────────────────────────────────────────── */

function ClickableEducationCard({
  edu,
  count,
  onClick,
}: {
  edu: Education;
  count: number | undefined;
  onClick: () => void;
}) {
  const [hovered, setHovered] = useState(false);

  return (
    <div
      className="card"
      onClick={onClick}
      onMouseEnter={() => setHovered(true)}
      onMouseLeave={() => setHovered(false)}
      style={{
        padding: 12,
        marginBottom: 6,
        cursor: 'pointer',
        borderLeft: hovered ? '3px solid #a855f7' : '3px solid transparent',
        transition: 'all 0.15s ease',
        backgroundColor: hovered ? 'var(--color-surface-el)' : undefined,
      }}
      title={`Find alumni of ${edu.school}`}
    >
      <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>
        <div>
          <div style={{
            fontSize: 13, fontWeight: 600,
            color: hovered ? '#a855f7' : 'var(--color-text)',
            textDecoration: hovered ? 'underline' : 'none',
            transition: 'color 0.15s',
          }}>
            {edu.school}
          </div>
          <div style={{ fontSize: 12, color: 'var(--color-text-secondary)', marginTop: 2 }}>
            {[edu.degree, edu.field_of_study].filter(Boolean).join(' in ')}
            {edu.start_year && edu.end_year ? ` (${edu.start_year}–${edu.end_year})` : edu.end_year ? ` (${edu.end_year})` : ''}
          </div>
        </div>
        <div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
          {count !== undefined && count > 0 && (
            <span style={{
              fontSize: 9, fontWeight: 700, padding: '2px 6px', borderRadius: 10,
              backgroundColor: hovered ? 'rgba(168,85,247,0.2)' : 'rgba(113,113,122,0.2)',
              color: hovered ? '#a855f7' : 'var(--color-text-muted)',
              transition: 'all 0.15s ease',
            }}>
              {count} alumni
            </span>
          )}
          <GraduationCap
            size={13}
            style={{ color: hovered ? '#a855f7' : 'var(--color-text-muted)', transition: 'color 0.15s' }}
          />
        </div>
      </div>
    </div>
  );
}