'use client'; import React, { useState, useEffect, useCallback, useMemo } from 'react'; import { formatCompactCurrency, formatCurrency } from '@/lib/utils'; import { TrendingUp, Loader2, DollarSign, Users, FileText, CheckCircle2, Clock, Plus, Search, X, Sparkles, Send, } from 'lucide-react'; import { ResponsiveContainer, ComposedChart, Bar, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, } from 'recharts'; import { ErrorState } from '@/components/ui/ErrorState'; // ─── Types ──────────────────────────────────────────────────────── interface MonthlySched { monthIndex: number; month: string; year: number; scheduled: number; recognized: number; posted: number; deferred: number; lineCount: number; } interface CustomerBreakdown { name: string; code: string; total: number; recognized: number; deferred: number; contractCount: number; recognitionRate: number; } interface ContractRow { id: string; contractNumber: string; name: string; customer: string; totalValue: number; recognizedToDate: number; deferredBalance: number; completionPercent: number; startDate: string; endDate: string; recognitionMethod: string; status: string; } interface Summary { totalContractValue: number; totalRecognized: number; totalDeferred: number; recognitionRate: number; activeContracts: number; totalContracts: number; totalCustomers: number; } interface RevRecData { year: number; monthlySchedule: MonthlySched[]; customerBreakdown: CustomerBreakdown[]; contracts: ContractRow[]; summary: Summary; } interface CustomerOption { id: string; customerName: string; customerCode: string; } // ─── Page ───────────────────────────────────────────────────────── export default function RevenueRecognitionPage() { const [data, setData] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [activeTab, setActiveTab] = useState<'schedule' | 'customers' | 'contracts'>('schedule'); const [contractSearch, setContractSearch] = useState(''); // Create panel state const [showCreate, setShowCreate] = useState(false); const [customers, setCustomers] = useState([]); const [creating, setCreating] = useState(false); const [seeding, setSeeding] = useState(false); const [posting, setPosting] = useState(false); // Form state const [form, setForm] = useState({ name: '', customerId: '', totalValue: '', startDate: new Date().toISOString().slice(0, 10), endDate: new Date(new Date().setMonth(new Date().getMonth() + 12)).toISOString().slice(0, 10), recognitionMethod: 'STRAIGHT_LINE', deferredAccountCode: '2300', revenueAccountCode: '4000', }); // Post-period state const [postPeriod, setPostPeriod] = useState({ year: new Date().getFullYear(), month: new Date().getMonth() + 1, }); const [postResult, setPostResult] = useState(null); const fetchData = useCallback(async () => { setError(null); const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), 30000); try { const res = await fetch('/api/revenue-recognition', { cache: 'no-store', signal: controller.signal, }); if (!res.ok) { const body = await res.json().catch(() => ({})); throw new Error(body?.details || body?.error || `HTTP ${res.status}`); } const json = (await res.json()) as RevRecData; if (!json || !Array.isArray(json.contracts) || !json.summary) { throw new Error('Unexpected response shape'); } setData(json); } catch (e: any) { if (e?.name !== 'AbortError') setError(e?.message || 'Failed to load'); } finally { clearTimeout(timeout); setLoading(false); } }, []); useEffect(() => { fetchData(); }, [fetchData]); useEffect(() => { // Lazy load customers when create panel opens if (showCreate && customers.length === 0) { fetch('/api/customers', { cache: 'no-store' }) .then((r) => r.json()) .then((d) => setCustomers(Array.isArray(d) ? d : d.customers || [])) .catch(() => {}); } }, [showCreate, customers.length]); // ─── Mutations ────────────────────────────────────────────────── const handleCreate = async () => { if (!form.name || !form.totalValue || !form.startDate || !form.endDate) { return; } setCreating(true); try { const res = await fetch('/api/revenue-contracts', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ ...form, totalValue: Number(form.totalValue), customerId: form.customerId || null, }), }); if (!res.ok) { const body = await res.json().catch(() => ({})); throw new Error(body?.error || 'Failed to create contract'); } setShowCreate(false); setForm({ name: '', customerId: '', totalValue: '', startDate: new Date().toISOString().slice(0, 10), endDate: new Date(new Date().setMonth(new Date().getMonth() + 12)).toISOString().slice(0, 10), recognitionMethod: 'STRAIGHT_LINE', deferredAccountCode: '2300', revenueAccountCode: '4000', }); await fetchData(); } catch (e: any) { alert(e?.message || 'Failed to create contract'); } finally { setCreating(false); } }; const handleSeed = async () => { setSeeding(true); try { const res = await fetch('/api/revenue-contracts', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ seed: true }), }); if (!res.ok) { const body = await res.json().catch(() => ({})); throw new Error(body?.error || 'Failed to seed'); } await fetchData(); } catch (e: any) { alert(e?.message || 'Failed to seed'); } finally { setSeeding(false); } }; const handlePost = async () => { setPosting(true); setPostResult(null); try { const res = await fetch('/api/revenue-recognition/post', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(postPeriod), }); const json = await res.json(); if (!res.ok) throw new Error(json?.error || 'Failed to post'); const lines = `Recognized ${json.recognizedCount} lines, ${formatCurrency(json.totalAmount)}`; const je = json.journalEntryId ? ' — JE created' : ''; const warn = json.warning ? ` (${json.warning})` : ''; setPostResult(lines + je + warn); await fetchData(); } catch (e: any) { setPostResult(`Error: ${e?.message || 'Failed to post'}`); } finally { setPosting(false); } }; // ─── Derived ──────────────────────────────────────────────────── const filteredContracts = useMemo(() => { if (!data) return []; const q = contractSearch.toLowerCase().trim(); if (!q) return data.contracts; return data.contracts.filter( (c) => c.contractNumber.toLowerCase().includes(q) || c.name.toLowerCase().includes(q) || c.customer.toLowerCase().includes(q) ); }, [data, contractSearch]); // ─── Render ───────────────────────────────────────────────────── if (loading) { return (
); } if (error) { return ; } if (!data) return null; const isEmpty = data.contracts.length === 0; return (
{/* Header */}

Revenue Recognition

ASC 606 contract management · {data.summary.activeContracts} active contracts

{isEmpty && ( )}
{/* Create panel */} {showCreate && (

New Revenue Contract

setForm({ ...form, name: e.target.value })} placeholder="Enterprise SaaS Subscription - Annual" className="w-full mt-1 bg-[#111114] border border-[#2A2A32] rounded-lg px-3 py-2 text-sm text-[#F0F0F3] focus:outline-none focus:border-emerald-500" />
setForm({ ...form, totalValue: e.target.value })} placeholder="120000" className="w-full mt-1 bg-[#111114] border border-[#2A2A32] rounded-lg px-3 py-2 text-sm text-[#F0F0F3] focus:outline-none focus:border-emerald-500" />
setForm({ ...form, startDate: e.target.value })} className="w-full mt-1 bg-[#111114] border border-[#2A2A32] rounded-lg px-3 py-2 text-sm text-[#F0F0F3] focus:outline-none focus:border-emerald-500" />
setForm({ ...form, endDate: e.target.value })} className="w-full mt-1 bg-[#111114] border border-[#2A2A32] rounded-lg px-3 py-2 text-sm text-[#F0F0F3] focus:outline-none focus:border-emerald-500" />
setForm({ ...form, deferredAccountCode: e.target.value })} className="w-full mt-1 bg-[#111114] border border-[#2A2A32] rounded-lg px-3 py-2 text-sm text-[#F0F0F3] focus:outline-none focus:border-emerald-500" />
setForm({ ...form, revenueAccountCode: e.target.value })} className="w-full mt-1 bg-[#111114] border border-[#2A2A32] rounded-lg px-3 py-2 text-sm text-[#F0F0F3] focus:outline-none focus:border-emerald-500" />
)} {/* KPIs */}
} label="Total Contract Value" value={formatCompactCurrency(data.summary.totalContractValue)} sublabel={`${data.summary.totalContracts} contracts`} /> } label="Recognized to Date" value={formatCompactCurrency(data.summary.totalRecognized)} sublabel={`${data.summary.recognitionRate}% of total`} /> } label="Deferred Balance" value={formatCompactCurrency(data.summary.totalDeferred)} sublabel="Yet to recognize" /> } label="Customers" value={String(data.summary.totalCustomers)} sublabel={`${data.summary.activeContracts} active contracts`} />
{/* Post period card */}

Post Recognition for Period

Recognizes all SCHEDULED lines and posts a balanced JE (debits deferred, credits revenue).

{postResult && (
{postResult}
)}
{/* Tabs */}
{[ { key: 'schedule', label: 'Monthly Schedule', icon: }, { key: 'customers', label: 'Customers', icon: }, { key: 'contracts', label: 'Contracts', icon: }, ].map((tab) => ( ))}
{/* Schedule tab */} {activeTab === 'schedule' && (
formatCompactCurrency(v)} /> formatCurrency(Number(v))} />
{data.monthlySchedule.map((m) => ( ))}
Month Lines Scheduled Recognized Posted Deferred
{m.month} {m.lineCount} {formatCurrency(m.scheduled)} {formatCurrency(m.recognized)} {formatCurrency(m.posted)} {formatCurrency(m.deferred)}
)} {/* Customers tab */} {activeTab === 'customers' && (
{data.customerBreakdown.length === 0 ? (
No customers yet
) : ( {data.customerBreakdown.map((c) => ( ))}
Customer Contracts Total Value Recognized Deferred % Complete
{c.name} {c.contractCount} {formatCurrency(c.total)} {formatCurrency(c.recognized)} {formatCurrency(c.deferred)} {c.recognitionRate}%
)}
)} {/* Contracts tab */} {activeTab === 'contracts' && (
setContractSearch(e.target.value)} placeholder="Search contracts…" className="w-full pl-9 pr-3 py-2 bg-[#111114] border border-[#2A2A32] rounded-lg text-xs text-[#F0F0F3] focus:outline-none focus:border-emerald-500" />
{filteredContracts.length === 0 ? (
{isEmpty ? 'No contracts yet. Create one above or seed demo data.' : 'No contracts match your search.'}
) : (
{filteredContracts.map((c) => ( ))}
Contract Customer Method Total Recognized Deferred Complete Status
{c.contractNumber}
{c.name}
{c.customer} {c.recognitionMethod.replace('_', ' ')} {formatCurrency(c.totalValue)} {formatCurrency(c.recognizedToDate)} {formatCurrency(c.deferredBalance)}
{c.completionPercent}%
{c.status}
)}
)}
); } // ─── KpiCard ────────────────────────────────────────────────────── function KpiCard({ icon, label, value, sublabel, }: { icon: React.ReactNode; label: string; value: string; sublabel?: string; }) { return (
{icon}

{label}

{value}

{sublabel &&

{sublabel}

}
); }