← back to Norma

components/agents/GrantFinder.tsx

537 lines

'use client';

import { useState, useEffect } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import {
  Search, Building2, DollarSign, Calendar, ChevronDown, ChevronUp,
  ExternalLink, Sparkles, Filter, Tag, AlertCircle, Loader2,
  Trophy, TrendingUp, Clock, FileText,
} from 'lucide-react';
import { useToast } from '../ToastProvider';

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

interface GrantResult {
  id: string;
  source: string;
  title: string;
  funder: string;
  funder_url: string | null;
  amount_min: number | null;
  amount_max: number | null;
  description: string | null;
  eligibility: string | null;
  focus_areas: string[];
  deadline: string | null;
  application_url: string | null;
  status: string;
  match_score: number;
}

const GRANT_TYPES = [
  { value: '', label: 'All Types' },
  { value: 'federal', label: 'Federal' },
  { value: 'state', label: 'State' },
  { value: 'foundation', label: 'Foundation' },
  { value: 'corporate', label: 'Corporate' },
];

const ISSUE_AREAS = [
  '', 'education', 'advocacy', 'healthcare', 'housing', 'workforce',
  'economic justice', 'racial equity', 'climate', 'food security', 'mental health',
];

interface GrantFinderProps {
  selectedOrgId: string;
  onOrgChange: (id: string) => void;
}

export default function GrantFinder({ selectedOrgId, onOrgChange }: GrantFinderProps) {
  const { addToast } = useToast();

  const [orgs, setOrgs] = useState<Organization[]>([]);
  const [grants, setGrants] = useState<GrantResult[]>([]);
  const [loading, setLoading] = useState(false);
  const [searched, setSearched] = useState(false);
  const [expandedId, setExpandedId] = useState<string | null>(null);
  const [generatingProposal, setGeneratingProposal] = useState<string | null>(null);
  const [proposalText, setProposalText] = useState<Record<string, string>>({});

  // Filters
  const [grantType, setGrantType] = useState('');
  const [issueArea, setIssueArea] = useState('');
  const [budgetMin, setBudgetMin] = useState('');
  const [budgetMax, setBudgetMax] = useState('');

  // Load organizations
  useEffect(() => {
    (async () => {
      try {
        const res = await fetch('/api/orgs?limit=200&sort=name&sort_dir=asc');
        if (res.ok) {
          const data = await res.json();
          setOrgs(data.rows || []);
        }
      } catch {}
    })();
  }, []);

  async function handleSearch() {
    if (!selectedOrgId) {
      addToast('Please select an organization first', 'error');
      return;
    }

    setLoading(true);
    setGrants([]);
    setSearched(true);

    try {
      const params = new URLSearchParams({ org_id: selectedOrgId });
      if (grantType) params.set('grant_type', grantType);
      if (issueArea) params.set('issue_area', issueArea);
      if (budgetMin) params.set('budget_min', budgetMin);
      if (budgetMax) params.set('budget_max', budgetMax);

      const res = await fetch(`/api/agents/grants/match?${params}`);
      if (!res.ok) {
        const data = await res.json();
        throw new Error(data.error || 'Search failed');
      }

      const data = await res.json();
      setGrants(data.grants || []);
      addToast(`Found ${(data.grants || []).length} matching grants`, 'success');
    } catch (err) {
      addToast((err as Error).message || 'Failed to search grants', 'error');
    }
    setLoading(false);
  }

  async function handleGenerateProposal(grant: GrantResult) {
    setGeneratingProposal(grant.id);
    try {
      const org = orgs.find(o => o.id === selectedOrgId);
      const prompt = `Generate a concise grant proposal outline for:
Organization: ${org?.name || 'Unknown'}
Grant: ${grant.title}
Funder: ${grant.funder}
Amount Range: $${(grant.amount_min || 0).toLocaleString()} - $${(grant.amount_max || 0).toLocaleString()}
Grant Focus: ${(grant.focus_areas || []).join(', ')}
Deadline: ${grant.deadline ? new Date(grant.deadline).toLocaleDateString() : 'N/A'}

Create a structured outline with:
1. Executive Summary (2-3 sentences)
2. Statement of Need
3. Project Description
4. Goals & Objectives (3-5 SMART goals)
5. Budget Overview
6. Evaluation Plan
7. Sustainability Plan

Keep it concise but professional.`;

      const res = await fetch('/api/ai/generate', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ prompt, temperature: 0.7, maxOutputTokens: 2048 }),
      });

      if (!res.ok) throw new Error('AI generation failed');
      const data = await res.json();
      const text = data?.text || 'Failed to generate proposal.';
      setProposalText(prev => ({ ...prev, [grant.id]: text }));
      addToast('Proposal outline generated', 'success');
    } catch (err) {
      addToast('Failed to generate proposal outline', 'error');
    }
    setGeneratingProposal(null);
  }

  function formatAmount(min: number | null, max: number | null): string {
    if (!min && !max) return 'Varies';
    const fmt = (n: number) => {
      if (n >= 1_000_000) return `$${(n / 1_000_000).toFixed(1)}M`;
      if (n >= 1_000) return `$${(n / 1_000).toFixed(0)}K`;
      return `$${n.toLocaleString()}`;
    };
    if (min && max) return `${fmt(min)} - ${fmt(max)}`;
    if (max) return `Up to ${fmt(max)}`;
    return `From ${fmt(min!)}`;
  }

  function getScoreColor(score: number): string {
    if (score >= 70) return '#22c55e';
    if (score >= 40) return '#f59e0b';
    return '#ef4444';
  }

  function getScoreLabel(score: number): string {
    if (score >= 70) return 'Strong Match';
    if (score >= 40) return 'Moderate Match';
    return 'Low Match';
  }

  return (
    <div style={{ padding: 24, maxWidth: 1100, margin: '0 auto' }}>
      {/* Header */}
      <div style={{ display: 'flex', alignItems: 'center', gap: 12, marginBottom: 24 }}>
        <div style={{
          width: 40, height: 40, borderRadius: 12,
          backgroundColor: 'rgba(245,158,11,0.12)',
          display: 'flex', alignItems: 'center', justifyContent: 'center',
        }}>
          <Search size={20} style={{ color: '#f59e0b' }} />
        </div>
        <div>
          <h1 style={{ fontSize: 22, fontWeight: 700, color: 'var(--color-text)', margin: 0 }}>
            AI Grant Finder
          </h1>
          <p style={{ fontSize: 13, color: 'var(--color-text-muted)', margin: 0 }}>
            Find and match grants to your organization's mission
          </p>
        </div>
      </div>

      {/* Filters */}
      <div className="card" style={{ padding: 16, marginBottom: 20 }}>
        <div style={{ display: 'flex', flexWrap: 'wrap', alignItems: 'flex-end', gap: 12 }}>
          {/* Organization */}
          <div style={{ flex: '1 1 220px' }}>
            <label style={{ fontSize: 11, fontWeight: 600, color: 'var(--color-text-muted)', display: 'flex', alignItems: 'center', gap: 4, marginBottom: 4, textTransform: 'uppercase', letterSpacing: '0.05em' }}>
              <Building2 size={12} /> Organization
            </label>
            <select className="input" value={selectedOrgId} onChange={e => onOrgChange(e.target.value)}>
              <option value="">-- Select Org --</option>
              {orgs.map(o => <option key={o.id} value={o.id}>{o.name}</option>)}
            </select>
          </div>

          {/* Grant Type */}
          <div style={{ flex: '0 1 150px' }}>
            <label style={{ fontSize: 11, fontWeight: 600, color: 'var(--color-text-muted)', display: 'flex', alignItems: 'center', gap: 4, marginBottom: 4, textTransform: 'uppercase', letterSpacing: '0.05em' }}>
              <Filter size={12} /> Type
            </label>
            <select className="input" value={grantType} onChange={e => setGrantType(e.target.value)}>
              {GRANT_TYPES.map(t => <option key={t.value} value={t.value}>{t.label}</option>)}
            </select>
          </div>

          {/* Issue Area */}
          <div style={{ flex: '0 1 160px' }}>
            <label style={{ fontSize: 11, fontWeight: 600, color: 'var(--color-text-muted)', display: 'flex', alignItems: 'center', gap: 4, marginBottom: 4, textTransform: 'uppercase', letterSpacing: '0.05em' }}>
              <Tag size={12} /> Issue Area
            </label>
            <select className="input" value={issueArea} onChange={e => setIssueArea(e.target.value)}>
              <option value="">All Areas</option>
              {ISSUE_AREAS.filter(Boolean).map(a => <option key={a} value={a}>{a.charAt(0).toUpperCase() + a.slice(1)}</option>)}
            </select>
          </div>

          {/* Budget Range */}
          <div style={{ flex: '0 1 120px' }}>
            <label style={{ fontSize: 11, fontWeight: 600, color: 'var(--color-text-muted)', display: 'flex', alignItems: 'center', gap: 4, marginBottom: 4, textTransform: 'uppercase', letterSpacing: '0.05em' }}>
              <DollarSign size={12} /> Min
            </label>
            <input
              className="input"
              type="number"
              value={budgetMin}
              onChange={e => setBudgetMin(e.target.value)}
              placeholder="0"
              min={0}
            />
          </div>
          <div style={{ flex: '0 1 120px' }}>
            <label style={{ fontSize: 11, fontWeight: 600, color: 'var(--color-text-muted)', display: 'flex', alignItems: 'center', gap: 4, marginBottom: 4, textTransform: 'uppercase', letterSpacing: '0.05em' }}>
              <DollarSign size={12} /> Max
            </label>
            <input
              className="input"
              type="number"
              value={budgetMax}
              onChange={e => setBudgetMax(e.target.value)}
              placeholder="No limit"
              min={0}
            />
          </div>

          {/* Search Button */}
          <button
            className="btn btn-primary"
            onClick={handleSearch}
            disabled={loading || !selectedOrgId}
            style={{ height: 38 }}
          >
            {loading ? (
              <>
                <span className="spinner" style={{ width: 14, height: 14 }} />
                Searching...
              </>
            ) : (
              <>
                <Search size={14} />
                Find Grants
              </>
            )}
          </button>
        </div>
      </div>

      {/* Results */}
      {loading && (
        <div style={{ textAlign: 'center', padding: 48 }}>
          <div className="spinner" style={{ width: 32, height: 32, margin: '0 auto 16px' }} />
          <p style={{ fontSize: 14, color: 'var(--color-text-secondary)' }}>Matching grants to your organization...</p>
        </div>
      )}

      {!loading && searched && grants.length === 0 && (
        <div className="card" style={{ padding: 48, textAlign: 'center' }}>
          <AlertCircle size={32} style={{ color: 'var(--color-text-muted)', margin: '0 auto 12px', display: 'block' }} />
          <h3 style={{ fontSize: 16, fontWeight: 600, color: 'var(--color-text)', margin: '0 0 8px' }}>
            No matching grants found
          </h3>
          <p style={{ fontSize: 13, color: 'var(--color-text-muted)', maxWidth: 400, margin: '0 auto' }}>
            Try adjusting your filters or broadening your search criteria.
          </p>
        </div>
      )}

      {!loading && grants.length > 0 && (
        <div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
          <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 4 }}>
            <span style={{ fontSize: 13, color: 'var(--color-text-secondary)' }}>
              {grants.length} grants found, sorted by match score
            </span>
          </div>

          {grants.map((grant, idx) => (
            <motion.div
              key={grant.id}
              initial={{ opacity: 0, y: 10 }}
              animate={{ opacity: 1, y: 0 }}
              transition={{ delay: idx * 0.03, duration: 0.2 }}
            >
              <div className="card" style={{ padding: 0, overflow: 'hidden' }}>
                {/* Main Row */}
                <div
                  style={{
                    padding: 16,
                    display: 'flex', alignItems: 'flex-start', gap: 16,
                    cursor: 'pointer',
                  }}
                  onClick={() => setExpandedId(expandedId === grant.id ? null : grant.id)}
                >
                  {/* Score Badge */}
                  <div style={{
                    width: 52, height: 52, borderRadius: 12, flexShrink: 0,
                    backgroundColor: `${getScoreColor(grant.match_score)}15`,
                    display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center',
                  }}>
                    <span style={{ fontSize: 18, fontWeight: 700, color: getScoreColor(grant.match_score), lineHeight: 1 }}>
                      {grant.match_score}
                    </span>
                    <span style={{ fontSize: 8, fontWeight: 600, color: getScoreColor(grant.match_score), textTransform: 'uppercase' }}>
                      match
                    </span>
                  </div>

                  {/* Content */}
                  <div style={{ flex: 1, minWidth: 0 }}>
                    <div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 4 }}>
                      <h3 style={{ fontSize: 14, fontWeight: 600, color: 'var(--color-text)', margin: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
                        {grant.title}
                      </h3>
                      <span className={`badge ${grant.source === 'federal' ? 'lane-b' : 'lane-c'}`} style={{ flexShrink: 0 }}>
                        {grant.source}
                      </span>
                    </div>
                    <div style={{ display: 'flex', flexWrap: 'wrap', gap: 12, fontSize: 12, color: 'var(--color-text-secondary)' }}>
                      {(grant.application_url || grant.funder_url) ? (
                        <a
                          href={grant.application_url || grant.funder_url || '#'}
                          target="_blank"
                          rel="noopener noreferrer"
                          onClick={e => e.stopPropagation()}
                          style={{ display: 'flex', alignItems: 'center', gap: 4, color: 'var(--color-primary)', textDecoration: 'none' }}
                        >
                          <Building2 size={12} /> {grant.funder} <ExternalLink size={10} />
                        </a>
                      ) : (
                        <span style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
                          <Building2 size={12} /> {grant.funder}
                        </span>
                      )}
                      <span style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
                        <DollarSign size={12} /> {formatAmount(grant.amount_min, grant.amount_max)}
                      </span>
                      {grant.deadline && (
                        <span style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
                          <Calendar size={12} />
                          {new Date(grant.deadline).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })}
                        </span>
                      )}
                    </div>
                  </div>

                  <div style={{ display: 'flex', alignItems: 'center', gap: 8, flexShrink: 0 }}>
                    <span style={{
                      fontSize: 11, fontWeight: 600, padding: '3px 10px', borderRadius: 10,
                      backgroundColor: `${getScoreColor(grant.match_score)}15`,
                      color: getScoreColor(grant.match_score),
                    }}>
                      {getScoreLabel(grant.match_score)}
                    </span>
                    {expandedId === grant.id ? <ChevronUp size={16} style={{ color: 'var(--color-text-muted)' }} /> : <ChevronDown size={16} style={{ color: 'var(--color-text-muted)' }} />}
                  </div>
                </div>

                {/* Expanded Details */}
                <AnimatePresence>
                  {expandedId === grant.id && (
                    <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={{
                        padding: '0 16px 16px',
                        borderTop: '1px solid var(--color-border)',
                        paddingTop: 16,
                      }}>
                        <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 16, marginBottom: 16 }}>
                          {/* Description */}
                          <div>
                            <h4 style={{ fontSize: 12, fontWeight: 600, color: 'var(--color-text-muted)', textTransform: 'uppercase', margin: '0 0 6px' }}>
                              Description
                            </h4>
                            <p style={{ fontSize: 13, color: 'var(--color-text-secondary)', lineHeight: 1.6, margin: 0 }}>
                              {grant.description ? (grant.description.length > 400 ? grant.description.slice(0, 400) + '...' : grant.description) : 'No description available.'}
                            </p>
                          </div>

                          {/* Eligibility */}
                          <div>
                            <h4 style={{ fontSize: 12, fontWeight: 600, color: 'var(--color-text-muted)', textTransform: 'uppercase', margin: '0 0 6px' }}>
                              Eligibility
                            </h4>
                            <p style={{ fontSize: 13, color: 'var(--color-text-secondary)', lineHeight: 1.6, margin: 0 }}>
                              {grant.eligibility ? (grant.eligibility.length > 400 ? grant.eligibility.slice(0, 400) + '...' : grant.eligibility) : 'See application for details.'}
                            </p>
                          </div>
                        </div>

                        {/* Focus Areas */}
                        {grant.focus_areas && grant.focus_areas.length > 0 && (
                          <div style={{ marginBottom: 16 }}>
                            <h4 style={{ fontSize: 12, fontWeight: 600, color: 'var(--color-text-muted)', textTransform: 'uppercase', margin: '0 0 6px' }}>
                              Focus Areas
                            </h4>
                            <div style={{ display: 'flex', flexWrap: 'wrap', gap: 6 }}>
                              {grant.focus_areas.map((area, i) => (
                                <span key={i} style={{
                                  fontSize: 11, padding: '3px 10px', borderRadius: 10,
                                  backgroundColor: 'rgba(99,102,241,0.1)',
                                  color: '#34d399', fontWeight: 500,
                                }}>
                                  {area}
                                </span>
                              ))}
                            </div>
                          </div>
                        )}

                        {/* Actions */}
                        <div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
                          <button
                            className="btn btn-primary btn-sm"
                            onClick={(e) => { e.stopPropagation(); handleGenerateProposal(grant); }}
                            disabled={generatingProposal === grant.id}
                          >
                            {generatingProposal === grant.id ? (
                              <>
                                <span className="spinner" style={{ width: 12, height: 12 }} />
                                Generating...
                              </>
                            ) : (
                              <>
                                <Sparkles size={14} />
                                Generate Proposal Outline
                              </>
                            )}
                          </button>
                          {(grant.application_url || grant.funder_url) && (
                            <a
                              href={grant.application_url || grant.funder_url || '#'}
                              target="_blank"
                              rel="noopener noreferrer"
                              className="btn btn-secondary btn-sm"
                              onClick={e => e.stopPropagation()}
                            >
                              <ExternalLink size={14} />
                              {grant.application_url ? 'Apply' : 'View Funder'}
                            </a>
                          )}
                        </div>

                        {/* Proposal Outline */}
                        {proposalText[grant.id] && (
                          <div style={{
                            marginTop: 16, padding: 16, borderRadius: 8,
                            backgroundColor: 'var(--color-surface-el)',
                            border: '1px solid var(--color-border)',
                          }}>
                            <h4 style={{ fontSize: 13, fontWeight: 600, color: 'var(--color-text)', margin: '0 0 8px', display: 'flex', alignItems: 'center', gap: 6 }}>
                              <FileText size={14} style={{ color: 'var(--color-primary)' }} />
                              Proposal Outline
                            </h4>
                            <pre style={{
                              fontSize: 12, color: 'var(--color-text-secondary)',
                              lineHeight: 1.6, whiteSpace: 'pre-wrap', margin: 0,
                              fontFamily: 'inherit',
                            }}>
                              {proposalText[grant.id]}
                            </pre>
                          </div>
                        )}
                      </div>
                    </motion.div>
                  )}
                </AnimatePresence>
              </div>
            </motion.div>
          ))}
        </div>
      )}

      {/* Empty state before search */}
      {!searched && !loading && (
        <div className="card" style={{ padding: 48, textAlign: 'center' }}>
          <div style={{
            width: 56, height: 56, borderRadius: 16, margin: '0 auto 16px',
            backgroundColor: 'rgba(245,158,11,0.1)',
            display: 'flex', alignItems: 'center', justifyContent: 'center',
          }}>
            <Trophy size={26} style={{ color: '#f59e0b', opacity: 0.6 }} />
          </div>
          <h3 style={{ fontSize: 16, fontWeight: 600, color: 'var(--color-text)', margin: '0 0 8px' }}>
            Find Your Perfect Grant Match
          </h3>
          <p style={{ fontSize: 13, color: 'var(--color-text-muted)', maxWidth: 400, margin: '0 auto' }}>
            Select an organization and configure your filters. The AI will score and rank grants
            based on mission alignment, issue overlap, and focus populations.
          </p>
        </div>
      )}
    </div>
  );
}