initial commit: rocket.new export of broadcastbeat
This commit is contained in:
168
src/app/dashboard/billing/components/OverviewTab.tsx
Normal file
168
src/app/dashboard/billing/components/OverviewTab.tsx
Normal file
@@ -0,0 +1,168 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts';
|
||||
|
||||
interface Props {
|
||||
companyId?: string;
|
||||
selectedCompany: 'rmp' | 'jtr';
|
||||
}
|
||||
|
||||
interface Stats {
|
||||
revenueThisMonth: number;
|
||||
revenueLastMonth: number;
|
||||
revenueChange: number;
|
||||
outstandingCount: number;
|
||||
outstandingAmount: number;
|
||||
overdueCount: number;
|
||||
overdueAmount: number;
|
||||
paidThisMonthAmount: number;
|
||||
upcomingCount: number;
|
||||
upcomingAmount: number;
|
||||
}
|
||||
|
||||
function fmt(n: number) {
|
||||
return new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', maximumFractionDigits: 0 }).format(n);
|
||||
}
|
||||
|
||||
export default function OverviewTab({ companyId, selectedCompany }: Props) {
|
||||
const [stats, setStats] = useState<Stats | null>(null);
|
||||
const [monthlyRevenue, setMonthlyRevenue] = useState<{ month: string; revenue: number }[]>([]);
|
||||
const [recentInvoices, setRecentInvoices] = useState<any[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(true);
|
||||
const params = companyId ? `?companyId=${companyId}` : '';
|
||||
Promise.all([
|
||||
fetch(`/api/billing/stats${params}`).then(r => r.json()),
|
||||
fetch(`/api/billing/invoices${params}&limit=10`).then(r => r.json()),
|
||||
]).then(([statsData, invData]) => {
|
||||
setStats(statsData.stats);
|
||||
setMonthlyRevenue(statsData.monthlyRevenue || []);
|
||||
setRecentInvoices((invData.data || []).slice(0, 10));
|
||||
}).finally(() => setLoading(false));
|
||||
}, [companyId]);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-64">
|
||||
<div className="w-6 h-6 border-2 border-blue-500 border-t-transparent rounded-full animate-spin" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const cards = [
|
||||
{
|
||||
label: 'Revenue This Month',
|
||||
value: fmt(stats?.revenueThisMonth || 0),
|
||||
sub: stats?.revenueChange !== undefined
|
||||
? `${stats.revenueChange >= 0 ? '+' : ''}${stats.revenueChange.toFixed(1)}% vs last month`
|
||||
: '',
|
||||
color: 'text-green-400',
|
||||
bg: 'bg-green-900/20 border-green-800',
|
||||
},
|
||||
{
|
||||
label: 'Outstanding Invoices',
|
||||
value: fmt(stats?.outstandingAmount || 0),
|
||||
sub: `${stats?.outstandingCount || 0} invoices`,
|
||||
color: 'text-blue-400',
|
||||
bg: 'bg-blue-900/20 border-blue-800',
|
||||
},
|
||||
{
|
||||
label: 'Overdue',
|
||||
value: fmt(stats?.overdueAmount || 0),
|
||||
sub: `${stats?.overdueCount || 0} invoices`,
|
||||
color: 'text-red-400',
|
||||
bg: 'bg-red-900/20 border-red-800',
|
||||
},
|
||||
{
|
||||
label: 'Paid This Month',
|
||||
value: fmt(stats?.paidThisMonthAmount || 0),
|
||||
sub: '',
|
||||
color: 'text-emerald-400',
|
||||
bg: 'bg-emerald-900/20 border-emerald-800',
|
||||
},
|
||||
{
|
||||
label: 'Due in 7 Days',
|
||||
value: fmt(stats?.upcomingAmount || 0),
|
||||
sub: `${stats?.upcomingCount || 0} invoices`,
|
||||
color: 'text-yellow-400',
|
||||
bg: 'bg-yellow-900/20 border-yellow-800',
|
||||
},
|
||||
];
|
||||
|
||||
const statusColors: Record<string, string> = {
|
||||
draft: 'bg-gray-700 text-gray-300',
|
||||
sent: 'bg-blue-900 text-blue-300',
|
||||
viewed: 'bg-purple-900 text-purple-300',
|
||||
paid: 'bg-green-900 text-green-300',
|
||||
overdue: 'bg-red-900 text-red-300',
|
||||
cancelled: 'bg-gray-800 text-gray-400',
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Summary Cards */}
|
||||
<div className="grid grid-cols-2 lg:grid-cols-5 gap-4">
|
||||
{cards.map(card => (
|
||||
<div key={card.label} className={`rounded-xl border p-4 ${card.bg}`}>
|
||||
<p className="text-xs text-gray-400 mb-1">{card.label}</p>
|
||||
<p className={`text-xl font-bold ${card.color}`}>{card.value}</p>
|
||||
{card.sub && <p className="text-xs text-gray-500 mt-1">{card.sub}</p>}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Revenue Chart */}
|
||||
<div className="bg-gray-900 border border-gray-800 rounded-xl p-5">
|
||||
<h3 className="text-sm font-semibold text-gray-300 mb-4">Revenue — Last 12 Months</h3>
|
||||
<ResponsiveContainer width="100%" height={220}>
|
||||
<LineChart data={monthlyRevenue}>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="#374151" />
|
||||
<XAxis dataKey="month" tick={{ fill: '#9ca3af', fontSize: 11 }} />
|
||||
<YAxis tick={{ fill: '#9ca3af', fontSize: 11 }} tickFormatter={v => `$${(v / 1000).toFixed(0)}k`} />
|
||||
<Tooltip
|
||||
contentStyle={{ background: '#1f2937', border: '1px solid #374151', borderRadius: 8 }}
|
||||
labelStyle={{ color: '#e5e7eb' }}
|
||||
formatter={(v: any) => [fmt(v), 'Revenue']}
|
||||
/>
|
||||
<Line
|
||||
type="monotone"
|
||||
dataKey="revenue"
|
||||
stroke={selectedCompany === 'rmp' ? '#3b82f6' : '#a855f7'}
|
||||
strokeWidth={2}
|
||||
dot={{ fill: selectedCompany === 'rmp' ? '#3b82f6' : '#a855f7', r: 3 }}
|
||||
/>
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
|
||||
{/* Recent Activity */}
|
||||
<div className="bg-gray-900 border border-gray-800 rounded-xl p-5">
|
||||
<h3 className="text-sm font-semibold text-gray-300 mb-4">Recent Invoice Activity</h3>
|
||||
{recentInvoices.length === 0 ? (
|
||||
<p className="text-gray-500 text-sm text-center py-8">No recent activity. Sync from MoonInvoice or create your first invoice.</p>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{recentInvoices.map((inv: any) => (
|
||||
<div key={inv.id} className="flex items-center justify-between py-2 border-b border-gray-800 last:border-0">
|
||||
<div className="flex items-center gap-3">
|
||||
<span className={`px-2 py-0.5 rounded text-xs font-medium ${statusColors[inv.status] || 'bg-gray-700 text-gray-300'}`}>
|
||||
{inv.status}
|
||||
</span>
|
||||
<div>
|
||||
<span className="text-sm text-gray-200">{inv.invoice_number}</span>
|
||||
<span className="text-gray-500 mx-2">·</span>
|
||||
<span className="text-sm text-gray-400">{inv.billing_clients?.name || '—'}</span>
|
||||
</div>
|
||||
</div>
|
||||
<span className="text-sm font-medium text-gray-200">{fmt(inv.total || 0)}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user