← back to Norma

components/collaborations/CollaborationsTab.tsx

499 lines

'use client';

import { useState, useEffect, useCallback } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import {
  Users,
  Building2,
  Landmark,
  MapPin,
  ExternalLink,
  Mail,
  Phone,
  Search,
  RefreshCw,
  ChevronDown,
  ChevronUp,
  Sparkles,
  MessageSquare,
  CheckCircle2,
  Clock,
  Send,
  Globe,
  BadgeCheck,
} from 'lucide-react';
import { useToast } from '../ToastProvider';
import { useDebounce } from '@/hooks/useDebounce';
import { SkeletonList } from '../Skeleton';
import SortDropdown from '../shared/SortDropdown';
import { useClientSort, SortConfig } from '@/hooks/useClientSort';

const COLLAB_SORT_OPTIONS = [
  { value: 'name_asc', label: 'Name A-Z' },
  { value: 'name_desc', label: 'Name Z-A' },
  { value: 'type', label: 'Type A-Z' },
  { value: 'date_desc', label: 'Newest First' },
  { value: 'date_asc', label: 'Oldest First' },
];

const COLLAB_SORT_CONFIGS: Record<string, SortConfig> = {
  name_asc: { key: 'name', direction: 'asc', type: 'string' },
  name_desc: { key: 'name', direction: 'desc', type: 'string' },
  type: { key: 'collab_type', direction: 'asc', type: 'string' },
  date_desc: { key: 'created_at', direction: 'desc', type: 'date' },
  date_asc: { key: 'created_at', direction: 'asc', type: 'date' },
};

/* ─── Types ──────────────────────────────────────────────────────────────── */
type CollabType = 'nonprofit' | 'politician' | 'corporation' | 'municipality';

interface Collaboration {
  id: string;
  collab_type: CollabType;
  name: string;
  title: string | null;
  organization: string | null;
  website_url: string | null;
  email: string | null;
  phone: string | null;
  district: string | null;
  state: string | null;
  city: string | null;
  party: string | null;
  committees: string[] | null;
  focus_areas: string[] | null;
  ai_reason: string | null;
  ai_relevance: number | null;
  ai_talking_points: string[] | null;
  status: string;
  notes: string | null;
  last_contacted: string | null;
  outreach_letter: string | null;
  created_at: string;
}

/* ─── Sub-tab config ─────────────────────────────────────────────────────── */
const SUB_TABS: Array<{ id: CollabType; label: string; Icon: React.ElementType; color: string }> = [
  { id: 'nonprofit',     label: 'Non-Profits',    Icon: Users,     color: '#10b981' },
  { id: 'politician',    label: 'Politicians',     Icon: Landmark,  color: '#6366f1' },
  { id: 'corporation',   label: 'Corporations',    Icon: Building2, color: '#f59e0b' },
  { id: 'municipality',  label: 'Municipalities',  Icon: MapPin,    color: '#ec4899' },
];

const STATUS_COLORS: Record<string, string> = {
  suggested: '#94a3b8',
  contacted: '#34d399',
  in_discussion: '#fbbf24',
  partnered: '#34d399',
  declined: '#f87171',
  archived: '#6b7280',
};

/* ─── Helpers ────────────────────────────────────────────────────────────── */
function relevanceBar(score: number | null) {
  if (score === null || score === undefined) return null;
  const pct = Math.round(score * 100);
  const color = pct >= 80 ? '#34d399' : pct >= 60 ? '#fbbf24' : '#f87171';
  return (
    <div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
      <div style={{ width: 60, height: 6, borderRadius: 3, backgroundColor: 'rgba(255,255,255,0.1)' }}>
        <div style={{ width: pct + '%', height: '100%', borderRadius: 3, backgroundColor: color, transition: 'width 0.3s' }} />
      </div>
      <span style={{ fontSize: 11, color, fontWeight: 600 }}>{pct}%</span>
    </div>
  );
}

/* ─── Collaboration Card ─────────────────────────────────────────────────── */
function CollabCard({ collab, tabColor }: { collab: Collaboration; tabColor: string }) {
  const [expanded, setExpanded] = useState(false);

  return (
    <motion.div
      initial={{ opacity: 0, y: 8 }}
      animate={{ opacity: 1, y: 0 }}
      transition={{ duration: 0.2 }}
      style={{
        backgroundColor: 'var(--color-surface-el)',
        border: '1px solid var(--color-border)',
        borderRadius: 12,
        padding: '16px 20px',
        cursor: 'pointer',
      }}
      onClick={function() { setExpanded(function(e) { return !e; }); }}
    >
      {/* Header Row */}
      <div style={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', gap: 12 }}>
        <div style={{ flex: 1 }}>
          <div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 4 }}>
            <h3 style={{ fontSize: 15, fontWeight: 600, color: 'var(--color-text)', margin: 0 }}>
              {collab.name}
            </h3>
            {collab.party && (
              <span style={{
                fontSize: 10, fontWeight: 700, padding: '2px 6px', borderRadius: 4,
                backgroundColor: collab.party === 'D' || collab.party === 'Democrat' ? 'rgba(99,102,241,0.2)' : collab.party === 'R' || collab.party === 'Republican' ? 'rgba(239,68,68,0.2)' : 'rgba(156,163,175,0.2)',
                color: collab.party === 'D' || collab.party === 'Democrat' ? '#34d399' : collab.party === 'R' || collab.party === 'Republican' ? '#f87171' : '#9ca3af',
              }}>
                {collab.party}
              </span>
            )}
          </div>

          {(collab.title || collab.organization) && (
            <p style={{ fontSize: 12, color: 'var(--color-text-muted)', margin: '0 0 4px' }}>
              {[collab.title, collab.organization].filter(Boolean).join(' at ')}
            </p>
          )}

          {(collab.state || collab.district || collab.city) && (
            <p style={{ fontSize: 11, color: 'var(--color-text-muted)', margin: 0, display: 'flex', alignItems: 'center', gap: 4 }}>
              <MapPin size={11} />
              {[collab.city, collab.state, collab.district].filter(Boolean).join(', ')}
            </p>
          )}
        </div>

        <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-end', gap: 6 }}>
          {/* Status badge */}
          <span style={{
            fontSize: 10, fontWeight: 600, padding: '2px 8px', borderRadius: 10,
            backgroundColor: (STATUS_COLORS[collab.status] || '#6b7280') + '22',
            color: STATUS_COLORS[collab.status] || '#6b7280',
            textTransform: 'capitalize',
          }}>
            {collab.status.replace('_', ' ')}
          </span>
          {/* Relevance */}
          {relevanceBar(collab.ai_relevance)}
        </div>
      </div>

      {/* Focus Areas */}
      {collab.focus_areas && collab.focus_areas.length > 0 && (
        <div style={{ display: 'flex', flexWrap: 'wrap', gap: 4, marginTop: 8 }}>
          {collab.focus_areas.map(function(area, i) {
            return (
              <span key={i} style={{
                fontSize: 10, padding: '2px 8px', borderRadius: 10,
                backgroundColor: tabColor + '18',
                color: tabColor,
                fontWeight: 500,
              }}>
                {area}
              </span>
            );
          })}
        </div>
      )}

      {/* AI Reason (always visible) */}
      {collab.ai_reason && (
        <p style={{ fontSize: 12, color: 'var(--color-text-secondary)', margin: '8px 0 0', lineHeight: 1.5 }}>
          <Sparkles size={12} style={{ display: 'inline', marginRight: 4, color: tabColor }} />
          {collab.ai_reason}
        </p>
      )}

      {/* Expanded Details */}
      <AnimatePresence>
        {expanded && (
          <motion.div
            initial={{ height: 0, opacity: 0 }}
            animate={{ height: 'auto', opacity: 1 }}
            exit={{ height: 0, opacity: 0 }}
            transition={{ duration: 0.2 }}
            style={{ overflow: 'hidden' }}
          >
            <div style={{ borderTop: '1px solid var(--color-border)', marginTop: 12, paddingTop: 12 }}>
              {/* Talking Points */}
              {collab.ai_talking_points && collab.ai_talking_points.length > 0 && (
                <div style={{ marginBottom: 12 }}>
                  <h4 style={{ fontSize: 11, fontWeight: 600, color: 'var(--color-text-muted)', marginBottom: 6, textTransform: 'uppercase', letterSpacing: '0.05em' }}>
                    <MessageSquare size={12} style={{ display: 'inline', marginRight: 4 }} />
                    Talking Points
                  </h4>
                  <ul style={{ margin: 0, paddingLeft: 16 }}>
                    {collab.ai_talking_points.map(function(pt, i) {
                      return <li key={i} style={{ fontSize: 12, color: 'var(--color-text-secondary)', lineHeight: 1.6 }}>{pt}</li>;
                    })}
                  </ul>
                </div>
              )}

              {/* Action Buttons */}
              <div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
                {collab.website_url && (
                  <a
                    href={collab.website_url}
                    target="_blank"
                    rel="noopener noreferrer"
                    onClick={function(e) { e.stopPropagation(); }}
                    style={{
                      fontSize: 11, fontWeight: 600, padding: '6px 12px', borderRadius: 6,
                      backgroundColor: tabColor + '22', color: tabColor,
                      textDecoration: 'none', display: 'flex', alignItems: 'center', gap: 4,
                    }}
                  >
                    <Globe size={12} /> Website
                  </a>
                )}
                {collab.email && (
                  <a
                    href={'mailto:' + collab.email}
                    onClick={function(e) { e.stopPropagation(); }}
                    style={{
                      fontSize: 11, fontWeight: 600, padding: '6px 12px', borderRadius: 6,
                      backgroundColor: 'rgba(52,211,153,0.15)', color: '#34d399',
                      textDecoration: 'none', display: 'flex', alignItems: 'center', gap: 4,
                    }}
                  >
                    <Mail size={12} /> Email
                  </a>
                )}
                {collab.phone && (
                  <a
                    href={'tel:' + collab.phone}
                    onClick={function(e) { e.stopPropagation(); }}
                    style={{
                      fontSize: 11, fontWeight: 600, padding: '6px 12px', borderRadius: 6,
                      backgroundColor: 'rgba(52,211,153,0.15)', color: '#34d399',
                      textDecoration: 'none', display: 'flex', alignItems: 'center', gap: 4,
                    }}
                  >
                    <Phone size={12} /> Call
                  </a>
                )}
              </div>
            </div>
          </motion.div>
        )}
      </AnimatePresence>

      {/* Expand indicator */}
      <div style={{ display: 'flex', justifyContent: 'center', marginTop: 8 }}>
        {expanded ? <ChevronUp size={14} style={{ color: 'var(--color-text-muted)' }} /> : <ChevronDown size={14} style={{ color: 'var(--color-text-muted)' }} />}
      </div>
    </motion.div>
  );
}

/* ─── Main Tab ───────────────────────────────────────────────────────────── */
export default function CollaborationsTab() {
  const { addToast } = useToast();
  const [activeType, setActiveType] = useState<CollabType>('nonprofit');
  const [collabs, setCollabs] = useState<Collaboration[]>([]);
  const [loading, setLoading] = useState(false);
  const [discovering, setDiscovering] = useState(false);
  const [searchQuery, setSearchQuery] = useState('');
  const debouncedSearch = useDebounce(searchQuery, 300);
  const [sortBy, setSortBy] = useState('name_asc');
  const [stats, setStats] = useState<Record<CollabType, number>>({
    nonprofit: 0, politician: 0, corporation: 0, municipality: 0,
  });

  const fetchCollabs = useCallback(async function() {
    setLoading(true);
    try {
      const res = await fetch('/api/collaborations?type=' + activeType);
      const data = await res.json();
      setCollabs(data.collaborations || []);
    } catch {
      setCollabs([]);
    }
    setLoading(false);
  }, [activeType]);

  const fetchStats = useCallback(async function() {
    const types: CollabType[] = ['nonprofit', 'politician', 'corporation', 'municipality'];
    const counts: Record<string, number> = {};
    for (const t of types) {
      try {
        const res = await fetch('/api/collaborations?type=' + t);
        const data = await res.json();
        counts[t] = (data.collaborations || []).length;
      } catch {
        counts[t] = 0;
      }
    }
    setStats(counts as Record<CollabType, number>);
  }, []);

  useEffect(function() { fetchCollabs(); }, [fetchCollabs]);
  useEffect(function() { fetchStats(); }, [fetchStats]);

  async function handleDiscover() {
    setDiscovering(true);
    try {
      const res = await fetch('/api/collaborations/discover', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ type: activeType }),
      });
      const data = await res.json();
      if (data.success) {
        await fetchCollabs();
        await fetchStats();
        addToast('New collaborations discovered', 'success');
      }
    } catch {
      addToast('Discovery failed', 'error');
    }
    setDiscovering(false);
  }

  const sortedCollabs = useClientSort(collabs, sortBy, COLLAB_SORT_CONFIGS);
  const filtered = sortedCollabs.filter(function(c) {
    if (!debouncedSearch) return true;
    const q = debouncedSearch.toLowerCase();
    return (
      c.name.toLowerCase().includes(q) ||
      (c.organization || '').toLowerCase().includes(q) ||
      (c.state || '').toLowerCase().includes(q) ||
      (c.ai_reason || '').toLowerCase().includes(q)
    );
  });

  const activeTab = SUB_TABS.find(function(t) { return t.id === activeType; });
  const tabColor = activeTab ? activeTab.color : '#34d399';

  return (
    <div style={{ padding: '24px 28px', maxWidth: 1100, margin: '0 auto' }}>
      {/* Header */}
      <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 20 }}>
        <div>
          <h2 style={{ fontSize: 22, fontWeight: 700, color: 'var(--color-text)', margin: 0 }}>
            Collaborations
          </h2>
          <p style={{ fontSize: 13, color: 'var(--color-text-muted)', margin: '4px 0 0' }}>
            Discover and manage partnerships to amplify advocacy efforts
          </p>
        </div>
        <button
          onClick={handleDiscover}
          disabled={discovering}
          style={{
            display: 'flex', alignItems: 'center', gap: 6,
            padding: '8px 16px', borderRadius: 8, border: 'none',
            backgroundColor: tabColor, color: '#fff',
            fontSize: 13, fontWeight: 600, cursor: discovering ? 'not-allowed' : 'pointer',
            opacity: discovering ? 0.7 : 1,
          }}
        >
          {discovering ? (
            <RefreshCw size={14} style={{ animation: 'spin 1s linear infinite' }} />
          ) : (
            <Sparkles size={14} />
          )}
          {discovering ? 'Discovering...' : 'AI Discover'}
        </button>
      </div>

      {/* Sub-tab pills */}
      <div style={{ display: 'flex', gap: 8, marginBottom: 20, flexWrap: 'wrap' }}>
        {SUB_TABS.map(function(tab) {
          const isActive = activeType === tab.id;
          const Icon = tab.Icon;
          return (
            <button
              key={tab.id}
              onClick={function() { setActiveType(tab.id); }}
              style={{
                display: 'flex', alignItems: 'center', gap: 6,
                padding: '8px 16px', borderRadius: 20, border: 'none',
                backgroundColor: isActive ? tab.color + '22' : 'var(--color-surface-el)',
                color: isActive ? tab.color : 'var(--color-text-secondary)',
                fontSize: 13, fontWeight: isActive ? 600 : 500,
                cursor: 'pointer',
                transition: 'all 0.15s',
              }}
            >
              <Icon size={14} />
              {tab.label}
              <span style={{
                fontSize: 10, fontWeight: 700, padding: '1px 6px', borderRadius: 10,
                backgroundColor: isActive ? tab.color + '33' : 'rgba(255,255,255,0.06)',
                color: isActive ? tab.color : 'var(--color-text-muted)',
              }}>
                {stats[tab.id]}
              </span>
            </button>
          );
        })}
      </div>

      {/* Search */}
      <div style={{
        display: 'flex', alignItems: 'center', gap: 8,
        backgroundColor: 'var(--color-surface-el)', borderRadius: 8,
        padding: '8px 12px', marginBottom: 16,
        border: '1px solid var(--color-border)',
      }}>
        <SortDropdown options={COLLAB_SORT_OPTIONS} value={sortBy} onChange={setSortBy} />
        <Search size={14} style={{ color: 'var(--color-text-muted)', flexShrink: 0 }} />
        <input
          type="text"
          placeholder={'Search ' + (activeTab ? activeTab.label.toLowerCase() : '') + '...'}
          value={searchQuery}
          onChange={function(e) { setSearchQuery(e.target.value); }}
          aria-label="Search collaborations"
          style={{
            flex: 1, border: 'none', background: 'transparent',
            color: 'var(--color-text)', fontSize: 13, outline: 'none',
          }}
        />
      </div>

      {/* Content */}
      {loading ? (
        <SkeletonList count={4} />
      ) : filtered.length === 0 ? (
        <div style={{
          textAlign: 'center', padding: '48px 20px',
          backgroundColor: 'var(--color-surface-el)',
          borderRadius: 12, border: '1px solid var(--color-border)',
        }}>
          <div style={{
            width: 48, height: 48, borderRadius: 12,
            backgroundColor: tabColor + '15',
            display: 'flex', alignItems: 'center', justifyContent: 'center',
            margin: '0 auto 12px',
          }}>
            {activeTab && <activeTab.Icon size={22} style={{ color: tabColor }} />}
          </div>
          <h3 style={{ fontSize: 16, fontWeight: 600, color: 'var(--color-text)', margin: '0 0 4px' }}>
            No {activeTab ? activeTab.label : 'collaborations'} yet
          </h3>
          <p style={{ fontSize: 13, color: 'var(--color-text-muted)', margin: '0 0 16px' }}>
            Click &ldquo;AI Discover&rdquo; to find potential {activeTab ? activeTab.label.toLowerCase() : 'partners'} for your organization
          </p>
          <button
            onClick={handleDiscover}
            disabled={discovering}
            style={{
              display: 'inline-flex', alignItems: 'center', gap: 6,
              padding: '10px 20px', borderRadius: 8, border: 'none',
              backgroundColor: tabColor, color: '#fff',
              fontSize: 13, fontWeight: 600, cursor: 'pointer',
            }}
          >
            <Sparkles size={14} />
            Discover {activeTab ? activeTab.label : 'Partners'}
          </button>
        </div>
      ) : (
        <div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
          <AnimatePresence>
            {filtered.map(function(collab) {
              return <CollabCard key={collab.id} collab={collab} tabColor={tabColor} />;
            })}
          </AnimatePresence>
        </div>
      )}

      {/* Spin animation */}
      <style>{'\n@keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }\n'}</style>
    </div>
  );
}