158 lines
8.0 KiB
TypeScript
158 lines
8.0 KiB
TypeScript
'use client';
|
|
import React, { useState, useEffect } from 'react';
|
|
import Link from 'next/link';
|
|
import { useAuth } from '@/contexts/AuthContext';
|
|
import { useRouter } from 'next/navigation';
|
|
|
|
const STAT_CARDS = [
|
|
{ key: 'revenue_ytd', label: 'Revenue YTD', color: 'text-green-400', bg: 'bg-green-400/10', href: '/admin/accounting/invoices' },
|
|
{ key: 'expenses_ytd', label: 'Expenses YTD', color: 'text-red-400', bg: 'bg-red-400/10', href: '/admin/accounting/expenses' },
|
|
{ key: 'net_profit', label: 'Net Profit YTD', color: 'text-blue-400', bg: 'bg-blue-400/10', href: '/admin/accounting/reports' },
|
|
{ key: 'outstanding', label: 'Outstanding Invoices', color: 'text-yellow-400', bg: 'bg-yellow-400/10', href: '/admin/accounting/invoices?status=pending' },
|
|
{ key: 'unreconciled', label: 'Unreconciled Transactions', color: 'text-orange-400', bg: 'bg-orange-400/10', href: '/admin/accounting/reconciliation' },
|
|
{ key: 'commissions_outstanding', label: 'Commissions Outstanding', color: 'text-purple-400', bg: 'bg-purple-400/10', href: '/admin/accounting/commissions' },
|
|
];
|
|
|
|
function fmt(cents: number) {
|
|
return '$' + (cents / 100).toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
|
}
|
|
|
|
export default function AccountingDashboard() {
|
|
const { user, loading } = useAuth();
|
|
const router = useRouter();
|
|
const [stats, setStats] = useState<Record<string, number>>({});
|
|
const [siteRevenue, setSiteRevenue] = useState<any[]>([]);
|
|
const [topClients, setTopClients] = useState<any[]>([]);
|
|
const [loadingData, setLoadingData] = useState(true);
|
|
|
|
useEffect(() => {
|
|
if (!loading && !user) router.push('/login');
|
|
}, [user, loading, router]);
|
|
|
|
useEffect(() => {
|
|
if (!user) return;
|
|
fetch('/api/accounting/dashboard')
|
|
.then(r => r.json())
|
|
.then(d => {
|
|
setStats(d.stats ?? {});
|
|
setSiteRevenue(d.siteRevenue ?? []);
|
|
setTopClients(d.topClients ?? []);
|
|
})
|
|
.catch(() => {})
|
|
.finally(() => setLoadingData(false));
|
|
}, [user]);
|
|
|
|
if (loading || loadingData) return (
|
|
<div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center">
|
|
<div className="w-8 h-8 border-2 border-[#3b82f6] border-t-transparent rounded-full animate-spin" />
|
|
</div>
|
|
);
|
|
if (!user) return null;
|
|
|
|
return (
|
|
<div className="min-h-screen bg-[#0a0a0a] text-white p-6">
|
|
<div className="max-w-7xl mx-auto space-y-6">
|
|
{/* Header */}
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<h1 className="text-2xl font-bold text-white">Accounting Dashboard</h1>
|
|
<p className="text-[#888] text-sm mt-1">Relevant Media Properties, LLC</p>
|
|
</div>
|
|
<div className="flex gap-2 flex-wrap">
|
|
<Link href="/admin/accounting/clients" className="px-3 py-1.5 bg-[#1a2535] border border-[#2a3a50] rounded text-xs text-[#3b82f6] hover:bg-[#0d1a2d] transition-colors">+ New Client</Link>
|
|
<Link href="/admin/accounting/orders" className="px-3 py-1.5 bg-[#1a2535] border border-[#2a3a50] rounded text-xs text-[#3b82f6] hover:bg-[#0d1a2d] transition-colors">+ New Order</Link>
|
|
<Link href="/admin/accounting/invoices" className="px-3 py-1.5 bg-[#1a2535] border border-[#2a3a50] rounded text-xs text-[#3b82f6] hover:bg-[#0d1a2d] transition-colors">+ New Invoice</Link>
|
|
<Link href="/admin/accounting/expenses" className="px-3 py-1.5 bg-[#1a2535] border border-[#2a3a50] rounded text-xs text-[#3b82f6] hover:bg-[#0d1a2d] transition-colors">+ Log Expense</Link>
|
|
<Link href="/admin/accounting/reconciliation" className="px-3 py-1.5 bg-[#1a2535] border border-[#2a3a50] rounded text-xs text-[#3b82f6] hover:bg-[#0d1a2d] transition-colors">Upload Bank CSV</Link>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Stat Cards */}
|
|
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-3">
|
|
{STAT_CARDS.map(c => (
|
|
<Link key={c.key} href={c.href} className="bg-[#111] border border-[#252525] rounded-lg p-4 hover:border-[#333] transition-colors">
|
|
<p className={`text-lg font-bold ${c.color}`}>{fmt(stats[c.key] ?? 0)}</p>
|
|
<p className="text-xs text-[#888] mt-1">{c.label}</p>
|
|
</Link>
|
|
))}
|
|
</div>
|
|
|
|
{/* Site Revenue Table */}
|
|
<div className="bg-[#111] border border-[#252525] rounded-lg overflow-hidden">
|
|
<div className="px-4 py-3 border-b border-[#252525]">
|
|
<h2 className="text-sm font-semibold text-white">Revenue by Site</h2>
|
|
</div>
|
|
<table className="w-full text-sm">
|
|
<thead>
|
|
<tr className="border-b border-[#1a1a1a]">
|
|
{['Site','Revenue MTD','Revenue YTD','Outstanding','Active Orders'].map(h => (
|
|
<th key={h} className="px-4 py-2 text-left text-xs text-[#555] font-medium">{h}</th>
|
|
))}
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{siteRevenue.length === 0 ? (
|
|
<tr><td colSpan={5} className="px-4 py-6 text-center text-[#555] text-xs">No data yet</td></tr>
|
|
) : siteRevenue.map((row: any) => (
|
|
<tr key={row.site} className="border-b border-[#1a1a1a] hover:bg-[#161616]">
|
|
<td className="px-4 py-2 text-white font-medium capitalize">{row.site}</td>
|
|
<td className="px-4 py-2 text-green-400">{fmt(row.revenue_mtd ?? 0)}</td>
|
|
<td className="px-4 py-2 text-green-400">{fmt(row.revenue_ytd ?? 0)}</td>
|
|
<td className="px-4 py-2 text-yellow-400">{fmt(row.outstanding ?? 0)}</td>
|
|
<td className="px-4 py-2 text-[#ccc]">{row.active_orders ?? 0}</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
{/* Top Clients */}
|
|
<div className="bg-[#111] border border-[#252525] rounded-lg overflow-hidden">
|
|
<div className="px-4 py-3 border-b border-[#252525]">
|
|
<h2 className="text-sm font-semibold text-white">Top 5 Clients YTD</h2>
|
|
</div>
|
|
<table className="w-full text-sm">
|
|
<thead>
|
|
<tr className="border-b border-[#1a1a1a]">
|
|
{['Client','Revenue YTD','Active Orders'].map(h => (
|
|
<th key={h} className="px-4 py-2 text-left text-xs text-[#555] font-medium">{h}</th>
|
|
))}
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{topClients.length === 0 ? (
|
|
<tr><td colSpan={3} className="px-4 py-6 text-center text-[#555] text-xs">No data yet</td></tr>
|
|
) : topClients.map((c: any) => (
|
|
<tr key={c.id} className="border-b border-[#1a1a1a] hover:bg-[#161616]">
|
|
<td className="px-4 py-2 text-white">{c.company_name}</td>
|
|
<td className="px-4 py-2 text-green-400">{fmt(c.revenue_ytd ?? 0)}</td>
|
|
<td className="px-4 py-2 text-[#ccc]">{c.active_orders ?? 0}</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
{/* Sub-nav links */}
|
|
<div className="grid grid-cols-2 md:grid-cols-5 gap-2">
|
|
{[
|
|
{ label: 'Clients', href: '/admin/accounting/clients' },
|
|
{ label: 'Orders', href: '/admin/accounting/orders' },
|
|
{ label: 'Invoices', href: '/admin/accounting/invoices' },
|
|
{ label: 'Staff & Commissions', href: '/admin/accounting/staff' },
|
|
{ label: 'Expenses', href: '/admin/accounting/expenses' },
|
|
{ label: 'Reconciliation', href: '/admin/accounting/reconciliation' },
|
|
{ label: 'Commissions', href: '/admin/accounting/commissions' },
|
|
{ label: 'Reports', href: '/admin/accounting/reports' },
|
|
{ label: 'Documents', href: '/admin/accounting/documents' },
|
|
].map(l => (
|
|
<Link key={l.href} href={l.href} className="bg-[#111] border border-[#252525] rounded-lg p-3 text-center text-sm text-[#888] hover:text-white hover:border-[#3b82f6] transition-colors">
|
|
{l.label}
|
|
</Link>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|