← back to Patty

components/grants/GrantsTab.tsx

550 lines

'use client';

import { useState, useEffect, useCallback } from 'react';
import {
  DollarSign, Search, Sparkles, Loader2, ChevronDown, ChevronUp,
  Bookmark, BookmarkCheck, ExternalLink, Calendar, Tag, Target,
  AlertCircle, Clock, TrendingUp,
} from 'lucide-react';
import { useToast } from '../ToastProvider';
import { useDebounce } from '@/hooks/useDebounce';
import { SkeletonList } from '../Skeleton';
import { Grant, formatAmount, STATUS_COLORS, STATUS_LABELS } from './types';

/* ─── Constants ──────────────────────────────────────────────────────────── */
const PRIORITY_COLORS: Record<string, string> = {
  high: '#ef4444',
  medium: '#f59e0b',
  low: '#6b7280',
};

/* ─── Component ──────────────────────────────────────────────────────────── */
export default function GrantsTab({
  selectedId,
  onSelectedConsumed,
}: {
  selectedId?: string | null;
  onSelectedConsumed?: () => void;
}) {
  const { addToast } = useToast();
  const [grants, setGrants] = useState<Grant[]>([]);
  const [loading, setLoading] = useState(true);
  const [statusCounts, setStatusCounts] = useState<Record<string, number>>({});
  const [filter, setFilter] = useState('all');
  const [search, setSearch] = useState('');
  const debouncedSearch = useDebounce(search, 300);
  const [expanded, setExpanded] = useState<string | null>(null);
  const [discovering, setDiscovering] = useState(false);

  /* ── Quick stats ─────────────────────────────────────────────────── */
  const [dashStats, setDashStats] = useState<{
    grant_count: number;
    total_funding: number;
  } | null>(null);

  /* ── Fetch grants ────────────────────────────────────────────────── */
  const fetchGrants = useCallback(async () => {
    try {
      const params = new URLSearchParams();
      if (filter !== 'all') params.set('status', filter);
      if (debouncedSearch) params.set('search', debouncedSearch);
      const qs = params.toString() ? `?${params}` : '';

      const res = await fetch(`/api/grants-proxy/grants${qs}`);
      if (!res.ok) throw new Error('Failed to load grants');
      const data = await res.json();
      setGrants(data.rows || []);
      setStatusCounts(data.statusCounts || {});
    } catch (err) {
      console.error('Grants fetch error:', err);
      addToast('Failed to load grants', 'error');
    } finally {
      setLoading(false);
    }
  }, [filter, debouncedSearch, addToast]);

  const fetchDashStats = useCallback(async () => {
    try {
      const res = await fetch('/api/grants-proxy/dashboard');
      if (res.ok) {
        const data = await res.json();
        setDashStats({ grant_count: data.grant_count, total_funding: data.total_funding });
      }
    } catch { /* ignore */ }
  }, []);

  useEffect(() => {
    fetchGrants();
    fetchDashStats();
  }, [fetchGrants, fetchDashStats]);

  /* ── Cross-tab navigation ────────────────────────────────────────── */
  useEffect(() => {
    if (selectedId) {
      setExpanded(selectedId);
      onSelectedConsumed?.();
      // scroll into view after render
      setTimeout(() => {
        document.getElementById(`grant-${selectedId}`)?.scrollIntoView({ behavior: 'smooth', block: 'center' });
      }, 100);
    }
  }, [selectedId, onSelectedConsumed]);

  /* ── AI Discovery ────────────────────────────────────────────────── */
  async function discoverGrants() {
    setDiscovering(true);
    try {
      const res = await fetch('/api/grants-proxy/grants/discover', { method: 'POST' });
      if (!res.ok) throw new Error('Discovery failed');
      const data = await res.json();
      addToast(`Discovered ${data.discovered || 0} new grants`, 'success');
      fetchGrants();
      fetchDashStats();
    } catch (err) {
      console.error('Discovery error:', err);
      addToast('Grant discovery failed — check Grant app status', 'error');
    } finally {
      setDiscovering(false);
    }
  }

  /* ── Actions ─────────────────────────────────────────────────────── */
  async function toggleBookmark(grant: Grant) {
    try {
      const res = await fetch('/api/grants-proxy/grants', {
        method: 'PATCH',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ id: grant.id, is_bookmarked: !grant.is_bookmarked }),
      });
      if (!res.ok) throw new Error('Bookmark failed');
      setGrants((prev) =>
        prev.map((g) => (g.id === grant.id ? { ...g, is_bookmarked: !g.is_bookmarked } : g))
      );
    } catch {
      addToast('Failed to update bookmark', 'error');
    }
  }

  async function updateStatus(grantId: string, status: string) {
    try {
      const res = await fetch('/api/grants-proxy/grants', {
        method: 'PATCH',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ id: grantId, status }),
      });
      if (!res.ok) throw new Error('Status update failed');
      addToast(`Status updated to ${STATUS_LABELS[status] || status}`, 'success');
      fetchGrants();
    } catch {
      addToast('Failed to update status', 'error');
    }
  }

  /* ─── Render ─────────────────────────────────────────────────────── */
  if (loading) return <SkeletonList count={6} />;

  const statusFilters = ['all', 'discovered', 'researching', 'preparing', 'submitted', 'awarded', 'declined', 'expired'];

  return (
    <div style={{ padding: 24, maxWidth: 1200 }}>
      {/* ── Header ─────────────────────────────────────────────────── */}
      <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 20, flexWrap: 'wrap', gap: 12 }}>
        <div>
          <h2 style={{ fontSize: 20, fontWeight: 700, color: 'var(--color-text)', margin: 0 }}>
            Grants
            <span style={{ fontSize: 13, fontWeight: 400, color: 'var(--color-text-muted)', marginLeft: 10 }}>
              {statusCounts.all ?? grants.length} total
            </span>
          </h2>
        </div>
        <button
          onClick={discoverGrants}
          disabled={discovering}
          style={{
            display: 'flex', alignItems: 'center', gap: 6,
            padding: '8px 16px', borderRadius: 8, border: 'none',
            backgroundColor: '#059669', color: '#fff',
            fontSize: 13, fontWeight: 600, cursor: discovering ? 'wait' : 'pointer',
            opacity: discovering ? 0.7 : 1,
            transition: 'opacity 0.15s',
          }}
        >
          {discovering ? <Loader2 size={15} style={{ animation: 'spin 1s linear infinite' }} /> : <Sparkles size={15} />}
          {discovering ? 'Discovering…' : 'Discover Grants'}
        </button>
      </div>

      {/* ── Quick Stats ────────────────────────────────────────────── */}
      <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(160px, 1fr))', gap: 10, marginBottom: 20 }}>
        {[
          { label: 'Total Grants', value: statusCounts.all ?? 0, icon: DollarSign, color: '#059669' },
          { label: 'Submitted', value: statusCounts.submitted ?? 0, icon: Target, color: '#06b6d4' },
          { label: 'Awarded', value: statusCounts.awarded ?? 0, icon: TrendingUp, color: '#22c55e' },
          { label: 'Total Funding', value: dashStats?.total_funding ? `$${dashStats.total_funding.toLocaleString()}` : '$0', icon: DollarSign, color: '#10b981' },
        ].map((s) => (
          <div key={s.label} style={{
            padding: '14px 16px', borderRadius: 10,
            backgroundColor: 'var(--color-surface)',
            border: '1px solid var(--color-border)',
            display: 'flex', alignItems: 'center', gap: 12,
          }}>
            <div style={{
              width: 36, height: 36, borderRadius: 8,
              backgroundColor: s.color + '18',
              display: 'flex', alignItems: 'center', justifyContent: 'center',
            }}>
              <s.icon size={18} style={{ color: s.color }} />
            </div>
            <div>
              <div style={{ fontSize: 10, color: 'var(--color-text-muted)', fontWeight: 500 }}>{s.label}</div>
              <div style={{ fontSize: 18, fontWeight: 700, color: 'var(--color-text)' }}>{s.value}</div>
            </div>
          </div>
        ))}
      </div>

      {/* ── Search ─────────────────────────────────────────────────── */}
      <div style={{ marginBottom: 16, position: 'relative' }}>
        <Search size={14} style={{ position: 'absolute', left: 12, top: '50%', transform: 'translateY(-50%)', color: 'var(--color-text-muted)' }} />
        <input
          type="text"
          value={search}
          onChange={(e) => setSearch(e.target.value)}
          placeholder="Search grants by title, funder, or tags…"
          style={{
            width: '100%', padding: '8px 12px 8px 34px', borderRadius: 8,
            border: '1px solid var(--color-border)',
            backgroundColor: 'var(--color-surface)',
            color: 'var(--color-text)', fontSize: 13,
            outline: 'none',
          }}
        />
      </div>

      {/* ── Status Pipeline Tabs ───────────────────────────────────── */}
      <div style={{ display: 'flex', flexWrap: 'wrap', gap: 6, marginBottom: 20 }}>
        {statusFilters.map((s) => {
          const isActive = filter === s;
          const color = s === 'all' ? '#7c3aed' : STATUS_COLORS[s] || '#6b7280';
          const count = s === 'all' ? (statusCounts.all ?? grants.length) : (statusCounts[s] ?? 0);
          return (
            <button
              key={s}
              onClick={() => setFilter(isActive ? 'all' : s)}
              style={{
                display: 'flex', alignItems: 'center', gap: 6,
                padding: '5px 12px', borderRadius: 20, cursor: 'pointer',
                border: isActive ? `2px solid ${color}` : '1px solid var(--color-border)',
                backgroundColor: isActive ? color + '22' : 'var(--color-surface)',
                color: isActive ? color : 'var(--color-text-muted)',
                fontSize: 12, fontWeight: 500,
                transition: 'all 0.15s',
              }}
            >
              {s !== 'all' && (
                <span style={{ width: 7, height: 7, borderRadius: '50%', backgroundColor: color }} />
              )}
              {s === 'all' ? 'All' : STATUS_LABELS[s] || s} ({count})
            </button>
          );
        })}
      </div>

      {/* ── Grant Cards ────────────────────────────────────────────── */}
      <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
        {grants.map((grant) => {
          const isExpanded = expanded === grant.id;
          return (
            <div
              key={grant.id}
              id={`grant-${grant.id}`}
              style={{
                borderRadius: 10,
                backgroundColor: 'var(--color-surface)',
                border: isExpanded ? '1px solid #059669' : '1px solid var(--color-border)',
                transition: 'border-color 0.15s',
                overflow: 'hidden',
              }}
            >
              {/* Card Header */}
              <div
                style={{
                  padding: '14px 16px', cursor: 'pointer',
                  display: 'flex', alignItems: 'flex-start', gap: 12,
                }}
                onClick={() => setExpanded(isExpanded ? null : grant.id)}
              >
                {/* Left: status dot + content */}
                <div style={{ flex: 1, minWidth: 0 }}>
                  <div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 4 }}>
                    <span style={{
                      width: 8, height: 8, borderRadius: '50%', flexShrink: 0,
                      backgroundColor: STATUS_COLORS[grant.status] || '#6b7280',
                    }} />
                    <span style={{
                      fontSize: 14, fontWeight: 600, color: 'var(--color-text)',
                      overflow: 'hidden', whiteSpace: 'nowrap', textOverflow: 'ellipsis',
                    }}>
                      {grant.title}
                    </span>
                  </div>

                  <div style={{ display: 'flex', alignItems: 'center', gap: 12, fontSize: 12, color: 'var(--color-text-muted)', flexWrap: 'wrap' }}>
                    <span style={{ fontWeight: 500 }}>{grant.funder}</span>
                    <span style={{ color: '#059669', fontWeight: 600 }}>{formatAmount(grant.amount_min, grant.amount_max)}</span>
                    {grant.deadline && (
                      <span style={{ display: 'flex', alignItems: 'center', gap: 3 }}>
                        <Calendar size={11} />
                        {new Date(grant.deadline).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })}
                      </span>
                    )}
                    {grant.ai_fit_score != null && (
                      <span style={{ display: 'flex', alignItems: 'center', gap: 3 }}>
                        <Sparkles size={11} style={{ color: '#f59e0b' }} />
                        {Math.round(grant.ai_fit_score * 100)}% fit
                      </span>
                    )}
                  </div>
                </div>

                {/* Right: badges + chevron */}
                <div style={{ display: 'flex', alignItems: 'center', gap: 8, flexShrink: 0 }}>
                  <span style={{
                    fontSize: 10, fontWeight: 600, padding: '2px 8px', borderRadius: 4,
                    backgroundColor: (STATUS_COLORS[grant.status] || '#6b7280') + '22',
                    color: STATUS_COLORS[grant.status] || '#6b7280',
                    textTransform: 'uppercase',
                  }}>
                    {STATUS_LABELS[grant.status] || grant.status}
                  </span>
                  <span style={{
                    fontSize: 10, fontWeight: 600, padding: '2px 8px', borderRadius: 4,
                    backgroundColor: (PRIORITY_COLORS[grant.priority] || '#6b7280') + '18',
                    color: PRIORITY_COLORS[grant.priority] || '#6b7280',
                    textTransform: 'uppercase',
                  }}>
                    {grant.priority}
                  </span>
                  <button
                    onClick={(e) => { e.stopPropagation(); toggleBookmark(grant); }}
                    style={{ background: 'none', border: 'none', cursor: 'pointer', padding: 2 }}
                    title={grant.is_bookmarked ? 'Remove bookmark' : 'Bookmark'}
                  >
                    {grant.is_bookmarked
                      ? <BookmarkCheck size={16} style={{ color: '#f59e0b' }} />
                      : <Bookmark size={16} style={{ color: 'var(--color-text-muted)' }} />
                    }
                  </button>
                  {isExpanded ? <ChevronUp size={16} style={{ color: 'var(--color-text-muted)' }} /> : <ChevronDown size={16} style={{ color: 'var(--color-text-muted)' }} />}
                </div>
              </div>

              {/* Expanded Detail */}
              {isExpanded && (
                <div style={{ padding: '0 16px 16px', borderTop: '1px solid var(--color-border)' }}>
                  {/* AI Fit Score Bar */}
                  {grant.ai_fit_score != null && (
                    <div style={{ marginTop: 12, marginBottom: 12 }}>
                      <div style={{ fontSize: 11, color: 'var(--color-text-muted)', marginBottom: 4 }}>
                        AI Fit Score
                      </div>
                      <div style={{
                        height: 6, borderRadius: 3,
                        backgroundColor: 'var(--color-border)',
                        overflow: 'hidden',
                      }}>
                        <div style={{
                          height: '100%', borderRadius: 3,
                          width: `${Math.round(grant.ai_fit_score * 100)}%`,
                          backgroundColor: grant.ai_fit_score >= 0.7 ? '#22c55e' : grant.ai_fit_score >= 0.4 ? '#f59e0b' : '#ef4444',
                          transition: 'width 0.3s ease',
                        }} />
                      </div>
                    </div>
                  )}

                  {/* AI Suggestion */}
                  {grant.ai_suggestion && (
                    <div style={{
                      padding: '10px 12px', borderRadius: 8, marginBottom: 12,
                      backgroundColor: '#059669' + '12',
                      border: '1px solid #059669' + '30',
                      fontSize: 12, color: 'var(--color-text-secondary)', lineHeight: 1.5,
                    }}>
                      <strong style={{ color: '#10b981' }}>AI Suggestion:</strong> {grant.ai_suggestion}
                    </div>
                  )}

                  {/* Description */}
                  {grant.description && (
                    <div style={{ marginBottom: 12 }}>
                      <div style={{ fontSize: 11, fontWeight: 600, color: 'var(--color-text-muted)', marginBottom: 4, textTransform: 'uppercase', letterSpacing: '0.04em' }}>
                        Description
                      </div>
                      <div style={{ fontSize: 13, color: 'var(--color-text-secondary)', lineHeight: 1.5 }}>
                        {grant.description}
                      </div>
                    </div>
                  )}

                  {/* Eligibility */}
                  {grant.eligibility && (
                    <div style={{ marginBottom: 12 }}>
                      <div style={{ fontSize: 11, fontWeight: 600, color: 'var(--color-text-muted)', marginBottom: 4, textTransform: 'uppercase', letterSpacing: '0.04em' }}>
                        Eligibility
                      </div>
                      <div style={{ fontSize: 13, color: 'var(--color-text-secondary)', lineHeight: 1.5 }}>
                        {grant.eligibility}
                      </div>
                    </div>
                  )}

                  {/* Focus Areas */}
                  {grant.focus_areas?.length > 0 && (
                    <div style={{ marginBottom: 12 }}>
                      <div style={{ fontSize: 11, fontWeight: 600, color: 'var(--color-text-muted)', marginBottom: 4, textTransform: 'uppercase', letterSpacing: '0.04em' }}>
                        Focus Areas
                      </div>
                      <div style={{ display: 'flex', flexWrap: 'wrap', gap: 4 }}>
                        {grant.focus_areas.map((area) => (
                          <span key={area} style={{
                            padding: '2px 8px', borderRadius: 4, fontSize: 11, fontWeight: 500,
                            backgroundColor: '#059669' + '18', color: '#10b981',
                          }}>
                            {area}
                          </span>
                        ))}
                      </div>
                    </div>
                  )}

                  {/* Tags */}
                  {grant.tags?.length > 0 && (
                    <div style={{ marginBottom: 12 }}>
                      <div style={{ display: 'flex', flexWrap: 'wrap', gap: 4 }}>
                        {grant.tags.map((tag) => (
                          <span key={tag} style={{
                            display: 'flex', alignItems: 'center', gap: 3,
                            padding: '2px 8px', borderRadius: 4, fontSize: 11,
                            backgroundColor: 'var(--color-bg)', color: 'var(--color-text-muted)',
                          }}>
                            <Tag size={9} /> {tag}
                          </span>
                        ))}
                      </div>
                    </div>
                  )}

                  {/* Notes */}
                  {grant.notes && (
                    <div style={{ marginBottom: 12 }}>
                      <div style={{ fontSize: 11, fontWeight: 600, color: 'var(--color-text-muted)', marginBottom: 4, textTransform: 'uppercase', letterSpacing: '0.04em' }}>
                        Notes
                      </div>
                      <div style={{ fontSize: 12, color: 'var(--color-text-secondary)', lineHeight: 1.5, fontStyle: 'italic' }}>
                        {grant.notes}
                      </div>
                    </div>
                  )}

                  {/* Meta row */}
                  <div style={{ display: 'flex', alignItems: 'center', gap: 12, fontSize: 11, color: 'var(--color-text-muted)', marginBottom: 12, flexWrap: 'wrap' }}>
                    {grant.cycle && (
                      <span style={{ display: 'flex', alignItems: 'center', gap: 3 }}>
                        <Clock size={11} /> Cycle: {grant.cycle}
                      </span>
                    )}
                    <span>
                      Added {new Date(grant.created_at).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })}
                    </span>
                  </div>

                  {/* Actions row */}
                  <div style={{ display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' }}>
                    {grant.application_url && (
                      <a
                        href={grant.application_url}
                        target="_blank"
                        rel="noopener noreferrer"
                        style={{
                          display: 'flex', alignItems: 'center', gap: 4,
                          padding: '6px 12px', borderRadius: 6,
                          backgroundColor: '#059669', color: '#fff',
                          fontSize: 12, fontWeight: 600, textDecoration: 'none',
                        }}
                      >
                        <ExternalLink size={12} /> Apply
                      </a>
                    )}
                    {grant.funder_url && (
                      <a
                        href={grant.funder_url}
                        target="_blank"
                        rel="noopener noreferrer"
                        style={{
                          display: 'flex', alignItems: 'center', gap: 4,
                          padding: '6px 12px', borderRadius: 6,
                          border: '1px solid var(--color-border)',
                          backgroundColor: 'transparent', color: 'var(--color-text-secondary)',
                          fontSize: 12, fontWeight: 500, textDecoration: 'none',
                        }}
                      >
                        <ExternalLink size={12} /> Funder Site
                      </a>
                    )}

                    {/* Status change dropdown */}
                    <select
                      value={grant.status}
                      onChange={(e) => updateStatus(grant.id, e.target.value)}
                      style={{
                        padding: '6px 10px', borderRadius: 6, fontSize: 12,
                        border: '1px solid var(--color-border)',
                        backgroundColor: 'var(--color-surface)',
                        color: 'var(--color-text)',
                        cursor: 'pointer',
                      }}
                    >
                      {Object.entries(STATUS_LABELS).map(([val, label]) => (
                        <option key={val} value={val}>{label}</option>
                      ))}
                    </select>
                  </div>
                </div>
              )}
            </div>
          );
        })}
      </div>

      {/* ── Empty state ────────────────────────────────────────────── */}
      {grants.length === 0 && !loading && (
        <div style={{
          padding: 48, textAlign: 'center',
          color: 'var(--color-text-muted)',
        }}>
          <DollarSign size={40} style={{ marginBottom: 12, opacity: 0.3 }} />
          <div style={{ fontSize: 15, fontWeight: 600, marginBottom: 6 }}>No grants found</div>
          <div style={{ fontSize: 13, marginBottom: 16 }}>
            {filter !== 'all' ? 'Try a different status filter.' : 'Click "Discover Grants" to find funding opportunities with AI.'}
          </div>
          {filter === 'all' && (
            <button
              onClick={discoverGrants}
              disabled={discovering}
              style={{
                padding: '10px 20px', borderRadius: 8, border: 'none',
                backgroundColor: '#059669', color: '#fff',
                fontSize: 13, fontWeight: 600, cursor: 'pointer',
              }}
            >
              <Sparkles size={14} style={{ marginRight: 6, verticalAlign: 'middle' }} />
              Discover Grants
            </button>
          )}
        </div>
      )}
    </div>
  );
}