← back to Freddy

components/revenue/RevenueTab.tsx

254 lines

'use client';

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

interface Revenue {
  id: string;
  match_id: string | null;
  org_id: string | null;
  granter_id: string | null;
  fee_type: string;
  flat_fee: string;
  percentage_rate: number;
  grant_amount: string | null;
  commission_amount: string | null;
  status: string;
  invoice_date: string | null;
  paid_date: string | null;
  notes: string | null;
  created_at: string;
  cause_title?: string;
  org_name?: string;
  granter_name?: string;
}

const SORT_OPTIONS = [
  { value: 'date_desc', label: 'Newest First' },
  { value: 'date_asc', label: 'Oldest First' },
  { value: 'commission_desc', label: 'Highest Commission' },
  { value: 'fee_type', label: 'Fee Type 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' },
  commission_desc: { key: 'commission_amount', direction: 'desc', type: 'number' },
  fee_type:        { key: 'fee_type', direction: 'asc', type: 'string' },
};

const STATUS_FILTERS = ['all', 'pending', 'invoiced', 'paid', 'waived', 'overdue'];

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 RevenueTab() {
  const { addToast } = useToast();
  const [revenue, setRevenue] = useState<Revenue[]>([]);
  const [loading, setLoading] = useState(true);
  const [statusFilter, setStatusFilter] = useState('all');
  const [stats, setStats] = useState({ total: 0, pending: 0, invoiced: 0, paid: 0 });
  const [sortBy, setSortBy] = useState('date_desc');
  const sortedRevenue = useClientSort(revenue, sortBy, SORT_CONFIGS);

  const fetchRevenue = useCallback(async () => {
    try {
      const params = new URLSearchParams();
      if (statusFilter !== 'all') params.set('status', statusFilter);
      const res = await fetch(`/api/revenue?${params}`, { credentials: 'include' });
      if (res.ok) {
        const data = await res.json();
        setRevenue(data.revenue || []);
        setStats(data.stats || { total: 0, pending: 0, invoiced: 0, paid: 0 });
      }
    } catch (err) {
      console.error('Failed to fetch revenue:', err);
      addToast('Failed to load revenue data', 'error');
    } finally {
      setLoading(false);
    }
  }, [statusFilter]);

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

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

  const statusIcon = (s: string) => {
    switch (s) {
      case 'paid': return <CheckCircle size={14} style={{ color: '#22c55e' }} />;
      case 'invoiced': return <FileText size={14} style={{ color: '#60a5fa' }} />;
      case 'overdue': return <AlertCircle size={14} style={{ color: '#ef4444' }} />;
      default: return <Clock size={14} style={{ color: '#fbbf24' }} />;
    }
  };

  const statusBadge = (s: string) => {
    switch (s) {
      case 'paid': return 'badge-success';
      case 'invoiced': return 'badge-blue';
      case 'overdue': return 'badge-error';
      case 'waived': return 'badge-purple';
      default: return 'badge-warning';
    }
  };

  const feeLabel = (t: string) => {
    switch (t) {
      case 'introduction_fee': return 'Introduction Fee';
      case 'success_fee': return 'Success Fee';
      case 'subscription': return 'Subscription';
      case 'consulting': return 'Consulting';
      default: return t;
    }
  };

  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: '#d97706' }} />
            Revenue
          </h2>
          <p className="text-sm mt-1" style={{ color: 'var(--color-text-muted)' }}>
            Fee tracking, commissions, and payment status
          </p>
        </div>
        <button onClick={fetchRevenue} className="btn btn-secondary btn-sm">
          <RefreshCw size={14} /> Refresh
        </button>
      </div>

      {/* Stats cards */}
      <div className="grid grid-cols-2 md:grid-cols-4 gap-3">
        <div className="stat-card">
          <div className="flex items-center gap-2 mb-2">
            <TrendingUp size={14} style={{ color: '#22c55e' }} />
            <span className="text-xs" style={{ color: 'var(--color-text-muted)' }}>Total Earned</span>
          </div>
          <div className="text-xl font-bold" style={{ color: '#22c55e' }}>{fmtMoney(stats.total)}</div>
        </div>
        <div className="stat-card">
          <div className="flex items-center gap-2 mb-2">
            <Clock size={14} style={{ color: '#fbbf24' }} />
            <span className="text-xs" style={{ color: 'var(--color-text-muted)' }}>Pending</span>
          </div>
          <div className="text-xl font-bold" style={{ color: '#fbbf24' }}>{fmtMoney(stats.pending)}</div>
        </div>
        <div className="stat-card">
          <div className="flex items-center gap-2 mb-2">
            <FileText size={14} style={{ color: '#60a5fa' }} />
            <span className="text-xs" style={{ color: 'var(--color-text-muted)' }}>Invoiced</span>
          </div>
          <div className="text-xl font-bold" style={{ color: '#60a5fa' }}>{fmtMoney(stats.invoiced)}</div>
        </div>
        <div className="stat-card">
          <div className="flex items-center gap-2 mb-2">
            <CheckCircle size={14} style={{ color: '#22c55e' }} />
            <span className="text-xs" style={{ color: 'var(--color-text-muted)' }}>Paid</span>
          </div>
          <div className="text-xl font-bold" style={{ color: '#22c55e' }}>{fmtMoney(stats.paid)}</div>
        </div>
      </div>

      {/* Status filter */}
      <div className="flex items-center gap-2 flex-wrap">
        <SortDropdown options={SORT_OPTIONS} value={sortBy} onChange={setSortBy} />
        {STATUS_FILTERS.map((s) => (
          <button
            key={s}
            onClick={() => setStatusFilter(s)}
            className="btn btn-sm"
            style={{
              backgroundColor: statusFilter === s ? 'rgba(217, 119, 6, 0.2)' : 'var(--color-surface-el)',
              color: statusFilter === s ? '#fbbf24' : 'var(--color-text-secondary)',
              borderColor: statusFilter === s ? 'rgba(217, 119, 6, 0.3)' : 'var(--color-border)',
            }}
          >
            {s === 'all' ? 'All' : s}
          </button>
        ))}
      </div>

      {/* Revenue list */}
      {loading ? (
        <SkeletonList count={4} />
      ) : (
        <div className="space-y-3">
          <AnimatePresence>
            {sortedRevenue.map((r, i) => (
              <motion.div key={r.id} custom={i} initial="hidden" animate="visible" variants={fadeIn} className="card p-4">
                <div className="flex items-center justify-between flex-wrap gap-3">
                  <div className="flex items-center gap-3">
                    {statusIcon(r.status)}
                    <div>
                      <div className="text-sm font-semibold" style={{ color: 'var(--color-text)' }}>
                        {feeLabel(r.fee_type)}
                      </div>
                      <div className="text-xs" style={{ color: 'var(--color-text-muted)' }}>
                        {r.org_name && `Org: ${r.org_name}`}
                        {r.granter_name && ` | Granter: ${r.granter_name}`}
                        {r.cause_title && ` | Cause: ${r.cause_title}`}
                      </div>
                    </div>
                  </div>

                  <div className="flex items-center gap-4">
                    <div className="text-right">
                      {parseFloat(r.flat_fee) > 0 && (
                        <div className="text-xs" style={{ color: 'var(--color-text-muted)' }}>
                          Flat: {fmtMoney(r.flat_fee)}
                        </div>
                      )}
                      {r.percentage_rate > 0 && r.grant_amount && (
                        <div className="text-xs" style={{ color: 'var(--color-text-muted)' }}>
                          {r.percentage_rate}% of {fmtMoney(r.grant_amount)}
                        </div>
                      )}
                      {r.commission_amount && (
                        <div className="text-sm font-bold" style={{ color: '#fbbf24' }}>
                          {fmtMoney(r.commission_amount)}
                        </div>
                      )}
                    </div>
                    <span className={`badge ${statusBadge(r.status)}`}>{r.status}</span>
                  </div>
                </div>
                {r.notes && (
                  <p className="text-xs mt-2" style={{ color: 'var(--color-text-muted)' }}>{r.notes}</p>
                )}
              </motion.div>
            ))}
          </AnimatePresence>
        </div>
      )}

      {!loading && revenue.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 revenue entries yet. Revenue is generated when matches lead to funded introductions.
          </p>
        </div>
      )}
    </div>
  );
}