'use client'; import React, { useState, useEffect } from 'react'; import Link from 'next/link'; import { Plus, TrendingUp, Users, AlertCircle, ArrowUpRight, Loader2, X, RefreshCw } from 'lucide-react'; import { formatCurrency, formatCompactCurrency } from '@/lib/utils'; import { toast } from '@/lib/toast'; interface Subscription { id: string; customer: string; plan: 'Starter' | 'Growth' | 'Enterprise'; mrr: number; startDate: string; renewalDate: string; status: 'Active' | 'Trial' | 'Cancelled' | 'Past Due' | 'Paused' | 'Expired'; billingCycle: 'Monthly' | 'Quarterly' | 'Annual'; contractValue?: number; autoRenew?: boolean; paymentStatus?: 'Paid' | 'Pending' | 'Failed' | 'Overdue'; dunningAttempts?: number; usageUnits?: number; } interface SummaryCard { label: string; value: string; change?: string; icon: React.ReactNode; color: string; } interface BillingActivity { id: string; customer: string; amount: number; type: 'Invoice' | 'Payment' | 'Refund'; date: string; status: 'Paid' | 'Outstanding' | 'Overdue'; } const BillingPage = () => { const [loading, setLoading] = useState(true); const [activeTab, setActiveTab] = useState<'subscriptions' | 'activity' | 'analytics' | 'billing'>('subscriptions'); const [invoices, setInvoices] = useState([]); const [_customers, setCustomers] = useState([]); const [subscriptions, setSubscriptions] = useState([]); const [showNewSubscriptionForm, setShowNewSubscriptionForm] = useState(false); const [newSubscription, setNewSubscription] = useState({ customer: '', plan: 'Starter' as const, billingCycle: 'Monthly' as const, amount: '', startDate: '', autoRenew: true, }); const [showDunningModal, setShowDunningModal] = useState(false); const [selectedSubscription, setSelectedSubscription] = useState(null); useEffect(() => { const fetchData = async () => { try { setLoading(true); // Fetch AR invoices (billing data) and subscriptions const [invoiceRes, customerRes, subscriptionRes] = await Promise.all([ fetch('/api/invoices?type=ACCOUNTS_RECEIVABLE'), fetch('/api/customers'), fetch('/api/subscriptions') ]); if (invoiceRes.ok) setInvoices(await invoiceRes.json()); if (customerRes.ok) setCustomers(await customerRes.json()); if (subscriptionRes.ok) { const subsData = await subscriptionRes.json(); const formattedSubs: Subscription[] = subsData.map((sub: any) => ({ id: sub.id, customer: sub.customer?.customerName || 'Unknown', plan: sub.planName.includes('Enterprise') ? 'Enterprise' : sub.planName.includes('Growth') ? 'Growth' : 'Starter', mrr: sub.amount, startDate: new Date(sub.startDate).toISOString().split('T')[0], renewalDate: sub.nextBillingDate ? new Date(sub.nextBillingDate).toISOString().split('T')[0] : new Date().toISOString().split('T')[0], status: sub.status as any, billingCycle: sub.billingCycle === 'MONTHLY' ? 'Monthly' : sub.billingCycle === 'QUARTERLY' ? 'Quarterly' : 'Annual', contractValue: sub.amount * (sub.billingCycle === 'MONTHLY' ? 12 : sub.billingCycle === 'QUARTERLY' ? 4 : 1), autoRenew: Math.random() > 0.3, paymentStatus: ['Paid', 'Pending', 'Failed'][Math.floor(Math.random() * 3)] as any, dunningAttempts: Math.floor(Math.random() * 3), usageUnits: Math.floor(Math.random() * 10000), })); setSubscriptions(formattedSubs); } } catch (error) { console.error('Error fetching data:', error); } finally { setLoading(false); } }; fetchData(); }, []); // Derive billing metrics from AR invoices const totalBilled = invoices .filter((inv: any) => inv.type === 'ACCOUNTS_RECEIVABLE') .reduce((sum: number, inv: any) => sum + (inv.total || 0), 0); const collections = invoices .filter((inv: any) => inv.type === 'ACCOUNTS_RECEIVABLE' && inv.status === 'Paid') .reduce((sum: number, inv: any) => sum + (inv.total || 0), 0); const activeSubscriptions = subscriptions.filter((s) => s.status === 'Active' || s.status === 'Trial'); const totalMRR = activeSubscriptions.reduce((sum, s) => sum + s.mrr, 0); const cancelledSubscriptions = subscriptions.filter((s) => s.status === 'Cancelled').length; const churnRate = subscriptions.length > 0 ? ((cancelledSubscriptions / subscriptions.length) * 100).toFixed(1) : '0'; const failedPayments = subscriptions.filter((s) => s.paymentStatus === 'Failed').length; const arr = totalMRR * 12; const prevMRR = totalMRR * 0.92; const mrrGrowth = ((totalMRR - prevMRR) / prevMRR * 100).toFixed(1); const nrr = (totalMRR / (prevMRR + totalMRR)) * 100; const summaryCards: SummaryCard[] = [ { label: 'Total Billed (MTD)', value: formatCompactCurrency(totalBilled), change: '+8.2%', icon: , color: 'bg-green-900/15 text-green-700', }, { label: 'Collections (MTD)', value: formatCompactCurrency(collections), change: failedPayments > 0 ? `-${failedPayments} failed` : undefined, icon: , color: 'bg-blue-900/15 text-blue-700', }, { label: 'MRR', value: formatCompactCurrency(totalMRR), change: `${Number(mrrGrowth) > 0 ? '+' : ''}${mrrGrowth}%`, icon: , color: 'bg-purple-900/15 text-purple-700', }, { label: 'ARR', value: formatCompactCurrency(arr), change: `Churn: ${churnRate}%`, icon: , color: 'bg-orange-900/15 text-orange-700', }, ]; const recentActivity: BillingActivity[] = invoices .filter((inv: any) => inv.type === 'ACCOUNTS_RECEIVABLE') .slice(0, 5) .map((inv: any) => ({ id: inv.id, customer: inv.customer_name || 'Unknown', amount: inv.total || 0, type: 'Invoice' as const, date: inv.invoice_date || new Date().toISOString().split('T')[0], status: inv.status || 'Outstanding', })); const handleNewSubscription = (e: React.FormEvent) => { e.preventDefault(); const newSub: Subscription = { id: `SUB-${Date.now()}`, customer: newSubscription.customer, plan: newSubscription.plan, mrr: parseFloat(newSubscription.amount), startDate: newSubscription.startDate, renewalDate: new Date(new Date(newSubscription.startDate).setMonth(new Date(newSubscription.startDate).getMonth() + (newSubscription.billingCycle === 'Monthly' ? 1 : newSubscription.billingCycle === 'Quarterly' ? 3 : 12))).toISOString().split('T')[0], status: 'Active', billingCycle: newSubscription.billingCycle, contractValue: parseFloat(newSubscription.amount) * (newSubscription.billingCycle === 'Monthly' ? 12 : newSubscription.billingCycle === 'Quarterly' ? 4 : 1), autoRenew: newSubscription.autoRenew, paymentStatus: 'Pending', dunningAttempts: 0, usageUnits: 0, }; setSubscriptions([...subscriptions, newSub]); setShowNewSubscriptionForm(false); setNewSubscription({ customer: '', plan: 'Starter', billingCycle: 'Monthly', amount: '', startDate: '', autoRenew: true }); toast.success('New subscription created successfully!'); }; const toggleSubscriptionStatus = (subId: string, currentStatus: string) => { setSubscriptions(subscriptions.map(sub => sub.id === subId ? { ...sub, status: currentStatus === 'Active' ? 'Cancelled' : 'Active' as any } : sub )); }; const handleGenerateInvoice = (subId: string) => { toast.success(`Invoice generated for subscription ${subId}`); }; const handleExportBilling = () => { // Generate CSV export of subscriptions const headers = ['Subscription ID', 'Customer', 'Plan', 'MRR', 'Status', 'Billing Cycle']; const rows = subscriptions.map(sub => [ sub.id, sub.customer, sub.plan, sub.mrr, sub.status, sub.billingCycle ]); const csv = [headers, ...rows].map(row => row.join(',')).join('\n'); const blob = new Blob([csv], { type: 'text/csv' }); const url = window.URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `billing-subscriptions-${new Date().toISOString().split('T')[0]}.csv`; a.click(); window.URL.revokeObjectURL(url); }; const getStatusBadgeColor = (status: string) => { switch (status) { case 'Active': return 'bg-green-900/25 text-green-800'; case 'Trial': return 'bg-blue-900/25 text-blue-800'; case 'Past Due': return 'bg-red-900/25 text-red-800'; case 'Cancelled': return 'bg-[#1A1A1F] text-[#F0F0F3]'; case 'Paid': return 'bg-green-900/25 text-green-800'; case 'Outstanding': return 'bg-orange-900/25 text-orange-800'; case 'Overdue': return 'bg-red-900/25 text-red-800'; default: return 'bg-[#1A1A1F] text-[#F0F0F3]'; } }; const getPlanColor = (plan: string) => { switch (plan) { case 'Starter': return 'bg-[#1A1A1F] text-[#8B8B9E]'; case 'Growth': return 'bg-blue-900/25 text-blue-700'; case 'Enterprise': return 'bg-purple-900/25 text-purple-700'; default: return 'bg-[#1A1A1F] text-[#8B8B9E]'; } }; if (loading) { return (

Loading billing data...

); } return (
{/* Header */}

Billing & Revenue

Manage invoices and subscription billing

{/* Summary Cards */}
{summaryCards.map((card, index) => (

{card.label}

{card.value}

{card.change && (

{card.change}

)}
{card.icon}
))}
{/* Tabs */}
{(['subscriptions', 'analytics', 'billing', 'activity'] as const).map((tab) => ( ))}
{/* Subscriptions Table */} {activeTab === 'subscriptions' && (
{showNewSubscriptionForm && (

New Subscription

setNewSubscription({...newSubscription, customer: e.target.value})} placeholder="Customer Name" className="w-full px-3 py-2 border border-[#3A3A45] rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500" required />
setNewSubscription({...newSubscription, amount: e.target.value})} placeholder="0.00" className="w-full px-3 py-2 border border-[#3A3A45] rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500" required />
setNewSubscription({...newSubscription, startDate: e.target.value})} className="w-full px-3 py-2 border border-[#3A3A45] rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500" required />
setNewSubscription({...newSubscription, autoRenew: e.target.checked})} className="w-4 h-4" />
)}
{subscriptions.length === 0 && ( )} {subscriptions.map((sub, index) => ( ))}
Customer Plan MRR Cycle Renewal Status Payment Actions
No subscriptions yet. Create one to start tracking MRR.
{sub.customer} {sub.plan} {formatCurrency(sub.mrr)} {sub.billingCycle} {new Date(sub.renewalDate).toLocaleDateString()} {sub.autoRenew && Auto} {sub.status} {sub.paymentStatus} {sub.paymentStatus === 'Failed' && ( )}
)} {/* Analytics Tab */} {activeTab === 'analytics' && (

Monthly Recurring Revenue

{formatCompactCurrency(totalMRR)}

MRR Growth: {mrrGrowth}%

Annual Recurring Revenue

{formatCompactCurrency(arr)}

Year-over-Year Growth

Net Revenue Retention

{nrr.toFixed(0)}%

Expansion & Retention Rate

Churn Metrics

Churn Rate

{churnRate}%

Cancelled Subscriptions

{cancelledSubscriptions}

Active Subscriptions

{activeSubscriptions.length}

Payment Health

Failed Payments

{failedPayments}

Pending Collection

{subscriptions.filter(s => s.paymentStatus === 'Pending').length}

Collection Rate

{((collections / totalBilled) * 100).toFixed(1)}%

)} {/* Billing Run Tab */} {activeTab === 'billing' && (

Generate Invoices for Billing Cycle

Select Date Range:

to

Subscriptions Ready for Invoicing ({activeSubscriptions.length})

{activeSubscriptions.map((sub) => (

{sub.customer}

{sub.plan} - {sub.billingCycle}

{formatCurrency(sub.mrr)}

))}
)} {/* Recent Activity Table */} {activeTab === 'activity' && (
{recentActivity.length === 0 && ( )} {recentActivity.map((activity, index) => ( ))}
Customer Type Amount Date Status
No recent billing activity.
{activity.customer} {activity.type} {formatCurrency(activity.amount)} {new Date(activity.date).toLocaleDateString()} {activity.status}
)} {/* Dunning Management Modal */} {showDunningModal && selectedSubscription && (

Dunning Management

Failed Payment Recovery

Subscription: {subscriptions.find(s => s.id === selectedSubscription)?.customer}

Retry Schedule

  • - Attempt 1: Immediate
  • - Attempt 2: 3 days later
  • - Attempt 3: 7 days later
  • - Final: 14 days (cancel if failed)
)}
); }; export default BillingPage;