← back to Freddy

components/impact/ImpactTab.tsx

177 lines

'use client';

import { useState, useEffect, useCallback } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { SkeletonList } from '../Skeleton';
import { useToast } from '../ToastProvider';
import {
  TrendingUp, Users, DollarSign, RefreshCw,
  Building2, Flame, CheckCircle, FileText,
} from 'lucide-react';
import SortDropdown from '../shared/SortDropdown';
import { useClientSort, SortConfig } from '@/hooks/useClientSort';

interface ImpactReport {
  id: string;
  title: string;
  description: string | null;
  people_helped: number;
  funds_used: string | null;
  outcomes: string[] | null;
  report_url: string | null;
  status: string;
  period_start: string | null;
  period_end: string | null;
  created_at: string;
  org_name: string;
  cause_title: string | null;
  match_score: number | null;
}

const SORT_OPTIONS = [
  { value: 'date_desc', label: 'Newest First' },
  { value: 'date_asc', label: 'Oldest First' },
  { value: 'people_desc', label: 'Most People Helped' },
  { value: 'status', label: 'Status A-Z' },
];

const SORT_CONFIGS: Record<string, SortConfig> = {
  date_desc:   { key: 'created_at', direction: 'desc', type: 'date' },
  date_asc:    { key: 'created_at', direction: 'asc', type: 'date' },
  people_desc: { key: 'people_helped', direction: 'desc', type: 'number' },
  status:      { key: 'status', direction: 'asc', type: '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 ImpactTab() {
  const { addToast } = useToast();
  const [reports, setReports] = useState<ImpactReport[]>([]);
  const [loading, setLoading] = useState(true);
  const [sortBy, setSortBy] = useState('date_desc');
  const sortedReports = useClientSort(reports, sortBy, SORT_CONFIGS);

  const fetchReports = useCallback(async () => {
    try {
      const res = await fetch('/api/impact', { credentials: 'include' });
      if (res.ok) {
        const data = await res.json();
        setReports(data.impact_reports || []);
      }
    } catch (err) {
      console.error('Failed to fetch impact reports:', err);
      addToast('Failed to load impact reports', 'error');
    } finally {
      setLoading(false);
    }
  }, []);

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

  const fmtMoney = (val: string | null) => {
    if (!val) return 'N/A';
    const n = parseFloat(val);
    return `$${n.toLocaleString()}`;
  };

  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)' }}>
            <TrendingUp size={20} style={{ color: '#22c55e' }} />
            Impact Reports
          </h2>
          <p className="text-sm mt-1" style={{ color: 'var(--color-text-muted)' }}>
            Tracking real-world outcomes from funded matches
          </p>
        </div>
        <div className="flex items-center gap-2">
          <SortDropdown options={SORT_OPTIONS} value={sortBy} onChange={setSortBy} />
          <button onClick={fetchReports} className="btn btn-secondary btn-sm">
            <RefreshCw size={14} /> Refresh
          </button>
        </div>
      </div>

      {loading ? (
        <SkeletonList count={4} />
      ) : sortedReports.length > 0 ? (
        <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
          <AnimatePresence>
            {sortedReports.map((report, i) => (
              <motion.div key={report.id} custom={i} initial="hidden" animate="visible" variants={fadeIn} className="card p-5 flex flex-col gap-3">
                <div className="flex items-start justify-between">
                  <div>
                    <h3 className="text-sm font-semibold" style={{ color: 'var(--color-text)' }}>{report.title}</h3>
                    <div className="flex items-center gap-2 mt-1 text-xs" style={{ color: 'var(--color-text-muted)' }}>
                      <Building2 size={10} /> {report.org_name}
                      {report.cause_title && <><Flame size={10} /> {report.cause_title}</>}
                    </div>
                  </div>
                  <span className={`badge ${report.status === 'published' ? 'badge-success' : 'badge-warning'}`}>
                    {report.status}
                  </span>
                </div>

                {report.description && (
                  <p className="text-xs" style={{ color: 'var(--color-text-secondary)' }}>{report.description}</p>
                )}

                <div className="grid grid-cols-2 gap-2">
                  <div className="text-center p-2 rounded-lg" style={{ backgroundColor: 'var(--color-surface-el)' }}>
                    <Users size={14} style={{ color: '#fbbf24', margin: '0 auto 2px' }} />
                    <div className="text-sm font-bold" style={{ color: 'var(--color-text)' }}>{(report.people_helped || 0).toLocaleString()}</div>
                    <div className="text-xs" style={{ color: 'var(--color-text-muted)' }}>People Helped</div>
                  </div>
                  <div className="text-center p-2 rounded-lg" style={{ backgroundColor: 'var(--color-surface-el)' }}>
                    <DollarSign size={14} style={{ color: '#22c55e', margin: '0 auto 2px' }} />
                    <div className="text-sm font-bold" style={{ color: 'var(--color-text)' }}>{fmtMoney(report.funds_used)}</div>
                    <div className="text-xs" style={{ color: 'var(--color-text-muted)' }}>Funds Used</div>
                  </div>
                </div>

                {report.outcomes && report.outcomes.length > 0 && (
                  <div>
                    <div className="text-xs font-semibold mb-1" style={{ color: 'var(--color-text-secondary)' }}>Outcomes</div>
                    <ul className="space-y-1">
                      {report.outcomes.map((outcome, j) => (
                        <li key={j} className="flex items-start gap-2 text-xs" style={{ color: 'var(--color-text-muted)' }}>
                          <CheckCircle size={10} style={{ color: '#22c55e', marginTop: 2, flexShrink: 0 }} />
                          {outcome}
                        </li>
                      ))}
                    </ul>
                  </div>
                )}

                {report.period_start && (
                  <div className="text-xs" style={{ color: 'var(--color-text-muted)' }}>
                    Period: {new Date(report.period_start).toLocaleDateString()} - {report.period_end ? new Date(report.period_end).toLocaleDateString() : 'Ongoing'}
                  </div>
                )}
              </motion.div>
            ))}
          </AnimatePresence>
        </div>
      ) : (
        <div className="text-center py-12">
          <div className="w-16 h-16 rounded-2xl flex items-center justify-center mx-auto mb-4" style={{ backgroundColor: 'var(--color-surface-el)', border: '1px solid var(--color-border)' }}>
            <TrendingUp size={28} style={{ color: 'var(--color-text-muted)' }} />
          </div>
          <h3 className="text-lg font-semibold mb-2" style={{ color: 'var(--color-text)' }}>No Impact Reports Yet</h3>
          <p className="text-sm" style={{ color: 'var(--color-text-muted)' }}>
            Impact reports will appear here as funded organizations report their outcomes.
          </p>
        </div>
      )}
    </div>
  );
}