2import { useEffect, useState } from 'react';
3import { useRouter } from 'next/navigation';
4import { apiClient } from '@/lib/client/apiClient';
5import { Icon } from '@/contexts/IconContext';
14 customerName?: string;
17export default function InvoicesPage() {
18 const router = useRouter();
19 const [invoices, setInvoices] = useState<Invoice[]>([]);
20 const [loading, setLoading] = useState(true);
21 const [error, setError] = useState<string | null>(null);
22 const [searchTerm, setSearchTerm] = useState('');
23 const [editingInvoice, setEditingInvoice] = useState<Invoice | null>(null);
29 async function loadInvoices() {
34 console.log('[Invoices] Fetching sales/invoices...');
36 // Fetch sales/invoices from ELINK endpoint
37 const response = await fetch('/api/v1/elink/sales?limit=100');
40 throw new Error(`HTTP error! status: ${response.status}`);
43 const result = await response.json();
45 console.log('[Invoices] API Response:', result);
47 if (result.success && result.data) {
48 // Map ELINK fields to our interface
49 // ELINK sale fields: f100=SaleID, f101=CreatedDate, f102=Total, f109=CompletedDate, f117=Phase, f125=Classification
50 const mappedInvoices = Array.isArray(result.data) ? result.data.map((sale: any) => ({
52 externalId: sale.f500 || null,
53 completedDt: sale.f109 || sale.f101 || null,
54 total: parseFloat(sale.f102 || 0),
55 phase: parseInt(sale.f117 || 0),
56 customerId: sale.f20 || null,
57 customerName: sale.customerName || null
60 console.log(`[Invoices] Loaded ${mappedInvoices.length} invoices`);
61 console.log('[Invoices] Sample mapped invoice:', mappedInvoices[0]);
62 setInvoices(mappedInvoices);
64 console.warn('[Invoices] No data in response:', result);
67 setError(result.error);
71 console.error('[Invoices] Failed to load:', err);
72 setError(`Failed to load sales history: ${err instanceof Error ? err.message : 'Unknown error'}`);
79 async function deleteInvoice(invoice: Invoice) {
80 if (!confirm(`Are you sure you want to void/delete sale ${invoice.externalId || invoice.sid}?`)) {
85 const response = await fetch(`/api/v1/elink/sales/${invoice.sid}`, {
88 const result = await response.json();
91 alert('✅ Sale voided successfully');
94 alert(`❌ Failed to void invoice: ${result.error || result.details}`);
97 console.error('Delete failed:', error);
98 alert(`❌ Failed to void invoice: ${error instanceof Error ? error.message : 'Unknown error'}`);
102 function formatDate(dateStr?: string) {
103 if (!dateStr) return 'N/A';
105 // Handle ELINK pipe-delimited format: "YYYY|M|DD|HH|MM|SS||"
106 if (dateStr.includes('|')) {
107 const parts = dateStr.split('|').filter(p => p !== '');
108 if (parts.length >= 6) {
109 const [year, month, day, hour, minute, second] = parts.map(p => parseInt(p));
110 const date = new Date(year, month - 1, day, hour, minute, second);
111 return date.toLocaleString('en-US', {
120 // Fallback to standard date parsing
121 return new Date(dateStr).toLocaleString();
127 const filteredInvoices = invoices.filter(inv => {
128 if (!searchTerm) return true;
129 const search = searchTerm.toLowerCase();
131 inv.externalId?.toLowerCase().includes(search) ||
132 inv.sid?.toString().includes(search) ||
133 inv.customerName?.toLowerCase().includes(search) ||
134 inv.customerId?.toLowerCase().includes(search)
139 <div className="p-8 h-full overflow-y-auto bg-bg">
140 <div className="max-w-7xl mx-auto">
141 <div className="flex justify-between items-center mb-6">
142 <h1 className="text-3xl font-bold text-text">Sales History</h1>
145 className="px-4 py-2 bg-brand text-surface rounded-lg hover:bg-brand2 font-semibold inline-flex items-center gap-2"
147 <Icon name="add" size={20} /> New Sale
151 {/* Search and Filters */}
152 <div className="bg-surface shadow-lg rounded-lg p-6 mb-6">
153 <div className="flex gap-4">
156 placeholder="Search by Sale ID, Customer, or Amount..."
158 onChange={(e) => setSearchTerm(e.target.value)}
159 className="flex-1 px-4 py-2 border border-border rounded-lg focus:ring-2 focus:ring-brand focus:border-brand"
162 onClick={loadInvoices}
163 className="px-6 py-2 bg-surface-2 text-text rounded-lg hover:bg-surface font-semibold flex items-center gap-2"
165 <Icon name="refresh" size={18} /> Refresh
170 {/* Invoices Table */}
171 <div className="bg-surface rounded-lg shadow-sm border border-border">
173 <div className="p-12 text-center text-muted">
174 Loading sales history...
177 <div className="p-12 text-center">
178 <div className="text-danger mb-4">{error}</div>
180 onClick={loadInvoices}
181 className="px-4 py-2 bg-brand text-surface rounded hover:bg-brand2"
186 ) : filteredInvoices.length === 0 ? (
187 <div className="p-12 text-center">
188 <div className="text-6xl mb-4"><Icon name="receipt_long" size={72} /></div>
189 <h3 className="text-xl font-semibold text-text mb-2">
190 {searchTerm ? 'No matching sales' : 'No sales yet'}
192 <p className="text-muted mb-6">
194 ? 'Try a different search term'
195 : 'Process your first sale to get started'}
199 onClick={() => window.location.href = '/sales'}
200 className="px-6 py-3 bg-brand text-surface rounded-lg hover:bg-brand2 font-semibold"
207 <div className="overflow-x-auto">
208 <table className="w-full">
209 <thead className="bg-[var(--brand)] text-surface">
211 <th className="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider">
214 <th className="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider">
217 <th className="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider">
220 <th className="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider">
223 <th className="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider">
226 <th className="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider">
229 <th className="px-6 py-3 text-center text-xs font-medium uppercase tracking-wider">
234 <tbody className="bg-surface divide-y divide-border">
235 {filteredInvoices.map((invoice) => (
236 <tr key={invoice.sid} className="hover:bg-surface-2">
237 <td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-text">
240 <td className="px-6 py-4 whitespace-nowrap text-sm text-muted">
241 {invoice.externalId || '-'}
243 <td className="px-6 py-4 whitespace-nowrap text-sm text-muted">
244 {formatDate(invoice.completedDt)}
246 <td className="px-6 py-4 whitespace-nowrap text-sm text-muted">
247 {invoice.customerName || invoice.customerId || 'Guest'}
249 <td className="px-6 py-4 whitespace-nowrap text-sm font-semibold text-text">
250 ${invoice.total?.toFixed(2) || '0.00'}
252 <td className="px-6 py-4 whitespace-nowrap">
254 className={`px-2 py-1 text-xs font-semibold rounded ${
256 ? 'bg-success/20 text-success'
257 : invoice.phase === 200
258 ? 'bg-warn/20 text-warn'
259 : 'bg-surface-2 text-muted'
264 : invoice.phase === 200
266 : `Phase ${invoice.phase || 'Unknown'}`}
269 <td className="px-6 py-4 whitespace-nowrap text-center">
271 onClick={() => router.push(`/pages/invoices/${invoice.sid}`)}
272 className="px-3 py-1 bg-info text-surface rounded hover:bg-info/80 text-sm font-medium flex items-center gap-1"
274 <Icon name="visibility" size={14} /> View
285 {/* Edit Invoice Modal */}
287 <div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
288 <div className="bg-surface rounded-lg p-8 max-w-2xl w-full mx-4 max-h-[90vh] overflow-y-auto">
289 <div className="flex justify-between items-center mb-6">
290 <h2 className="text-2xl font-bold text-text">
291 Edit Sale #{editingInvoice.sid}
294 onClick={() => setEditingInvoice(null)}
295 className="text-muted hover:text-text text-2xl"
301 <form onSubmit={async (e) => {
303 const formData = new FormData(e.currentTarget);
305 const updatedInvoice = {
306 sid: editingInvoice.sid,
307 externalId: formData.get('externalId') as string,
308 total: parseFloat(formData.get('total') as string),
309 phase: parseInt(formData.get('phase') as string),
310 customerId: formData.get('customerId') as string,
311 customerName: formData.get('customerName') as string,
315 const response = await fetch(`/api/v1/elink/sales/${editingInvoice.sid}`, {
317 headers: { 'Content-Type': 'application/json' },
318 body: JSON.stringify(updatedInvoice)
321 const result = await response.json();
323 if (result.success) {
324 alert('✅ Sale updated successfully');
325 setEditingInvoice(null);
328 alert(`❌ Failed to update sale: ${result.error}`);
331 console.error('Update failed:', error);
332 alert(`❌ Failed to update sale: ${error instanceof Error ? error.message : 'Unknown error'}`);
335 <div className="space-y-4">
337 <label className="block text-sm font-medium text-muted mb-2">
338 Invoice ID (Read-only)
342 value={editingInvoice.sid}
344 className="w-full px-4 py-2 bg-surface-2 border border-border rounded-lg text-muted"
349 <label className="block text-sm font-medium text-muted mb-2">
350 External ID / Receipt Number
355 defaultValue={editingInvoice.externalId || ''}
356 className="w-full px-4 py-2 border border-border rounded-lg focus:ring-2 focus:ring-brand focus:border-brand"
361 <label className="block text-sm font-medium text-muted mb-2">
367 defaultValue={editingInvoice.customerName || ''}
368 className="w-full px-4 py-2 border border-border rounded-lg focus:ring-2 focus:ring-brand focus:border-brand"
373 <label className="block text-sm font-medium text-muted mb-2">
379 defaultValue={editingInvoice.customerId || ''}
380 className="w-full px-4 py-2 border border-border rounded-lg focus:ring-2 focus:ring-brand focus:border-brand"
385 <label className="block text-sm font-medium text-muted mb-2">
392 defaultValue={editingInvoice.total || 0}
393 className="w-full px-4 py-2 border border-border rounded-lg focus:ring-2 focus:ring-brand focus:border-brand"
399 <label className="block text-sm font-medium text-muted mb-2">
404 defaultValue={editingInvoice.phase || 0}
405 className="w-full px-4 py-2 border border-border rounded-lg focus:ring-2 focus:ring-brand focus:border-brand"
407 <option value="0">Draft</option>
408 <option value="1">Complete</option>
409 <option value="200">Pending</option>
410 <option value="300">Void</option>
415 <div className="mt-6 flex justify-end gap-3">
418 onClick={() => setEditingInvoice(null)}
419 className="px-6 py-2 bg-surface-2 text-text rounded hover:bg-surface"
425 className="px-6 py-2 bg-brand text-surface rounded hover:bg-brand2 flex items-center gap-2"
427 <Icon name="save" size={18} /> Save Changes