initial commit: rocket.new export of broadcastbeat

This commit is contained in:
Ryan Salazar
2026-05-07 16:39:17 +00:00
commit 70aa1ad46e
302 changed files with 60710 additions and 0 deletions

View 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>
);
}