← back to Freddy

components/funding/FundingTab.tsx

255 lines

'use client';

import { useState, useEffect, useCallback } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { SkeletonList } from '../Skeleton';
import {
  DollarSign, Plus, X, Loader2, RefreshCw,
  Calendar, Landmark, Clock, CheckCircle, AlertCircle,
} from 'lucide-react';

interface FundingRound {
  id: string;
  title: string;
  total_pool: string;
  allocated: string;
  remaining: string;
  status: string;
  opens_at: string | null;
  closes_at: string | null;
  cause_filter: string[];
  granter_name: string;
  granter_id: string;
  created_at: string;
}

interface Granter {
  id: string;
  name: string;
}

const fadeIn = {
  hidden: { opacity: 0, y: 20 },
  visible: (i: number) => ({
    opacity: 1, y: 0,
    transition: { delay: i * 0.04, duration: 0.35 },
  }),
};

export default function FundingTab() {
  const [rounds, setRounds] = useState<FundingRound[]>([]);
  const [granters, setGranters] = useState<Granter[]>([]);
  const [loading, setLoading] = useState(true);
  const [showCreate, setShowCreate] = useState(false);

  const fetchData = useCallback(async () => {
    try {
      const [roundsRes, grantersRes] = await Promise.all([
        fetch('/api/funding', { credentials: 'include' }),
        fetch('/api/granters', { credentials: 'include' }),
      ]);
      if (roundsRes.ok) {
        const data = await roundsRes.json();
        setRounds(data.funding_rounds || []);
      }
      if (grantersRes.ok) {
        const data = await grantersRes.json();
        setGranters(data.granters || []);
      }
    } catch (err) {
      console.error('Failed to fetch funding data:', err);
    } finally {
      setLoading(false);
    }
  }, []);

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

  const handleCreate = async (formData: Record<string, string>) => {
    try {
      const res = await fetch('/api/funding', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        credentials: 'include',
        body: JSON.stringify({
          title: formData.title,
          granter_id: formData.granter_id,
          total_pool: parseFloat(formData.total_pool),
          opens_at: formData.opens_at || null,
          closes_at: formData.closes_at || null,
        }),
      });
      if (res.ok) {
        setShowCreate(false);
        await fetchData();
      }
    } catch (err) {
      console.error('Create funding round error:', err);
    }
  };

  const fmtMoney = (val: string | null) => {
    if (!val) return '$0';
    const n = parseFloat(val);
    return `$${n.toLocaleString(undefined, { minimumFractionDigits: 0, maximumFractionDigits: 0 })}`;
  };

  const statusBadge = (s: string) => {
    switch (s) {
      case 'open': return 'badge-success';
      case 'completed': return 'badge-blue';
      case 'closed': return 'badge-error';
      default: return 'badge-warning';
    }
  };

  return (
    <div className="p-6 space-y-6">
      <div className="flex items-center justify-between flex-wrap gap-3">
        <div>
          <h2 className="text-xl font-bold flex items-center gap-2" style={{ color: 'var(--color-text)' }}>
            <DollarSign size={20} style={{ color: '#22c55e' }} />
            Funding Pipeline
          </h2>
          <p className="text-sm mt-1" style={{ color: 'var(--color-text-muted)' }}>
            Granter-initiated funding rounds and allocation tracking
          </p>
        </div>
        <div className="flex items-center gap-2">
          <button onClick={() => setShowCreate(true)} className="btn btn-primary">
            <Plus size={14} /> Create Funding Round
          </button>
          <button onClick={fetchData} className="btn btn-secondary btn-sm">
            <RefreshCw size={14} />
          </button>
        </div>
      </div>

      {loading ? (
        <SkeletonList count={4} />
      ) : (
        <div className="space-y-4">
          <AnimatePresence>
            {rounds.map((round, i) => {
              const total = parseFloat(round.total_pool) || 1;
              const allocated = parseFloat(round.allocated) || 0;
              const remaining = parseFloat(round.remaining) || 0;
              const pct = Math.min(100, (allocated / total) * 100);

              return (
                <motion.div key={round.id} custom={i} initial="hidden" animate="visible" variants={fadeIn} className="card p-5">
                  <div className="flex items-start justify-between mb-3">
                    <div>
                      <h3 className="text-base font-semibold" style={{ color: 'var(--color-text)' }}>{round.title}</h3>
                      <div className="flex items-center gap-2 mt-1 text-xs" style={{ color: 'var(--color-text-muted)' }}>
                        <Landmark size={12} /> {round.granter_name}
                        {round.opens_at && (
                          <span className="flex items-center gap-1"><Calendar size={10} /> Opens: {new Date(round.opens_at).toLocaleDateString()}</span>
                        )}
                        {round.closes_at && (
                          <span className="flex items-center gap-1"><Clock size={10} /> Closes: {new Date(round.closes_at).toLocaleDateString()}</span>
                        )}
                      </div>
                    </div>
                    <span className={`badge ${statusBadge(round.status)}`}>{round.status}</span>
                  </div>

                  <div className="grid grid-cols-3 gap-3 mb-3">
                    <div className="text-center p-2 rounded-lg" style={{ backgroundColor: 'var(--color-surface-el)' }}>
                      <div className="text-xs" style={{ color: 'var(--color-text-muted)' }}>Total Pool</div>
                      <div className="text-sm font-bold" style={{ color: 'var(--color-text)' }}>{fmtMoney(round.total_pool)}</div>
                    </div>
                    <div className="text-center p-2 rounded-lg" style={{ backgroundColor: 'var(--color-surface-el)' }}>
                      <div className="text-xs" style={{ color: 'var(--color-text-muted)' }}>Allocated</div>
                      <div className="text-sm font-bold" style={{ color: '#fbbf24' }}>{fmtMoney(round.allocated)}</div>
                    </div>
                    <div className="text-center p-2 rounded-lg" style={{ backgroundColor: 'var(--color-surface-el)' }}>
                      <div className="text-xs" style={{ color: 'var(--color-text-muted)' }}>Remaining</div>
                      <div className="text-sm font-bold" style={{ color: '#22c55e' }}>{fmtMoney(round.remaining)}</div>
                    </div>
                  </div>

                  <div className="flex items-center justify-between text-xs mb-1" style={{ color: 'var(--color-text-muted)' }}>
                    <span>Allocation Progress</span>
                    <span>{pct.toFixed(0)}%</span>
                  </div>
                  <div className="urgency-gauge">
                    <div className="urgency-gauge-fill" style={{ width: `${pct}%`, background: 'linear-gradient(90deg, #d97706, #fbbf24)' }} />
                  </div>
                </motion.div>
              );
            })}
          </AnimatePresence>
        </div>
      )}

      {!loading && rounds.length === 0 && (
        <div className="text-center py-12">
          <DollarSign size={32} style={{ color: 'var(--color-text-muted)', margin: '0 auto 12px' }} />
          <p style={{ color: 'var(--color-text-muted)' }}>No funding rounds yet. Create one to start allocating grants.</p>
        </div>
      )}

      {showCreate && <CreateFundingModal granters={granters} onClose={() => setShowCreate(false)} onSubmit={handleCreate} />}
    </div>
  );
}

function CreateFundingModal({ granters, onClose, onSubmit }: { granters: Granter[]; onClose: () => void; onSubmit: (data: Record<string, string>) => void }) {
  const [form, setForm] = useState({ title: '', granter_id: '', total_pool: '', opens_at: '', closes_at: '' });
  const [submitting, setSubmitting] = useState(false);

  const handleSubmit = async () => {
    setSubmitting(true);
    await onSubmit(form);
    setSubmitting(false);
  };

  const set = (k: string) => (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => setForm(f => ({ ...f, [k]: e.target.value }));

  return (
    <div className="modal-overlay" onClick={onClose}>
      <motion.div initial={{ opacity: 0, scale: 0.95 }} animate={{ opacity: 1, scale: 1 }} className="modal-content" onClick={(e) => e.stopPropagation()}>
        <div className="flex items-center justify-between mb-4">
          <h3 className="text-lg font-semibold" style={{ color: 'var(--color-text)' }}>Create Funding Round</h3>
          <button onClick={onClose} className="btn btn-ghost btn-sm"><X size={16} /></button>
        </div>
        <div className="space-y-3">
          <div>
            <label className="block text-sm mb-1" style={{ color: 'var(--color-text-secondary)' }}>Title *</label>
            <input className="input" value={form.title} onChange={set('title')} placeholder="2024 Education Grant Round" />
          </div>
          <div>
            <label className="block text-sm mb-1" style={{ color: 'var(--color-text-secondary)' }}>Granter *</label>
            <select className="input" value={form.granter_id} onChange={set('granter_id')}>
              <option value="">Select a granter...</option>
              {granters.map(g => <option key={g.id} value={g.id}>{g.name}</option>)}
            </select>
          </div>
          <div>
            <label className="block text-sm mb-1" style={{ color: 'var(--color-text-secondary)' }}>Total Pool ($) *</label>
            <input className="input" type="number" value={form.total_pool} onChange={set('total_pool')} placeholder="100000" />
          </div>
          <div className="grid grid-cols-2 gap-3">
            <div>
              <label className="block text-sm mb-1" style={{ color: 'var(--color-text-secondary)' }}>Opens</label>
              <input className="input" type="date" value={form.opens_at} onChange={set('opens_at')} />
            </div>
            <div>
              <label className="block text-sm mb-1" style={{ color: 'var(--color-text-secondary)' }}>Closes</label>
              <input className="input" type="date" value={form.closes_at} onChange={set('closes_at')} />
            </div>
          </div>
          <div className="flex justify-end gap-2 pt-2">
            <button onClick={onClose} className="btn btn-secondary">Cancel</button>
            <button onClick={handleSubmit} disabled={!form.title || !form.granter_id || !form.total_pool || submitting} className="btn btn-primary">
              {submitting ? <Loader2 size={14} className="animate-spin" /> : null}
              Create Round
            </button>
          </div>
        </div>
      </motion.div>
    </div>
  );
}