134 lines
6.9 KiB
TypeScript
134 lines
6.9 KiB
TypeScript
'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>
|
|
);
|
|
}
|