initial commit: rocket.new export of broadcastbeat
This commit is contained in:
133
src/app/admin/accounting/commissions/page.tsx
Normal file
133
src/app/admin/accounting/commissions/page.tsx
Normal file
@@ -0,0 +1,133 @@
|
||||
'use client';
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { useAuth } from '@/contexts/AuthContext';
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
function fmt(cents: number) { return '$' + (cents / 100).toLocaleString('en-US', { minimumFractionDigits: 2 }); }
|
||||
|
||||
export default function CommissionsPage() {
|
||||
const { user, loading } = useAuth();
|
||||
const router = useRouter();
|
||||
const [commissions, setCommissions] = useState<any[]>([]);
|
||||
const [summary, setSummary] = useState<any[]>([]);
|
||||
const [loadingData, setLoadingData] = useState(true);
|
||||
const [year, setYear] = useState(new Date().getFullYear());
|
||||
const [filterStaff, setFilterStaff] = useState('');
|
||||
const [staff, setStaff] = useState<any[]>([]);
|
||||
|
||||
useEffect(() => { if (!loading && !user) router.push('/login'); }, [user, loading, router]);
|
||||
|
||||
const load = useCallback(async () => {
|
||||
setLoadingData(true);
|
||||
const params = new URLSearchParams({ year: String(year) });
|
||||
if (filterStaff) params.set('staff_id', filterStaff);
|
||||
const [cr, sr, staffR] = await Promise.all([
|
||||
fetch(`/api/accounting/commissions?${params}`),
|
||||
fetch(`/api/accounting/commissions/summary?year=${year}`),
|
||||
fetch('/api/accounting/staff'),
|
||||
]);
|
||||
const [cd, sd, staffD] = await Promise.all([cr.json(), sr.json(), staffR.json()]);
|
||||
setCommissions(cd.commissions ?? []);
|
||||
setSummary(sd.summary ?? []);
|
||||
setStaff(staffD.staff ?? []);
|
||||
setLoadingData(false);
|
||||
}, [year, filterStaff]);
|
||||
|
||||
useEffect(() => { if (user) load(); }, [user, load]);
|
||||
|
||||
async function markPaid(id: string) {
|
||||
await fetch(`/api/accounting/commissions/${id}/mark-paid`, { method: 'POST' });
|
||||
load();
|
||||
}
|
||||
|
||||
if (loading) 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-6xl mx-auto space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-xl font-bold">Commissions</h1>
|
||||
<Link href="/admin/accounting" className="text-xs text-[#555] hover:text-[#888]">← Accounting</Link>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<select value={year} onChange={e => setYear(Number(e.target.value))} className="px-2 py-1 bg-[#111] border border-[#252525] rounded text-xs text-[#888] focus:outline-none">
|
||||
{[2024, 2025, 2026, 2027].map(y => <option key={y} value={y}>{y}</option>)}
|
||||
</select>
|
||||
<select value={filterStaff} onChange={e => setFilterStaff(e.target.value)} className="px-2 py-1 bg-[#111] border border-[#252525] rounded text-xs text-[#888] focus:outline-none">
|
||||
<option value="">All Staff</option>
|
||||
{staff.map((s: any) => <option key={s.id} value={s.id}>{s.full_name}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Summary per staff */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||
{summary.map((s: any) => {
|
||||
const pct = s.next_threshold_cents ? Math.min(100, Math.round((s.ytd_sales_cents / s.next_threshold_cents) * 100)) : 100;
|
||||
return (
|
||||
<div key={s.staff_id} className="bg-[#111] border border-[#252525] rounded-lg p-4 space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-sm font-semibold text-white">{s.full_name}</p>
|
||||
<span className="text-xs text-[#888]">{s.current_tier}</span>
|
||||
</div>
|
||||
<div className="grid grid-cols-3 gap-2 text-xs">
|
||||
<div><p className="text-[#555]">YTD Sales</p><p className="text-white font-medium">{fmt(s.ytd_sales_cents ?? 0)}</p></div>
|
||||
<div><p className="text-[#555]">Earned</p><p className="text-green-400 font-medium">{fmt(s.earned_cents ?? 0)}</p></div>
|
||||
<div><p className="text-[#555]">Outstanding</p><p className="text-yellow-400 font-medium">{fmt(s.outstanding_cents ?? 0)}</p></div>
|
||||
</div>
|
||||
{s.next_threshold_cents && (
|
||||
<div>
|
||||
<div className="flex justify-between text-xs text-[#555] mb-1">
|
||||
<span>Tier progress</span><span>{pct}%</span>
|
||||
</div>
|
||||
<div className="h-1.5 bg-[#1a1a1a] rounded-full overflow-hidden">
|
||||
<div className="h-full bg-[#3b82f6] rounded-full transition-all" style={{ width: `${pct}%` }} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Detail table */}
|
||||
<div className="bg-[#111] border border-[#252525] rounded-lg overflow-x-auto">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b border-[#1a1a1a]">
|
||||
{['Staff','Invoice','Sale Amount','Rate','Commission','YTD at Time','Paid',''].map(h => (
|
||||
<th key={h} className="px-3 py-2 text-left text-xs text-[#555] font-medium">{h}</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{loadingData ? (
|
||||
<tr><td colSpan={8} className="px-4 py-8 text-center text-[#555] text-xs">Loading…</td></tr>
|
||||
) : commissions.length === 0 ? (
|
||||
<tr><td colSpan={8} className="px-4 py-8 text-center text-[#555] text-xs">No commission records</td></tr>
|
||||
) : commissions.map((c: any) => (
|
||||
<tr key={c.id} className="border-b border-[#1a1a1a] hover:bg-[#161616]">
|
||||
<td className="px-3 py-2 text-white text-xs">{c.rmp_sales_staff?.full_name ?? '—'}</td>
|
||||
<td className="px-3 py-2 text-[#3b82f6] text-xs font-mono">{c.rmp_invoices?.invoice_number ?? '—'}</td>
|
||||
<td className="px-3 py-2 text-[#ccc] text-xs">{fmt(c.sale_amount_cents ?? 0)}</td>
|
||||
<td className="px-3 py-2 text-[#888] text-xs">{((c.commission_rate ?? 0) * 100).toFixed(1)}%</td>
|
||||
<td className="px-3 py-2 text-green-400 text-xs">{fmt(c.commission_amount_cents ?? 0)}</td>
|
||||
<td className="px-3 py-2 text-[#555] text-xs">{fmt(c.ytd_sales_at_time_cents ?? 0)}</td>
|
||||
<td className="px-3 py-2">
|
||||
<span className={`px-2 py-0.5 rounded-full text-xs ${c.paid ? 'bg-green-400/10 text-green-400' : 'bg-yellow-400/10 text-yellow-400'}`}>{c.paid ? 'Paid' : 'Unpaid'}</span>
|
||||
</td>
|
||||
<td className="px-3 py-2">
|
||||
{!c.paid && <button onClick={() => markPaid(c.id)} className="text-xs text-green-400 hover:underline">Mark Paid</button>}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user