Files
NeoCom/src/app/(dashboard)/revenue/billing/page.tsx
2026-04-09 20:36:10 -07:00

733 lines
34 KiB
TypeScript

'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<Subscription[]>([]);
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<string | null>(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: <TrendingUp className="w-6 h-6" />,
color: 'bg-green-900/15 text-green-700',
},
{
label: 'Collections (MTD)',
value: formatCompactCurrency(collections),
change: failedPayments > 0 ? `-${failedPayments} failed` : undefined,
icon: <ArrowUpRight className="w-6 h-6" />,
color: 'bg-blue-900/15 text-blue-700',
},
{
label: 'MRR',
value: formatCompactCurrency(totalMRR),
change: `${Number(mrrGrowth) > 0 ? '+' : ''}${mrrGrowth}%`,
icon: <Users className="w-6 h-6" />,
color: 'bg-purple-900/15 text-purple-700',
},
{
label: 'ARR',
value: formatCompactCurrency(arr),
change: `Churn: ${churnRate}%`,
icon: <AlertCircle className="w-6 h-6" />,
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 (
<div className="min-h-screen bg-[#111114] p-8 flex items-center justify-center">
<div className="flex flex-col items-center gap-4">
<Loader2 className="w-8 h-8 animate-spin text-blue-600" />
<p className="text-[#8B8B9E]">Loading billing data...</p>
</div>
</div>
);
}
return (
<div className="min-h-screen bg-[#111114] p-8">
<div className="max-w-7xl mx-auto">
{/* Header */}
<div className="flex justify-between items-center mb-8">
<div>
<h1 className="text-4xl font-bold text-[#F0F0F3]">Billing & Revenue</h1>
<p className="text-[#8B8B9E] mt-2">Manage invoices and subscription billing</p>
</div>
<div className="flex gap-3">
<button
onClick={() => setShowNewSubscriptionForm(true)}
className="flex items-center gap-2 bg-[#1A1A1F] border border-[#3A3A45] hover:bg-[#111114] text-[#F0F0F3] px-4 py-2 rounded-lg font-medium transition"
>
<Plus className="w-5 h-5" />
New Subscription
</button>
<Link href="/finance/ar/invoice/new">
<button className="flex items-center gap-2 bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg font-medium transition">
<Plus className="w-5 h-5" />
New Invoice
</button>
</Link>
</div>
</div>
{/* Summary Cards */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-8">
{summaryCards.map((card, index) => (
<div
key={index}
className={`${card.color} rounded-lg p-6 border border-[#2A2A32] shadow-sm`}
>
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium opacity-75">{card.label}</p>
<p className="text-2xl font-bold mt-2">{card.value}</p>
{card.change && (
<p className="text-xs font-semibold mt-1 opacity-75">{card.change}</p>
)}
</div>
<div className="opacity-20">{card.icon}</div>
</div>
</div>
))}
</div>
{/* Tabs */}
<div className="mb-6 border-b border-[#2A2A32] bg-[#1A1A1F] rounded-t-lg">
<div className="flex">
{(['subscriptions', 'analytics', 'billing', 'activity'] as const).map((tab) => (
<button
key={tab}
onClick={() => setActiveTab(tab)}
className={`px-6 py-4 font-medium text-sm transition ${
activeTab === tab
? 'border-b-2 border-blue-600 text-blue-600'
: 'text-[#8B8B9E] hover:text-[#F0F0F3]'
}`}
>
{tab === 'subscriptions' ? 'Subscriptions' : tab === 'analytics' ? 'Analytics' : tab === 'billing' ? 'Billing Run' : 'Activity'}
</button>
))}
</div>
</div>
{/* Subscriptions Table */}
{activeTab === 'subscriptions' && (
<div className="bg-[#1A1A1F] rounded-b-lg border border-[#2A2A32] border-t-0 shadow-sm">
<div className="px-6 py-4 border-b border-[#2A2A32] flex justify-between items-center">
<button
onClick={handleExportBilling}
className="text-sm font-medium text-blue-600 hover:text-blue-700 transition"
>
Export as CSV
</button>
{showNewSubscriptionForm && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
<div className="bg-[#1A1A1F] rounded-lg p-6 max-w-md w-full max-h-[90vh] overflow-y-auto">
<div className="flex justify-between items-center mb-4">
<h3 className="text-lg font-bold">New Subscription</h3>
<button onClick={() => setShowNewSubscriptionForm(false)} className="text-[#5A5A6E] hover:text-[#8B8B9E]">
<X className="w-5 h-5" />
</button>
</div>
<form onSubmit={handleNewSubscription} className="space-y-4">
<div>
<label className="block text-sm font-medium text-[#8B8B9E] mb-1">Customer</label>
<input
type="text"
value={newSubscription.customer}
onChange={(e) => 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
/>
</div>
<div>
<label className="block text-sm font-medium text-[#8B8B9E] mb-1">Plan</label>
<select
value={newSubscription.plan}
onChange={(e) => setNewSubscription({...newSubscription, plan: e.target.value as any})}
className="w-full px-3 py-2 border border-[#3A3A45] rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<option value="Starter">Starter</option>
<option value="Growth">Growth</option>
<option value="Enterprise">Enterprise</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-[#8B8B9E] mb-1">Billing Cycle</label>
<select
value={newSubscription.billingCycle}
onChange={(e) => setNewSubscription({...newSubscription, billingCycle: e.target.value as any})}
className="w-full px-3 py-2 border border-[#3A3A45] rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<option value="Monthly">Monthly</option>
<option value="Quarterly">Quarterly</option>
<option value="Annual">Annual</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-[#8B8B9E] mb-1">Amount</label>
<input
type="number"
value={newSubscription.amount}
onChange={(e) => 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
/>
</div>
<div>
<label className="block text-sm font-medium text-[#8B8B9E] mb-1">Start Date</label>
<input
type="date"
value={newSubscription.startDate}
onChange={(e) => 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
/>
</div>
<div className="flex items-center gap-2">
<input
type="checkbox"
id="autoRenew"
checked={newSubscription.autoRenew}
onChange={(e) => setNewSubscription({...newSubscription, autoRenew: e.target.checked})}
className="w-4 h-4"
/>
<label htmlFor="autoRenew" className="text-sm font-medium text-[#8B8B9E]">Auto-Renew</label>
</div>
<div className="flex gap-2 justify-end pt-4">
<button
type="button"
onClick={() => setShowNewSubscriptionForm(false)}
className="px-4 py-2 text-[#8B8B9E] border border-[#3A3A45] rounded-lg hover:bg-[#111114]"
>
Cancel
</button>
<button
type="submit"
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700"
>
Create
</button>
</div>
</form>
</div>
</div>
)}
</div>
<div className="overflow-x-auto">
<table className="w-full">
<thead className="bg-[#111114] border-b border-[#2A2A32]">
<tr>
<th className="px-6 py-3 text-left text-sm font-semibold text-[#F0F0F3]">Customer</th>
<th className="px-6 py-3 text-left text-sm font-semibold text-[#F0F0F3]">Plan</th>
<th className="px-6 py-3 text-right text-sm font-semibold text-[#F0F0F3]">MRR</th>
<th className="px-6 py-3 text-left text-sm font-semibold text-[#F0F0F3]">Cycle</th>
<th className="px-6 py-3 text-left text-sm font-semibold text-[#F0F0F3]">Renewal</th>
<th className="px-6 py-3 text-left text-sm font-semibold text-[#F0F0F3]">Status</th>
<th className="px-6 py-3 text-left text-sm font-semibold text-[#F0F0F3]">Payment</th>
<th className="px-6 py-3 text-left text-sm font-semibold text-[#F0F0F3]">Actions</th>
</tr>
</thead>
<tbody>
{subscriptions.length === 0 && (
<tr>
<td colSpan={8} className="px-6 py-16 text-center text-sm text-[#5A5A6E]">
No subscriptions yet. Create one to start tracking MRR.
</td>
</tr>
)}
{subscriptions.map((sub, index) => (
<tr
key={sub.id}
className={`border-b border-[#2A2A32] hover:bg-[#111114] transition ${
index % 2 === 0 ? 'bg-[#1A1A1F]' : 'bg-[#111114]'
}`}
>
<td className="px-6 py-4 text-sm font-medium">{sub.customer}</td>
<td className="px-6 py-4 text-sm">
<span className={`inline-block px-3 py-1 rounded text-xs font-semibold ${getPlanColor(sub.plan)}`}>
{sub.plan}
</span>
</td>
<td className="px-6 py-4 text-sm text-right font-medium">{formatCurrency(sub.mrr)}</td>
<td className="px-6 py-4 text-sm text-[#8B8B9E]">{sub.billingCycle}</td>
<td className="px-6 py-4 text-sm text-[#8B8B9E]">
{new Date(sub.renewalDate).toLocaleDateString()}
{sub.autoRenew && <span className="ml-2 text-xs bg-green-900/25 text-green-800 px-2 py-1 rounded">Auto</span>}
</td>
<td className="px-6 py-4 text-sm">
<span
className={`inline-block px-3 py-1 rounded-full text-xs font-semibold ${getStatusBadgeColor(sub.status)}`}
>
{sub.status}
</span>
</td>
<td className="px-6 py-4 text-sm">
<span
className={`inline-block px-3 py-1 rounded-full text-xs font-semibold ${getStatusBadgeColor(sub.paymentStatus || 'Paid')}`}
>
{sub.paymentStatus}
</span>
</td>
<td className="px-6 py-4 text-sm flex gap-2">
<button
onClick={() => handleGenerateInvoice(sub.id)}
className="text-blue-600 hover:text-blue-700 font-medium text-xs"
>
Invoice
</button>
{sub.paymentStatus === 'Failed' && (
<button
onClick={() => {
setSelectedSubscription(sub.id);
setShowDunningModal(true);
}}
className="text-orange-600 hover:text-orange-700 font-medium text-xs"
>
Retry
</button>
)}
<button
onClick={() => toggleSubscriptionStatus(sub.id, sub.status)}
className="text-[#8B8B9E] hover:text-[#8B8B9E] font-medium text-xs"
>
{sub.status === 'Active' ? 'Pause' : 'Reactivate'}
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
)}
{/* Analytics Tab */}
{activeTab === 'analytics' && (
<div className="bg-[#1A1A1F] rounded-b-lg border border-[#2A2A32] border-t-0 shadow-sm p-6 mb-8">
<div className="grid grid-cols-3 gap-6 mb-8">
<div className="bg-gradient-to-br from-blue-50 to-blue-100 p-6 rounded-lg border border-blue-200">
<p className="text-[#8B8B9E] text-sm font-medium mb-2">Monthly Recurring Revenue</p>
<p className="text-4xl font-bold text-blue-900" title={formatCurrency(totalMRR)}>{formatCompactCurrency(totalMRR)}</p>
<p className="text-sm text-blue-700 mt-2">MRR Growth: <span className="font-bold">{mrrGrowth}%</span></p>
</div>
<div className="bg-gradient-to-br from-green-50 to-green-100 p-6 rounded-lg border border-green-200">
<p className="text-[#8B8B9E] text-sm font-medium mb-2">Annual Recurring Revenue</p>
<p className="text-4xl font-bold text-green-900" title={formatCurrency(arr)}>{formatCompactCurrency(arr)}</p>
<p className="text-sm text-green-700 mt-2">Year-over-Year Growth</p>
</div>
<div className="bg-gradient-to-br from-purple-50 to-purple-100 p-6 rounded-lg border border-purple-200">
<p className="text-[#8B8B9E] text-sm font-medium mb-2">Net Revenue Retention</p>
<p className="text-4xl font-bold text-purple-900">{nrr.toFixed(0)}%</p>
<p className="text-sm text-purple-700 mt-2">Expansion & Retention Rate</p>
</div>
</div>
<div className="grid grid-cols-2 gap-6">
<div className="bg-orange-900/15 border border-orange-200 rounded-lg p-6">
<h3 className="font-semibold text-[#F0F0F3] mb-4">Churn Metrics</h3>
<div className="space-y-3">
<div className="flex justify-between">
<p className="text-[#8B8B9E]">Churn Rate</p>
<p className="font-bold">{churnRate}%</p>
</div>
<div className="flex justify-between">
<p className="text-[#8B8B9E]">Cancelled Subscriptions</p>
<p className="font-bold">{cancelledSubscriptions}</p>
</div>
<div className="flex justify-between">
<p className="text-[#8B8B9E]">Active Subscriptions</p>
<p className="font-bold">{activeSubscriptions.length}</p>
</div>
</div>
</div>
<div className="bg-red-900/15 border border-red-200 rounded-lg p-6">
<h3 className="font-semibold text-[#F0F0F3] mb-4">Payment Health</h3>
<div className="space-y-3">
<div className="flex justify-between">
<p className="text-[#8B8B9E]">Failed Payments</p>
<p className="font-bold text-red-600">{failedPayments}</p>
</div>
<div className="flex justify-between">
<p className="text-[#8B8B9E]">Pending Collection</p>
<p className="font-bold">{subscriptions.filter(s => s.paymentStatus === 'Pending').length}</p>
</div>
<div className="flex justify-between">
<p className="text-[#8B8B9E]">Collection Rate</p>
<p className="font-bold">{((collections / totalBilled) * 100).toFixed(1)}%</p>
</div>
</div>
</div>
</div>
</div>
)}
{/* Billing Run Tab */}
{activeTab === 'billing' && (
<div className="bg-[#1A1A1F] rounded-b-lg border border-[#2A2A32] border-t-0 shadow-sm p-6 mb-8">
<div className="bg-blue-900/15 border border-blue-200 rounded-lg p-6 mb-6">
<h3 className="text-lg font-bold text-[#F0F0F3] mb-4">Generate Invoices for Billing Cycle</h3>
<div className="flex items-center gap-4">
<div className="flex-1">
<p className="text-sm text-[#8B8B9E] mb-2">Select Date Range:</p>
<div className="flex gap-4">
<input type="date" defaultValue={new Date().toISOString().split('T')[0]} className="px-3 py-2 border border-[#3A3A45] rounded-lg" />
<span className="text-[#8B8B9E] flex items-center">to</span>
<input type="date" defaultValue={new Date(Date.now() + 30*24*60*60*1000).toISOString().split('T')[0]} className="px-3 py-2 border border-[#3A3A45] rounded-lg" />
</div>
</div>
<button
onClick={() => toast.success('Invoices generated for ' + activeSubscriptions.length + ' subscriptions!')}
className="bg-green-600 hover:bg-green-700 text-white px-6 py-2 rounded-lg font-medium flex items-center gap-2"
>
<RefreshCw className="w-4 h-4" />
Generate Invoices
</button>
</div>
</div>
<div>
<h3 className="text-lg font-bold text-[#F0F0F3] mb-4">Subscriptions Ready for Invoicing ({activeSubscriptions.length})</h3>
<div className="space-y-2 max-h-96 overflow-y-auto">
{activeSubscriptions.map((sub) => (
<div key={sub.id} className="bg-[#111114] border border-[#2A2A32] rounded-lg p-4 flex justify-between items-center">
<div>
<p className="font-medium text-[#F0F0F3]">{sub.customer}</p>
<p className="text-sm text-[#8B8B9E]">{sub.plan} - {sub.billingCycle}</p>
</div>
<p className="font-bold text-[#F0F0F3]">{formatCurrency(sub.mrr)}</p>
</div>
))}
</div>
</div>
</div>
)}
{/* Recent Activity Table */}
{activeTab === 'activity' && (
<div className="bg-[#1A1A1F] rounded-b-lg border border-[#2A2A32] border-t-0 shadow-sm">
<div className="overflow-x-auto">
<table className="w-full">
<thead className="bg-[#111114] border-b border-[#2A2A32]">
<tr>
<th className="px-6 py-3 text-left text-sm font-semibold text-[#F0F0F3]">Customer</th>
<th className="px-6 py-3 text-left text-sm font-semibold text-[#F0F0F3]">Type</th>
<th className="px-6 py-3 text-right text-sm font-semibold text-[#F0F0F3]">Amount</th>
<th className="px-6 py-3 text-left text-sm font-semibold text-[#F0F0F3]">Date</th>
<th className="px-6 py-3 text-left text-sm font-semibold text-[#F0F0F3]">Status</th>
</tr>
</thead>
<tbody>
{recentActivity.length === 0 && (
<tr>
<td colSpan={5} className="px-6 py-16 text-center text-sm text-[#5A5A6E]">
No recent billing activity.
</td>
</tr>
)}
{recentActivity.map((activity, index) => (
<tr
key={activity.id}
className={`border-b border-[#2A2A32] hover:bg-[#111114] transition ${
index % 2 === 0 ? 'bg-[#1A1A1F]' : 'bg-[#111114]'
}`}
>
<td className="px-6 py-4 text-sm font-medium">{activity.customer}</td>
<td className="px-6 py-4 text-sm">{activity.type}</td>
<td className="px-6 py-4 text-sm text-right font-medium">
{formatCurrency(activity.amount)}
</td>
<td className="px-6 py-4 text-sm text-[#8B8B9E]">
{new Date(activity.date).toLocaleDateString()}
</td>
<td className="px-6 py-4 text-sm">
<span
className={`inline-block px-3 py-1 rounded-full text-xs font-semibold ${getStatusBadgeColor(
activity.status
)}`}
>
{activity.status}
</span>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
)}
{/* Dunning Management Modal */}
{showDunningModal && selectedSubscription && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
<div className="bg-[#1A1A1F] rounded-lg p-6 max-w-md w-full">
<div className="flex justify-between items-center mb-4">
<h3 className="text-lg font-bold">Dunning Management</h3>
<button onClick={() => setShowDunningModal(false)} className="text-[#5A5A6E] hover:text-[#8B8B9E]">
<X className="w-5 h-5" />
</button>
</div>
<div className="bg-orange-900/15 border border-orange-200 rounded-lg p-4 mb-4">
<p className="text-sm font-medium text-[#F0F0F3]">Failed Payment Recovery</p>
<p className="text-xs text-[#8B8B9E] mt-1">Subscription: {subscriptions.find(s => s.id === selectedSubscription)?.customer}</p>
</div>
<div className="space-y-3 mb-6">
<div className="bg-[#111114] p-3 rounded border border-[#2A2A32]">
<p className="text-sm font-medium text-[#F0F0F3]">Retry Schedule</p>
<ul className="text-xs text-[#8B8B9E] mt-2 space-y-1">
<li>- Attempt 1: Immediate</li>
<li>- Attempt 2: 3 days later</li>
<li>- Attempt 3: 7 days later</li>
<li>- Final: 14 days (cancel if failed)</li>
</ul>
</div>
</div>
<div className="flex gap-2 justify-end">
<button
onClick={() => setShowDunningModal(false)}
className="px-4 py-2 text-[#8B8B9E] border border-[#3A3A45] rounded-lg hover:bg-[#111114]"
>
Cancel
</button>
<button
onClick={() => {
setSubscriptions(subscriptions.map(s =>
s.id === selectedSubscription
? { ...s, paymentStatus: 'Pending' as const, dunningAttempts: (s.dunningAttempts || 0) + 1 }
: s
));
setShowDunningModal(false);
toast.success('Retry scheduled successfully!');
}}
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700"
>
Schedule Retry
</button>
</div>
</div>
</div>
)}
</div>
</div>
);
};
export default BillingPage;