'use client'; import { useState, useEffect, useCallback, useRef, useMemo } from 'react'; import { useAppStore } from '@/stores/app-store'; import type { InvoiceRow, View, ChatMessage } from '@/stores/app-store'; import { Upload, FileText, BarChart3, Bot, Download, CreditCard, Key, User, LogOut, Menu, X, ChevronDown, ChevronUp, Search, Trash2, Send, Copy, Eye, EyeOff, AlertCircle, CheckCircle, Loader2, ArrowRight, Sparkles, Shield, Zap, Globe, Layers, RefreshCw, FileDown, FileJson, Quote, ChevronRight, ScanLine, Database, Lock, Mail, UserPlus, Inbox, MessageSquare, CircleDollarSign, CopyCheck, Terminal, ArrowLeft, Crown, Building2, Check, MousePointerClick, TrendingUp, Bell, Clock, Pencil, LogIn, Sun, Moon, ArrowUpDown, Command, Settings, } from 'lucide-react'; import type { LucideIcon } from 'lucide-react'; import { toast } from 'sonner'; import { AreaChart, Area, BarChart, Bar, PieChart, Pie, Cell, XAxis, YAxis, Tooltip, ResponsiveContainer, CartesianGrid, } from 'recharts'; import { PLAN_LIMITS, PLAN_LABELS, PLAN_PRICES } from '@/lib/auth'; import { useTheme } from 'next-themes'; // ─── Helpers ──────────────────────────────────────────────────────────────────── const currencyFmt = new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', }); function formatCurrency(amount: number | null, currency?: string): string { if (amount === null || amount === undefined) return '--'; try { return new Intl.NumberFormat('en-US', { style: 'currency', currency: currency || 'USD', }).format(amount); } catch { return currency ? `${currency} ${amount.toFixed(2)}` : `$${amount.toFixed(2)}`; } } function getPasswordStrength(pw: string): { label: string; color: string; width: string } { if (!pw) return { label: '', color: 'bg-zinc-700', width: 'w-0' }; let score = 0; if (pw.length >= 6) score++; if (pw.length >= 10) score++; if (/[A-Z]/.test(pw)) score++; if (/[0-9]/.test(pw)) score++; if (/[^A-Za-z0-9]/.test(pw)) score++; if (score <= 2) return { label: 'Weak', color: 'bg-red-500', width: 'w-1/3' }; if (score <= 3) return { label: 'Medium', color: 'bg-amber-500', width: 'w-2/3' }; return { label: 'Strong', color: 'bg-green-500', width: 'w-full' }; } const DASH_TABS = [ { id: 'upload', label: 'Upload', icon: Upload }, { id: 'invoices', label: 'Invoices', icon: FileText }, { id: 'analytics', label: 'Analytics', icon: BarChart3 }, { id: 'chat', label: 'AI Chat', icon: Bot }, { id: 'export', label: 'Export', icon: Download }, { id: 'upgrade', label: 'Upgrade', icon: Crown }, { id: 'api', label: 'API Access', icon: Key }, { id: 'profile', label: 'Profile', icon: User }, { id: 'activity', label: 'Activity', icon: Clock }, { id: 'settings', label: 'Settings', icon: Settings }, ] as const; function relativeTime(dateStr: string): string { const now = Date.now(); const then = new Date(dateStr).getTime(); const diffMs = now - then; const diffMins = Math.floor(diffMs / 60000); if (diffMins < 1) return 'Just now'; if (diffMins < 60) return `${diffMins}m ago`; const diffHours = Math.floor(diffMins / 60); if (diffHours < 24) return `${diffHours}h ago`; const diffDays = Math.floor(diffHours / 24); if (diffDays < 30) return `${diffDays}d ago`; return new Date(dateStr).toLocaleDateString('en-US', { month: 'short', day: 'numeric' }); } // ─── Main Page Component ──────────────────────────────────────────────────────── export default function Home() { const store = useAppStore(); const { theme, setTheme } = useTheme(); // ── Local Auth State ── const [authMode, setAuthMode] = useState<'login' | 'signup'>('login'); const [loginEmail, setLoginEmail] = useState(''); const [loginPassword, setLoginPassword] = useState(''); const [signupName, setSignupName] = useState(''); const [signupEmail, setSignupEmail] = useState(''); const [signupPassword, setSignupPassword] = useState(''); const [signupConfirm, setSignupConfirm] = useState(''); const [authLoading, setAuthLoading] = useState(false); const [authError, setAuthError] = useState(''); // ── Dashboard Local State ── const [invoiceSearch, setInvoiceSearch] = useState(''); const [invoiceFilter, setInvoiceFilter] = useState('all'); const [expandedInvoice, setExpandedInvoice] = useState(null); const [selectedInvoices, setSelectedInvoices] = useState>(new Set()); const [chatInput, setChatInput] = useState(''); const [showApiKey, setShowApiKey] = useState(false); const [profileCurrentPw, setProfileCurrentPw] = useState(''); const [profileNewPw, setProfileNewPw] = useState(''); const [profileConfirmPw, setProfileConfirmPw] = useState(''); const [profilePwLoading, setProfilePwLoading] = useState(false); const [showScrollTop, setShowScrollTop] = useState(false); // ── Landing Mobile Nav ── const [mobileMenuOpen, setMobileMenuOpen] = useState(false); const [dashMobileMenu, setDashMobileMenu] = useState(false); const [editingInvoice, setEditingInvoice] = useState(null); const [editForm, setEditForm] = useState>({}); const [editSaving, setEditSaving] = useState(false); const [dateFrom, setDateFrom] = useState(''); const [dateTo, setDateTo] = useState(''); const [sortField, setSortField] = useState('createdAt'); const [sortDir, setSortDir] = useState<'asc' | 'desc'>('desc'); // ── Activity Items (real API) ── const [activityLogs, setActivityLogs] = useState>([]); const [activityLoading, setActivityLoading] = useState(false); const fetchActivity = useCallback(async () => { setActivityLoading(true); try { const res = await fetch('/api/activity'); if (res.ok) { const data = await res.json(); setActivityLogs(data.activities); } } catch { /* silent */ } finally { setActivityLoading(false); } }, []); useEffect(() => { if (store.view === 'dashboard' && store.activeDashTab === 'activity') { fetchActivity(); } }, [store.view, store.activeDashTab, fetchActivity]); const [showNotifications, setShowNotifications] = useState(false); const [showShortcuts, setShowShortcuts] = useState(false); const [showCommandPalette, setShowCommandPalette] = useState(false); const [commandSearch, setCommandSearch] = useState(''); const notifications = [ ...(store.invoices.length > 0 ? [{ id: '1', title: 'Invoices processed', description: `${store.invoices.length} invoice(s) in your account`, time: 'Just now', icon: FileText, color: 'text-teal-500', }] : []), { id: '2', title: 'Welcome to OmniParse', description: 'Upload your first invoice to get started', time: 'Getting started', icon: Sparkles, color: 'text-amber-500', }, { id: '3', title: 'AI Chat available', description: store.user?.plan === 'free' ? 'Upgrade to Pro to unlock AI Chat' : 'Ask questions about your invoices', time: 'Feature', icon: Bot, color: 'text-amber-500', }, ]; // ── Settings State ── const [dateFormat, setDateFormat] = useState<'MM/DD/YYYY' | 'DD/MM/YYYY' | 'YYYY-MM-DD'>('MM/DD/YYYY'); const [defaultCurrency, setDefaultCurrency] = useState('USD'); const [compactView, setCompactView] = useState(false); const [settingsSaved, setSettingsSaved] = useState(false); const saveSettings = useCallback(() => { setSettingsSaved(true); toast.success('Settings saved successfully'); setTimeout(() => setSettingsSaved(false), 2000); }, []); // ── Upload State ── const [selectedFiles, setSelectedFiles] = useState([]); const [dragOver, setDragOver] = useState(false); const fileInputRef = useRef(null); const toggleSort = useCallback((field: string) => { if (sortField === field) { setSortDir(d => d === 'asc' ? 'desc' : 'asc'); } else { setSortField(field); setSortDir('asc'); } }, [sortField]); // ── Derived invoice data (before callbacks so they can reference it) ── const filteredInvoices = useMemo(() => { let result = store.invoices.filter((inv) => { if (invoiceSearch && !inv.vendor?.toLowerCase().includes(invoiceSearch.toLowerCase())) return false; if (invoiceFilter === 'done') return inv.status === 'done' && !inv.isDuplicate; if (invoiceFilter === 'review') return inv.status === 'review' || inv.isDuplicate; if (invoiceFilter === 'duplicates') return inv.isDuplicate; if (dateFrom && inv.invDate && inv.invDate < dateFrom) return false; if (dateTo && inv.invDate && inv.invDate > dateTo) return false; return true; }); result.sort((a, b) => { let cmp = 0; const f = sortField; if (f === 'vendor') cmp = (a.vendor || '').localeCompare(b.vendor || ''); else if (f === 'invNumber') cmp = (a.invNumber || '').localeCompare(b.invNumber || ''); else if (f === 'invDate') cmp = (a.invDate || '').localeCompare(b.invDate || ''); else if (f === 'amount') cmp = (a.amount ?? 0) - (b.amount ?? 0); else if (f === 'total') cmp = (a.total ?? 0) - (b.total ?? 0); else if (f === 'confidence') cmp = (a.confidence ?? 0) - (b.confidence ?? 0); else if (f === 'status') cmp = a.status.localeCompare(b.status); else cmp = new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(); return sortDir === 'asc' ? cmp : -cmp; }); return result; }, [store.invoices, invoiceSearch, invoiceFilter, dateFrom, dateTo, sortField, sortDir]); // ── Chat scroll ref ── const chatEndRef = useRef(null); // ── Scroll refs for landing ── const featuresRef = useRef(null); const pricingRef = useRef(null); // ────────────────────────────────────────────────────────────────────────────── // API Callbacks // ────────────────────────────────────────────────────────────────────────────── const fetchMe = useCallback(async () => { const { setUser, setView, setLoading } = useAppStore.getState(); try { const res = await fetch('/api/auth/me'); if (res.ok) { const data = await res.json(); setUser(data.user); setView('dashboard'); } else { setUser(null); setLoading(false); } } catch { setUser(null); setLoading(false); } }, []); const fetchLogin = useCallback(async () => { setAuthError(''); setAuthLoading(true); try { const res = await fetch('/api/auth/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email: loginEmail, password: loginPassword }), }); const data = await res.json(); if (!res.ok) { setAuthError(data.error || 'Login failed'); setAuthLoading(false); return; } const { setUser, setView } = useAppStore.getState(); setUser(data.user); setView('dashboard'); toast.success('Welcome back!'); } catch { setAuthError('Network error. Please try again.'); } finally { setAuthLoading(false); } }, [loginEmail, loginPassword]); const fetchSignup = useCallback(async () => { setAuthError(''); if (signupPassword !== signupConfirm) { setAuthError('Passwords do not match'); return; } setAuthLoading(true); try { const res = await fetch('/api/auth/signup', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: signupName, email: signupEmail, password: signupPassword }), }); const data = await res.json(); if (!res.ok) { setAuthError(data.error || 'Signup failed'); setAuthLoading(false); return; } const { setUser, setView } = useAppStore.getState(); setUser(data.user); setView('dashboard'); toast.success('Account created successfully!'); } catch { setAuthError('Network error. Please try again.'); } finally { setAuthLoading(false); } }, [signupName, signupEmail, signupPassword, signupConfirm]); const fetchLogout = useCallback(() => { fetch('/api/auth/logout', { method: 'POST' }).catch(() => {}); toast.success('Logged out'); window.location.href = '/'; }, []); const fetchInvoices = useCallback(async (filter?: string) => { const { setLoading, setInvoices } = useAppStore.getState(); setLoading(true); try { const params = filter && filter !== 'all' ? `?filter=${filter}` : ''; const res = await fetch(`/api/invoices${params}`); if (res.ok) { const data = await res.json(); setInvoices(data.invoices); } } catch { toast.error('Failed to load invoices'); } finally { setLoading(false); } }, []); const fetchUpload = useCallback(async () => { if (selectedFiles.length === 0) return; const { setUploadLoading, setUploadResults } = useAppStore.getState(); setUploadLoading(true); try { const formData = new FormData(); for (const file of selectedFiles) { formData.append('files', file); } const res = await fetch('/api/invoices/upload', { method: 'POST', body: formData, }); const data = await res.json(); if (!res.ok) { toast.error(data.error || 'Upload failed'); setUploadLoading(false); return; } setUploadResults(data.invoices); setSelectedFiles([]); toast.success(`${data.invoices.length} invoice(s) processed successfully!`); fetchInvoices(invoiceFilter); } catch { toast.error('Upload failed. Please try again.'); } finally { setUploadLoading(false); } }, [selectedFiles, invoiceFilter, fetchInvoices]); const fetchDeleteInvoice = useCallback(async (id: string) => { if (!window.confirm('Are you sure you want to delete this invoice? This action cannot be undone.')) return; try { const res = await fetch('/api/invoices', { method: 'DELETE', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id }), }); if (res.ok) { toast.success('Invoice deleted'); setSelectedInvoices(prev => { const n = new Set(prev); n.delete(id); return n; }); fetchInvoices(invoiceFilter); if (expandedInvoice === id) setExpandedInvoice(null); } else { const data = await res.json(); toast.error(data.error || 'Failed to delete'); } } catch { toast.error('Failed to delete invoice'); } }, [invoiceFilter, expandedInvoice, fetchInvoices]); const fetchBatchDelete = useCallback(async () => { const ids = Array.from(selectedInvoices); if (ids.length === 0) return; if (!window.confirm(`Delete ${ids.length} invoice(s)? This action cannot be undone.`)) return; try { await Promise.all(ids.map(id => fetch('/api/invoices', { method: 'DELETE', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id }), }) )); toast.success(`${ids.length} invoice(s) deleted`); setSelectedInvoices(new Set()); fetchInvoices(invoiceFilter); } catch { toast.error('Failed to delete some invoices'); } }, [selectedInvoices, invoiceFilter, fetchInvoices]); const toggleSelect = useCallback((id: string) => { setSelectedInvoices(prev => { const next = new Set(prev); if (next.has(id)) next.delete(id); else next.add(id); return next; }); }, []); const toggleSelectAll = useCallback(() => { if (selectedInvoices.size === filteredInvoices.length) { setSelectedInvoices(new Set()); } else { setSelectedInvoices(new Set(filteredInvoices.map(i => i.id))); } }, [selectedInvoices.size, filteredInvoices]); const fetchChat = useCallback(async () => { if (!chatInput.trim()) return; const { addChatMessage, chatHistory, setChatLoading } = useAppStore.getState(); const userMsg: ChatMessage = { role: 'user', content: chatInput.trim() }; addChatMessage(userMsg); const historyForApi = [...chatHistory, userMsg].map((m) => ({ role: m.role, content: m.content })); setChatInput(''); setChatLoading(true); try { const res = await fetch('/api/chat', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ message: userMsg.content, history: historyForApi.slice(0, -1) }), }); const data = await res.json(); if (!res.ok) { addChatMessage({ role: 'assistant', content: data.error || 'Something went wrong.' }); } else { addChatMessage({ role: 'assistant', content: data.reply }); } } catch { addChatMessage({ role: 'assistant', content: 'Network error. Please try again.' }); } finally { setChatLoading(false); } }, [chatInput]); const exportCsv = useCallback(() => { window.open('/api/invoices/export/csv', '_blank'); toast.success('CSV download started'); }, []); const exportJson = useCallback(() => { const { user } = useAppStore.getState(); if (user?.plan === 'free') { toast.error('JSON export is available on Pro+ plans. Please upgrade.'); return; } window.open('/api/invoices/export/json', '_blank'); toast.success('JSON download started'); }, []); const openEditModal = useCallback((inv: InvoiceRow) => { setEditingInvoice(inv); setEditForm({ vendor: inv.vendor || '', invNumber: inv.invNumber || '', invDate: inv.invDate || '', dueDate: inv.dueDate || '', amount: String(inv.amount || ''), vatAmount: String(inv.vatAmount || ''), total: String(inv.total || ''), currency: inv.currency, status: inv.status === 'review' ? 'review' : 'done', }); }, []); const saveEdit = useCallback(async () => { if (!editingInvoice) return; setEditSaving(true); try { const res = await fetch('/api/invoices/update', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id: editingInvoice.id, ...editForm }), }); if (res.ok) { toast.success('Invoice updated'); setEditingInvoice(null); fetchInvoices(invoiceFilter); } else { const data = await res.json(); toast.error(data.error || 'Update failed'); } } catch { toast.error('Failed to update invoice'); } finally { setEditSaving(false); } }, [editingInvoice, editForm, invoiceFilter, fetchInvoices]); // ── Effects ── useEffect(() => { fetchMe(); }, [fetchMe]); useEffect(() => { if (store.view === 'dashboard' && store.activeDashTab === 'invoices') { fetchInvoices(invoiceFilter); } }, [store.view, store.activeDashTab, invoiceFilter, fetchInvoices]); useEffect(() => { chatEndRef.current?.scrollIntoView({ behavior: 'smooth' }); }, [store.chatHistory, store.chatLoading]); // ── Drag-and-drop handlers ── const handleDragOver = useCallback((e: React.DragEvent) => { e.preventDefault(); setDragOver(true); }, []); const handleDragLeave = useCallback((e: React.DragEvent) => { e.preventDefault(); setDragOver(false); }, []); const handleDrop = useCallback((e: React.DragEvent) => { e.preventDefault(); setDragOver(false); const files = Array.from(e.dataTransfer.files).filter( (f) => f.type === 'application/pdf' || f.type.startsWith('image/') ); setSelectedFiles((prev) => [...prev, ...files]); }, []); const handleFileSelect = useCallback((e: React.ChangeEvent) => { if (e.target.files) { setSelectedFiles((prev) => [...prev, ...Array.from(e.target.files!)]); } }, []); const removeFile = useCallback((index: number) => { setSelectedFiles((prev) => prev.filter((_, i) => i !== index)); }, []); // ── Scroll helpers ── const scrollToFeatures = useCallback(() => { featuresRef.current?.scrollIntoView({ behavior: 'smooth' }); setMobileMenuOpen(false); }, []); const scrollToPricing = useCallback(() => { pricingRef.current?.scrollIntoView({ behavior: 'smooth' }); setMobileMenuOpen(false); }, []); // ── Keyboard shortcut for search (decorative hint, but functional) ── useEffect(() => { const handler = (e: KeyboardEvent) => { if ((e.ctrlKey || e.metaKey) && e.key === 'k') { e.preventDefault(); const { view } = useAppStore.getState(); if (view === 'dashboard') { setShowCommandPalette(true); } } if ((e.ctrlKey || e.metaKey) && e.key === '/') { e.preventDefault(); const { view } = useAppStore.getState(); if (view === 'dashboard') { setShowShortcuts(true); } } }; window.addEventListener('keydown', handler); return () => window.removeEventListener('keydown', handler); }, []); useEffect(() => { const handleScroll = () => { setShowScrollTop(window.scrollY > 400); }; window.addEventListener('scroll', handleScroll, { passive: true }); return () => window.removeEventListener('scroll', handleScroll); }, []); // ── Loading screen ── if (store.isLoading) { return (
); } // ────────────────────────────────────────────────────────────────────────────── // LANDING VIEW // ────────────────────────────────────────────────────────────────────────────── if (store.view === 'landing') { return (
{/* ── Navbar ── */}
{mobileMenuOpen && (
)}
{/* ── Hero Section ── */}
Powered by advanced AI extraction

Invoice Processing,{' '} Reimagined

Upload any invoice and let AI extract vendor details, amounts, dates, and line items in seconds. Export structured data to CSV, JSON, or connect via API.

SOC 2 Compliant
{/* ── Stats Bar ── */}
10K+
Invoices Processed
99.2%
Accuracy Rate
500+
Companies Trust Us

{/* ── How It Works ── */}

How It Works

Three simple steps to transform your invoices into structured data

1

Upload

Drag and drop your invoice PDFs or images. We support batch uploads of multiple files at once.

2

Extract

Our AI automatically extracts vendor details, line items, amounts, dates, and detects duplicates.

3

Export

Download your data as CSV or JSON, or integrate directly with your systems using our REST API.


{/* ── Features Section ── */}

Everything You Need

Powerful features designed to streamline your invoice processing workflow

Instant Extraction

Extract all invoice fields in under 3 seconds per document with near-perfect accuracy.

Duplicate Detection

Automatically flags potential duplicate invoices based on vendor and amount matching.

AI Chat Assistant

Ask questions about your invoices, get spending insights, and analyze vendor relationships.

Multi-Currency

Automatically detects and processes invoices in USD, EUR, GBP, CAD, AUD, and more.

Bulk Processing

Upload dozens of invoices at once. Our system processes them in parallel for maximum speed.

Enterprise Security

Your data is encrypted at rest and in transit. SOC 2 compliant infrastructure.


{/* ── Testimonials ── */}

Trusted by Teams Worldwide

See what our customers have to say

“OmniParse reduced our invoice processing time from hours to minutes. The accuracy is outstanding and the duplicate detection has saved us from overpaying vendors multiple times.”

SM
Sarah Mitchell
CFO, TechVentures Inc.

“We process over 500 invoices per month. The API integration was seamless and the structured JSON output fits perfectly into our ERP system. A game changer for our accounting team.”

JK
James Kim
CTO, DataFlow Systems

“The AI chat feature is incredible. I can ask questions like 'Which vendor had the highest spending this quarter?' and get instant answers. It's like having a financial analyst on demand.”

RP
Rachel Patel
VP Finance, Meridian Group

{/* ── FAQ ── */}

Frequently Asked Questions

What file formats are supported?
We support PDF files and common image formats including PNG, JPG, and JPEG. You can upload multiple files at once for batch processing.
How accurate is the extraction?
Our AI achieves 99.2% accuracy on standard invoices. Each extraction includes a confidence score so you can review lower-confidence results. The system also flags potential duplicates automatically.
Is my data secure?
Absolutely. All data is encrypted at rest and in transit. We use SOC 2 compliant infrastructure and your documents are never shared with third parties. You can delete your data at any time.
Can I integrate OmniParse with my existing tools?
Yes! We provide a REST API with your unique API key. You can upload invoices, retrieve processed data, and trigger exports programmatically. Full API documentation is available in your dashboard.
What happens when I reach my plan limit?
You will receive a notification when you are approaching your monthly limit. Once reached, you can upgrade your plan or wait for the next billing cycle. Enterprise plans have unlimited processing.

{/* ── Pricing Section ── */}

Simple, Transparent Pricing

Choose the plan that fits your needs. Upgrade or downgrade anytime.

{[ { name: 'Free', price: 0, limit: '20 invoices/mo', features: ['Basic extraction', 'CSV export', 'Email support', '1 user'] }, { name: 'Pro', price: 129, limit: '2,000 invoices/mo', features: ['Advanced extraction', 'CSV & JSON export', 'AI Chat assistant', 'API access', 'Priority support', '5 users'], highlight: true }, { name: 'Business', price: 29, limit: '200 invoices/mo', features: ['Standard extraction', 'CSV & JSON export', 'API access', 'Email & chat support', '3 users'] }, { name: 'Enterprise', price: 499, limit: 'Unlimited', features: ['Custom AI models', 'All export formats', 'Dedicated AI Chat', 'Full API access', 'Dedicated support', 'Unlimited users', 'SLA guarantee'] }, ].map((plan) => (
{plan.highlight &&
Most Popular
}

{plan.name}

${plan.price}/mo
{plan.limit}
    {plan.features.map((f) => (
  • {f}
  • ))}
))}
{/* ── Footer ── */}

Product

  • Features
  • Pricing
  • API Docs
  • Changelog

Company

  • About
  • Blog
  • Careers
  • Contact

Resources

  • Documentation
  • Help Center
  • Status Page

Legal

  • Privacy Policy
  • Terms of Service
  • Security
OmniParse AI

© {new Date().getFullYear()} OmniParse AI. All rights reserved.

{/* ── Scroll to Top ── */} {/* ── Download HF Space (preview only) ── */} Download HF Space (.zip)
); } // ────────────────────────────────────────────────────────────────────────────── // AUTH VIEW // ────────────────────────────────────────────────────────────────────────────── if (store.view === 'auth') { const pwStrength = getPasswordStrength(authMode === 'signup' ? signupPassword : ''); return (

{authMode === 'login' ? 'Welcome back' : 'Create your account'}

{authMode === 'login' ? 'Sign in to your OmniParse account' : 'Get started with OmniParse for free'}

{/* Tab Toggle */}
{/* Error Display */} {authError && (
{authError}
)} {authMode === 'login' ? (
{ e.preventDefault(); fetchLogin(); }} className="space-y-4" >
setLoginEmail(e.target.value)} required placeholder="you@company.com" className="input-field input-field-glow pl-10" />
setLoginPassword(e.target.value)} required placeholder="Enter your password" className="input-field input-field-glow pl-10" />

Demo credentials: demo@omniparse.ai / demo1234

) : (
{ e.preventDefault(); fetchSignup(); }} className="space-y-4" >
setSignupName(e.target.value)} required placeholder="Your full name" className="input-field input-field-glow pl-10" />
setSignupEmail(e.target.value)} required placeholder="you@company.com" className="input-field input-field-glow pl-10" />
setSignupPassword(e.target.value)} required minLength={6} placeholder="At least 6 characters" className="input-field input-field-glow pl-10" />
{signupPassword && (

{pwStrength.label}

)}
setSignupConfirm(e.target.value)} required placeholder="Confirm your password" className="input-field input-field-glow pl-10" />
)}
); } // ────────────────────────────────────────────────────────────────────────────── // DASHBOARD VIEW // ────────────────────────────────────────────────────────────────────────────── if (store.view === 'dashboard' && store.user) { const planLimit = PLAN_LIMITS[store.user.plan] ?? PLAN_LIMITS.free; const planLabel = PLAN_LABELS[store.user.plan] ?? store.user.plan; const invoiceCount = store.invoices.length; const totalAmount = store.invoices.reduce((s, i) => s + (i.total ?? 0), 0); const dupCount = store.invoices.filter((i) => i.isDuplicate).length; const avgConfidence = store.invoices.length > 0 ? store.invoices.reduce((s, i) => s + (i.confidence ?? 0), 0) / store.invoices.length : 0; const usagePercent = planLimit === Infinity ? 0 : Math.min((invoiceCount / planLimit) * 100, 100); return (
{/* ── Dashboard Header ── */}
OmniParse {planLabel}
{showNotifications && ( <>
setShowNotifications(false)} />

Notifications

{notifications.map((n) => { const NIcon = n.icon; return (
{n.title}
{n.description}
); })}
)}
{store.user.name.charAt(0).toUpperCase()}
{store.user.name}
{/* Mobile tab menu */} {dashMobileMenu && (
)}
{/* ── Dashboard Tabs (desktop) ── */}
{/* ── Dashboard Content ── */}
{/* ── Stats Overview ── */}
Total Invoices
{store.invoices.length}
Total Amount
{formatCurrency(store.invoices.reduce((sum, inv) => sum + (inv.total ?? 0), 0))}
Duplicates
{store.invoices.filter(i => i.isDuplicate).length}
Avg Confidence
{store.invoices.length > 0 ? `${((store.invoices.reduce((sum, inv) => sum + (inv.confidence ?? 0), 0) / store.invoices.length) * 100).toFixed(1)}%` : '--'}
{/* ═══════════════════════════════════════════════════════════════════════ */} {/* TAB 1: UPLOAD */} {/* ═══════════════════════════════════════════════════════════════════════ */} {store.activeDashTab === 'upload' && (
{/* Welcome Panel */}

Welcome back, {store.user?.name?.split(' ')[0] || 'there'}!

Here's a quick overview of your account

{store.user?.plan || 'Free'} Plan

Upload Invoices

Drag and drop PDF or image files to extract invoice data

{/* Usage Progress */}
Monthly usage {invoiceCount} / {planLimit === Infinity ? 'Unlimited' : planLimit}
90 ? '!bg-red-500' : ''}`} style={{ width: `${planLimit === Infinity ? 0 : usagePercent}%` }} />
{/* Drop Zone */}
fileInputRef.current?.click()} role="button" tabIndex={0} aria-label="Click or drag files to upload" onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') fileInputRef.current?.click(); }} >

Drop your invoice files here, or click to browse

Supports PDF, PNG, JPG, JPEG. You can upload multiple files at once.

{/* Selected Files List */} {selectedFiles.length > 0 && (

{selectedFiles.length} file(s) selected

    {selectedFiles.map((file, idx) => (
  • {file.name} {(file.size / 1024).toFixed(1)} KB
  • ))}
)} {/* Upload Results */} {store.uploadResults && store.uploadResults.length > 0 && (

Results

{store.uploadResults.map((inv) => ( ))}
Vendor Invoice # Amount Confidence Status
{inv.vendor || '--'} {inv.invNumber || '--'} {formatCurrency(inv.total, inv.currency)} {inv.confidence ? `${(inv.confidence * 100).toFixed(1)}%` : '--'} {inv.isDuplicate ? 'Duplicate' : 'Done'}
)} {/* Empty State */} {!store.uploadLoading && (!store.uploadResults || store.uploadResults.length === 0) && selectedFiles.length === 0 && (

Upload your first invoice to get started

)}
)} {/* ═══════════════════════════════════════════════════════════════════════ */} {/* TAB 2: INVOICES */} {/* ═══════════════════════════════════════════════════════════════════════ */} {store.activeDashTab === 'invoices' && (

Invoices

{filteredInvoices.length} invoice(s) found Ctrl+K

{/* Search and Filter */}
setInvoiceSearch(e.target.value)} placeholder="Search by vendor name..." className="input-field input-field-glow pl-10" />
{(['all', 'done', 'review', 'duplicates'] as const).map((f) => ( ))}
{/* Date Range Filter */}
setDateFrom(e.target.value)} className="input-field input-field-glow" />
setDateTo(e.target.value)} className="input-field input-field-glow" />
{(dateFrom || dateTo) && (
)}
{/* Duplicate Alert Panel */} {!store.isLoading && store.invoices.some(i => i.isDuplicate) && (

Duplicate Invoices Detected

{store.invoices.filter(i => i.isDuplicate).length} potential duplicate(s) found. Review and decide whether to keep or remove them.

)} {/* Selection Toolbar */} {selectedInvoices.size > 0 && (
{selectedInvoices.size} invoice(s) selected
)} {/* Loading Skeletons */} {store.isLoading && (
{Array.from({ length: 5 }).map((_, i) => (
))}
)} {/* Invoice Table */} {!store.isLoading && filteredInvoices.length > 0 && (
{filteredInvoices.map((inv) => ( setExpandedInvoice(expandedInvoice === inv.id ? null : inv.id)} onDelete={() => fetchDeleteInvoice(inv.id)} isSelected={selectedInvoices.has(inv.id)} onSelect={() => toggleSelect(inv.id)} onEdit={() => openEditModal(inv)} /> ))}
0} onChange={toggleSelectAll} className="invoice-checkbox" aria-label="Select all invoices" /> toggleSort('vendor')} >
Vendor {sortField === 'vendor' ? ( {sortDir === 'asc' ? '↑' : '↓'} ) : ( )}
toggleSort('invNumber')} >
Invoice # {sortField === 'invNumber' ? ( {sortDir === 'asc' ? '↑' : '↓'} ) : ( )}
toggleSort('invDate')} >
Date {sortField === 'invDate' ? ( {sortDir === 'asc' ? '↑' : '↓'} ) : ( )}
toggleSort('amount')} >
Amount {sortField === 'amount' ? ( {sortDir === 'asc' ? '↑' : '↓'} ) : ( )}
toggleSort('total')} >
Total {sortField === 'total' ? ( {sortDir === 'asc' ? '↑' : '↓'} ) : ( )}
toggleSort('status')} >
Status {sortField === 'status' ? ( {sortDir === 'asc' ? '↑' : '↓'} ) : ( )}
toggleSort('confidence')} >
Confidence {sortField === 'confidence' ? ( {sortDir === 'asc' ? '↑' : '↓'} ) : ( )}
Actions
)} {/* Empty State */} {!store.isLoading && filteredInvoices.length === 0 && (

No invoices found

Upload your first invoice to get started

)}
)} {/* ═══════════════════════════════════════════════════════════════════════ */} {/* TAB 3: ANALYTICS */} {/* ═══════════════════════════════════════════════════════════════════════ */} {store.activeDashTab === 'analytics' && ( )} {/* ═══════════════════════════════════════════════════════════════════════ */} {/* TAB 4: AI CHAT */} {/* ═══════════════════════════════════════════════════════════════════════ */} {store.activeDashTab === 'chat' && (

AI Chat

Ask questions about your invoices

{store.chatHistory.length > 0 && ( )}
{/* Plan gate */} {store.user.plan === 'free' && (

Upgrade to Use AI Chat

AI Chat is available on Pro and Enterprise plans. Get instant insights about your invoices, spending analysis, and more.

)} {/* Chat Messages */} {store.chatHistory.length === 0 && store.user.plan !== 'free' && (

Start a conversation

Ask about your invoices, get spending insights, or request analysis.

{['Summarize my invoices', 'Who is my top vendor?', 'Show spending trends'].map((suggestion) => ( ))}
)}
{store.chatHistory.map((msg, idx) => (
{msg.role === 'assistant' && (
)}

{msg.content}

))} {store.chatLoading && (
)}
{/* Chat Input */} {store.user.plan !== 'free' && (
setChatInput(e.target.value)} onKeyDown={(e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); fetchChat(); } }} placeholder="Ask about your invoices..." disabled={store.chatLoading} className="input-field input-field-glow rounded-xl disabled:opacity-50" />
Send
)}
)} {/* ═══════════════════════════════════════════════════════════════════════ */} {/* TAB 5: EXPORT */} {/* ═══════════════════════════════════════════════════════════════════════ */} {store.activeDashTab === 'export' && (

Export Data

Download your invoice data in your preferred format

{/* CSV Export */}

CSV Export

Comma-separated values

Download all your invoices as a CSV file, compatible with Excel, Google Sheets, and any spreadsheet application. Includes all fields: vendor, amounts, dates, status, and confidence scores.

{/* JSON Export */}

JSON Export

{store.user?.plan === 'free' && ( Pro+ )}

Structured JSON data with raw extraction

Download structured JSON data including raw extraction results, line items, and bank details. Perfect for API integrations and programmatic processing.

{/* XLSX Export */}

Excel Export

XLSX spreadsheet format

Download a professionally formatted Excel spreadsheet with styled headers, currency formatting, color-coded statuses, and a summary total row. Compatible with Microsoft Excel, Google Sheets, and LibreOffice Calc.

{/* Empty state hint */} {invoiceCount === 0 && (

No invoices to export yet. Upload some invoices first.

)}
)} {/* ═══════════════════════════════════════════════════════════════════════ */} {/* TAB 6: UPGRADE */} {/* ═══════════════════════════════════════════════════════════════════════ */} {store.activeDashTab === 'upgrade' && (

Upgrade Your Plan

You are currently on the {planLabel} plan

{[ { name: 'Free', price: 0, limit: '20 invoices/mo', features: ['Basic extraction', 'CSV export', 'Email support'] }, { name: 'Pro', price: 129, limit: '2,000 invoices/mo', features: ['Advanced extraction', 'CSV & JSON export', 'AI Chat assistant', 'API access', 'Priority support'] }, { name: 'Business', price: 29, limit: '200 invoices/mo', features: ['Standard extraction', 'CSV & JSON export', 'API access', 'Email & chat support'] }, { name: 'Enterprise', price: 499, limit: 'Unlimited', features: ['Custom AI models', 'All export formats', 'Dedicated AI Chat', 'Full API access', 'Dedicated support', 'SLA guarantee'] }, ].map((plan) => { const isCurrent = plan.name.toLowerCase() === store.user.plan; return (
{plan.name === 'Pro' &&
Most Popular
}

{plan.name}

{isCurrent && ( Current )}
${plan.price}/mo
{plan.limit}
    {plan.features.map((f) => (
  • {f}
  • ))}
); })}
)} {/* ═══════════════════════════════════════════════════════════════════════ */} {/* TAB 7: API ACCESS */} {/* ═══════════════════════════════════════════════════════════════════════ */} {store.activeDashTab === 'api' && (

API Access

Use your API key to integrate OmniParse into your workflow

{/* API Key Display */}

Your API Key

{showApiKey ? (store.user.apiKey || 'Not available') : (store.user.apiKey ? store.user.apiKey.slice(0, 10) + '...' + store.user.apiKey.slice(-4) : 'Not available') }
{showApiKey ? 'Hide' : 'Show'}
Copy
{/* cURL Example */}

Quick Start Example

{`curl -X POST /api/invoices/upload \\
  -H "Cookie: op_session=YOUR_SESSION" \\
  -F "files=@invoice.pdf"`}
{/* Endpoint Documentation */}

Available Endpoints

{[ { method: 'GET', path: '/api/auth/me', desc: 'Get current user info' }, { method: 'POST', path: '/api/invoices/upload', desc: 'Upload and process invoice files' }, { method: 'GET', path: '/api/invoices', desc: 'List all invoices with optional filters' }, { method: 'DELETE', path: '/api/invoices', desc: 'Delete an invoice by ID' }, { method: 'GET', path: '/api/invoices/export/csv', desc: 'Export all invoices as CSV' }, { method: 'GET', path: '/api/invoices/export/json', desc: 'Export all invoices as JSON (Pro+)' }, { method: 'POST', path: '/api/chat', desc: 'Send a message to the AI assistant (Pro+)' }, ].map((ep) => (
{ep.method}
{ep.path}
{ep.desc}
))}
)} {/* ═══════════════════════════════════════════════════════════════════════ */} {/* TAB 8: PROFILE */} {/* ═══════════════════════════════════════════════════════════════════════ */} {store.activeDashTab === 'profile' && (

Profile

Manage your account settings

{/* User Info Grid */}

Account Information

Email
{store.user.email}
Name
{store.user.name}
Plan
{planLabel}
Member Since
{new Date(store.user.createdAt).toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' })}
API Key
{store.user.apiKey ? store.user.apiKey.slice(0, 20) + '...' : 'Not available'} {store.user.apiKey && (
Copy
)}
{/* Change Password */}

Change Password

{ e.preventDefault(); if (!profileCurrentPw || !profileNewPw || !profileConfirmPw) { toast.error('Please fill in all password fields'); return; } setProfilePwLoading(true); try { const res = await fetch('/api/auth/password', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ currentPassword: profileCurrentPw, newPassword: profileNewPw, confirmPassword: profileConfirmPw, }), }); const data = await res.json(); if (!res.ok) { toast.error(data.error || 'Failed to update password'); return; } toast.success('Password updated successfully. Please log in again.'); setProfileCurrentPw(''); setProfileNewPw(''); setProfileConfirmPw(''); setTimeout(() => { fetchLogout(); }, 1500); } catch { toast.error('Network error. Please try again.'); } finally { setProfilePwLoading(false); } }} className="space-y-4" >
setProfileCurrentPw(e.target.value)} placeholder="Enter current password" className="input-field input-field-glow" />
setProfileNewPw(e.target.value)} placeholder="Enter new password" className="input-field input-field-glow" /> {profileNewPw && (() => { const strength = getPasswordStrength(profileNewPw); return (

{strength.label}

); })()}
setProfileConfirmPw(e.target.value)} placeholder="Confirm new password" className="input-field input-field-glow" />
{/* Danger Zone */}

Danger Zone

Permanently delete your account and all associated data. This action cannot be undone.

)} {/* ═══════════════════════════════════════════════════════════════════════ */} {/* TAB 10: SETTINGS */} {/* ═══════════════════════════════════════════════════════════════════════ */} {store.activeDashTab === 'settings' && (

Settings

Customize your dashboard experience

{/* Appearance */}

Appearance

Theme
Switch between light and dark mode
Compact Table View
Reduce row height in invoice tables
{/* Regional */}

Regional

{(['MM/DD/YYYY', 'DD/MM/YYYY', 'YYYY-MM-DD'] as const).map((fmt) => ( ))}
{/* Notifications & Keyboard */}

Notifications & Shortcuts

Global Search Ctrl+K
Keyboard Shortcuts Ctrl+/
Go to Invoices Ctrl+K
{/* Save Button */}
)} {/* ═══════════════════════════════════════════════════════════════════════ */} {/* TAB 9: ACTIVITY */} {/* ═══════════════════════════════════════════════════════════════════════ */} {store.activeDashTab === 'activity' && (

Activity

Recent actions and events

{activityLoading ? (
{Array.from({ length: 3 }).map((_, i) => (
))}
) : activityLogs.length > 0 ? (
{/* Vertical line */}
{activityLogs.map((log) => { const iconMap: Record = { login: LogIn, upload: Upload, delete: Trash2, export: Download, edit: Pencil, signup: UserPlus, password_change: Lock, }; const colorMap: Record = { login: 'text-amber-500', upload: 'text-teal-500', delete: 'text-red-400', export: 'text-amber-500', edit: 'text-blue-400', signup: 'text-green-400', password_change: 'text-amber-500', }; const ItemIcon = iconMap[log.type] || Clock; const itemColor = colorMap[log.type] || 'text-muted-foreground'; return (

{log.title}

{relativeTime(log.createdAt)}

{log.description}

); })}
) : (

No activity yet. Actions will appear here as you use the app.

)}
)}
{showCommandPalette && (
setShowCommandPalette(false)}>
e.stopPropagation()}>
setCommandSearch(e.target.value)} placeholder="Search invoices, navigate tabs..." onKeyDown={e => { if (e.key === 'Escape') setShowCommandPalette(false); }} /> ESC
Quick Actions
{DASH_TABS.filter(t => !commandSearch || t.label.toLowerCase().includes(commandSearch.toLowerCase())).map(tab => { const Icon = tab.icon; return ( ); })} {commandSearch && ( <>
Invoice Results
{store.invoices .filter(inv => !commandSearch || inv.vendor?.toLowerCase().includes(commandSearch.toLowerCase()) || inv.invNumber?.toLowerCase().includes(commandSearch.toLowerCase())) .slice(0, 5) .map(inv => ( )) } {store.invoices.filter(inv => inv.vendor?.toLowerCase().includes(commandSearch.toLowerCase()) || inv.invNumber?.toLowerCase().includes(commandSearch.toLowerCase())).length === 0 && (
No matching invoices
)} )}
)}
OmniParse AI
Privacy Terms Support
{showShortcuts && (
setShowShortcuts(false)}>
e.stopPropagation()}>

Keyboard Shortcuts

{[ { keys: ['Ctrl', 'K'], desc: 'Go to Invoices' }, { keys: ['Ctrl', '/'], desc: 'Show shortcuts' }, { keys: ['Enter'], desc: 'Send chat message' }, ].map((s) => (
{s.desc}
{s.keys.map((k, i) => ( {k} {i < s.keys.length - 1 && +} ))}
))}
)} {editingInvoice && (
setEditingInvoice(null)}>
e.stopPropagation()}>

Edit Invoice

{[ { key: 'vendor', label: 'Vendor' }, { key: 'invNumber', label: 'Invoice Number' }, { key: 'invDate', label: 'Invoice Date' }, { key: 'dueDate', label: 'Due Date' }, { key: 'amount', label: 'Amount' }, { key: 'vatAmount', label: 'VAT Amount' }, { key: 'total', label: 'Total' }, { key: 'currency', label: 'Currency' }, ].map(({ key, label }) => (
setEditForm(prev => ({ ...prev, [key]: e.target.value }))} className="input-field input-field-glow" />
))}
{['done', 'review'].map((s) => ( ))}
)}
); } return null; } // ────────────────────────────────────────────────────────────────────────────── // ─── Analytics Tab Component ──────────────────────────────────────────────────── const CHART_COLORS = { amber: '#F59E0B', teal: '#14B8A6', red: '#EF4444', green: '#22C55E', amberLight: 'rgba(245, 158, 11, 0.15)', }; function AnalyticsTab({ invoices }: { invoices: InvoiceRow[] }) { // ── Computed analytics data ── const monthlyData = useMemo(() => { const map = new Map(); invoices.forEach((inv) => { const dateStr = inv.invDate || inv.createdAt; if (!dateStr) return; const monthKey = dateStr.substring(0, 7); // YYYY-MM map.set(monthKey, (map.get(monthKey) || 0) + (inv.total ?? 0)); }); return Array.from(map.entries()) .sort(([a], [b]) => a.localeCompare(b)) .map(([month, total]) => { const [y, m] = month.split('-'); const label = new Date(Number(y), Number(m) - 1).toLocaleString('en-US', { month: 'short', year: '2-digit', }); return { month: label, total: Math.round(total * 100) / 100 }; }); }, [invoices]); const vendorData = useMemo(() => { const map = new Map(); invoices.forEach((inv) => { const vendor = inv.vendor || 'Unknown'; map.set(vendor, (map.get(vendor) || 0) + (inv.total ?? 0)); }); return Array.from(map.entries()) .sort((a, b) => b[1] - a[1]) .slice(0, 8) .map(([name, total]) => ({ name: name.length > 18 ? name.slice(0, 16) + '…' : name, total: Math.round(total * 100) / 100 })); }, [invoices]); const statusData = useMemo(() => { const counts = { Done: 0, Review: 0, Duplicate: 0 }; invoices.forEach((inv) => { if (inv.status === 'done') counts.Done++; else if (inv.status === 'review') counts.Review++; else if (inv.status === 'duplicate' || inv.isDuplicate) counts.Duplicate++; }); return [ { name: 'Done', value: counts.Done, color: CHART_COLORS.teal }, { name: 'Review', value: counts.Review, color: CHART_COLORS.amber }, { name: 'Duplicate', value: counts.Duplicate, color: CHART_COLORS.red }, ].filter((d) => d.value > 0); }, [invoices]); const currencyData = useMemo(() => { const map = new Map(); invoices.forEach((inv) => map.set(inv.currency, (map.get(inv.currency) || 0) + 1)); const entries = Array.from(map.entries()).sort((a, b) => b[1] - a[1]); const maxCount = entries.length > 0 ? entries[0][1] : 1; return entries.map(([currency, count]) => ({ currency, count, pct: Math.round((count / maxCount) * 100) })); }, [invoices]); const quickStats = useMemo(() => { const totals = invoices.map((inv) => inv.total ?? 0).filter((v) => v > 0); const vendors = invoices.filter((inv) => inv.vendor); const vendorCounts = new Map(); vendors.forEach((inv) => { const v = inv.vendor as string; vendorCounts.set(v, (vendorCounts.get(v) || 0) + 1); }); let mostActive = '--'; let maxCount = 0; vendorCounts.forEach((count, name) => { if (count > maxCount) { mostActive = name; maxCount = count; } }); return { highest: totals.length > 0 ? Math.max(...totals) : null, lowest: totals.length > 0 ? Math.min(...totals) : null, mostActiveVendor: mostActive, average: totals.length > 0 ? totals.reduce((a, b) => a + b, 0) / totals.length : null, }; }, [invoices]); const hasData = invoices.length > 0; return (
{/* Section Header */}

Analytics

Visual insights into your invoice data

{!hasData ? (

No invoice data yet

Upload invoices to see analytics here

) : ( <> {/* ── Quick Stats Summary ── */}
Highest Invoice
{quickStats.highest !== null ? formatCurrency(quickStats.highest) : '--'}
Lowest Invoice
{quickStats.lowest !== null ? formatCurrency(quickStats.lowest) : '--'}
Most Active Vendor
{quickStats.mostActiveVendor}
Average Amount
{quickStats.average !== null ? formatCurrency(quickStats.average) : '--'}
{/* ── Charts Grid ── */}
{/* a) Monthly Spending Trend */}

Monthly Spending Trend

{monthlyData.length === 0 ? (
No date data available
) : ( `$${(v / 1000).toFixed(v >= 1000 ? 1 : 0)}k`} /> [formatCurrency(value), 'Total']} /> )}
{/* b) Vendor Spending Breakdown */}

Top Vendors by Spending

{vendorData.length === 0 ? (
No vendor data available
) : ( `$${(v / 1000).toFixed(v >= 1000 ? 1 : 0)}k`} /> [formatCurrency(value), 'Total']} /> )}
{/* c) Status Distribution */}

Status Distribution

{statusData.length === 0 ? (
No status data available
) : (
{statusData.map((entry, idx) => ( ))} [`${value} invoices`, 'Count']} />
{statusData.map((entry) => (
{entry.name} {entry.value}
))}
)}
{/* d) Currency Distribution */}

Currency Distribution

{currencyData.length === 0 ? (
No currency data available
) : (
{currencyData.map(({ currency, count, pct }) => (
{currency}
{count}
))}
)}
)}
); } // Invoice Table Row Component (with expandable detail panel) // ────────────────────────────────────────────────────────────────────────────── function InvoiceTableBody({ invoice, isExpanded, onToggle, onDelete, isSelected, onSelect, onEdit, }: { invoice: InvoiceRow; isExpanded: boolean; onToggle: () => void; onDelete: () => void; isSelected: boolean; onSelect: () => void; onEdit?: () => void; }) { const statusBadgeClass = invoice.isDuplicate ? 'badge-amber-enhanced' : invoice.status === 'review' ? 'badge-amber-enhanced' : 'badge-teal-enhanced'; const statusLabel = invoice.isDuplicate ? 'Duplicate' : invoice.status === 'review' ? 'Review' : 'Done'; return ( <> { if (e.key === 'Enter' || e.key === ' ') onToggle(); }} aria-expanded={isExpanded} > { e.stopPropagation(); onSelect(); }} onClick={(e) => e.stopPropagation()} className="invoice-checkbox" aria-label={`Select invoice ${invoice.vendor || invoice.invNumber}`} />
{isExpanded ? : } {invoice.vendor || '--'}
{invoice.invNumber || '--'} {invoice.invDate || '--'} {formatCurrency(invoice.amount, invoice.currency)} {formatCurrency(invoice.total, invoice.currency)} {statusLabel} {invoice.confidence ? `${(invoice.confidence * 100).toFixed(1)}%` : '--'}
Edit
Delete
{isExpanded && (

Invoice Details

Filename
{invoice.filename || '--'}
Due Date
{invoice.dueDate || '--'}
VAT Amount
{formatCurrency(invoice.vatAmount, invoice.currency)}
Created
{new Date(invoice.createdAt).toLocaleDateString()}

Raw JSON

{JSON.stringify({
                  vendor: invoice.vendor,
                  invNumber: invoice.invNumber,
                  invDate: invoice.invDate,
                  dueDate: invoice.dueDate,
                  amount: invoice.amount,
                  vatAmount: invoice.vatAmount,
                  total: invoice.total,
                  currency: invoice.currency,
                  confidence: invoice.confidence,
                  status: invoice.status,
                  isDuplicate: invoice.isDuplicate,
                  filename: invoice.filename,
                }, null, 2)}
)} ); }