EverydayTech Platform - Developer Reference
Complete Source Code Documentation - All Applications
Loading...
Searching...
No Matches
page.tsx
Go to the documentation of this file.
1"use client";
2import { useEffect, useState } from 'react';
3import { useRouter } from 'next/navigation';
4import { apiClient } from '@/lib/client/apiClient';
5import { Icon } from '@/contexts/IconContext';
6
7interface Invoice {
8 sid: number;
9 externalId?: string;
10 completedDt?: string;
11 total?: number;
12 phase?: number;
13 customerId?: string;
14 customerName?: string;
15}
16
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);
24
25 useEffect(() => {
26 loadInvoices();
27 }, []);
28
29 async function loadInvoices() {
30 try {
31 setLoading(true);
32 setError(null);
33
34 console.log('[Invoices] Fetching sales/invoices...');
35
36 // Fetch sales/invoices from ELINK endpoint
37 const response = await fetch('/api/v1/elink/sales?limit=100');
38
39 if (!response.ok) {
40 throw new Error(`HTTP error! status: ${response.status}`);
41 }
42
43 const result = await response.json();
44
45 console.log('[Invoices] API Response:', result);
46
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) => ({
51 sid: sale.f100 || 0,
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
58 })) : [];
59
60 console.log(`[Invoices] Loaded ${mappedInvoices.length} invoices`);
61 console.log('[Invoices] Sample mapped invoice:', mappedInvoices[0]);
62 setInvoices(mappedInvoices);
63 } else {
64 console.warn('[Invoices] No data in response:', result);
65 setInvoices([]);
66 if (result.error) {
67 setError(result.error);
68 }
69 }
70 } catch (err) {
71 console.error('[Invoices] Failed to load:', err);
72 setError(`Failed to load sales history: ${err instanceof Error ? err.message : 'Unknown error'}`);
73 setInvoices([]);
74 } finally {
75 setLoading(false);
76 }
77 }
78
79 async function deleteInvoice(invoice: Invoice) {
80 if (!confirm(`Are you sure you want to void/delete sale ${invoice.externalId || invoice.sid}?`)) {
81 return;
82 }
83
84 try {
85 const response = await fetch(`/api/v1/elink/sales/${invoice.sid}`, {
86 method: 'DELETE'
87 });
88 const result = await response.json();
89
90 if (result.success) {
91 alert('✅ Sale voided successfully');
92 loadInvoices();
93 } else {
94 alert(`❌ Failed to void invoice: ${result.error || result.details}`);
95 }
96 } catch (error) {
97 console.error('Delete failed:', error);
98 alert(`❌ Failed to void invoice: ${error instanceof Error ? error.message : 'Unknown error'}`);
99 }
100 }
101
102 function formatDate(dateStr?: string) {
103 if (!dateStr) return 'N/A';
104 try {
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', {
112 year: 'numeric',
113 month: 'short',
114 day: 'numeric',
115 hour: '2-digit',
116 minute: '2-digit'
117 });
118 }
119 }
120 // Fallback to standard date parsing
121 return new Date(dateStr).toLocaleString();
122 } catch {
123 return dateStr;
124 }
125 }
126
127 const filteredInvoices = invoices.filter(inv => {
128 if (!searchTerm) return true;
129 const search = searchTerm.toLowerCase();
130 return (
131 inv.externalId?.toLowerCase().includes(search) ||
132 inv.sid?.toString().includes(search) ||
133 inv.customerName?.toLowerCase().includes(search) ||
134 inv.customerId?.toLowerCase().includes(search)
135 );
136 });
137
138 return (
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>
143 <a
144 href="/sales"
145 className="px-4 py-2 bg-brand text-surface rounded-lg hover:bg-brand2 font-semibold inline-flex items-center gap-2"
146 >
147 <Icon name="add" size={20} /> New Sale
148 </a>
149 </div>
150
151 {/* Search and Filters */}
152 <div className="bg-surface shadow-lg rounded-lg p-6 mb-6">
153 <div className="flex gap-4">
154 <input
155 type="text"
156 placeholder="Search by Sale ID, Customer, or Amount..."
157 value={searchTerm}
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"
160 />
161 <button
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"
164 >
165 <Icon name="refresh" size={18} /> Refresh
166 </button>
167 </div>
168 </div>
169
170 {/* Invoices Table */}
171 <div className="bg-surface rounded-lg shadow-sm border border-border">
172 {loading ? (
173 <div className="p-12 text-center text-muted">
174 Loading sales history...
175 </div>
176 ) : error ? (
177 <div className="p-12 text-center">
178 <div className="text-danger mb-4">{error}</div>
179 <button
180 onClick={loadInvoices}
181 className="px-4 py-2 bg-brand text-surface rounded hover:bg-brand2"
182 >
183 Try Again
184 </button>
185 </div>
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'}
191 </h3>
192 <p className="text-muted mb-6">
193 {searchTerm
194 ? 'Try a different search term'
195 : 'Process your first sale to get started'}
196 </p>
197 {!searchTerm && (
198 <button
199 onClick={() => window.location.href = '/sales'}
200 className="px-6 py-3 bg-brand text-surface rounded-lg hover:bg-brand2 font-semibold"
201 >
202 Process First Sale
203 </button>
204 )}
205 </div>
206 ) : (
207 <div className="overflow-x-auto">
208 <table className="w-full">
209 <thead className="bg-[var(--brand)] text-surface">
210 <tr>
211 <th className="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider">
212 Sale ID
213 </th>
214 <th className="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider">
215 External ID
216 </th>
217 <th className="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider">
218 Date
219 </th>
220 <th className="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider">
221 Customer
222 </th>
223 <th className="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider">
224 Total
225 </th>
226 <th className="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider">
227 Status
228 </th>
229 <th className="px-6 py-3 text-center text-xs font-medium uppercase tracking-wider">
230 Actions
231 </th>
232 </tr>
233 </thead>
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">
238 #{invoice.sid}
239 </td>
240 <td className="px-6 py-4 whitespace-nowrap text-sm text-muted">
241 {invoice.externalId || '-'}
242 </td>
243 <td className="px-6 py-4 whitespace-nowrap text-sm text-muted">
244 {formatDate(invoice.completedDt)}
245 </td>
246 <td className="px-6 py-4 whitespace-nowrap text-sm text-muted">
247 {invoice.customerName || invoice.customerId || 'Guest'}
248 </td>
249 <td className="px-6 py-4 whitespace-nowrap text-sm font-semibold text-text">
250 ${invoice.total?.toFixed(2) || '0.00'}
251 </td>
252 <td className="px-6 py-4 whitespace-nowrap">
253 <span
254 className={`px-2 py-1 text-xs font-semibold rounded ${
255 invoice.phase === 1
256 ? 'bg-success/20 text-success'
257 : invoice.phase === 200
258 ? 'bg-warn/20 text-warn'
259 : 'bg-surface-2 text-muted'
260 }`}
261 >
262 {invoice.phase === 1
263 ? 'Complete'
264 : invoice.phase === 200
265 ? 'Pending'
266 : `Phase ${invoice.phase || 'Unknown'}`}
267 </span>
268 </td>
269 <td className="px-6 py-4 whitespace-nowrap text-center">
270 <button
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"
273 >
274 <Icon name="visibility" size={14} /> View
275 </button>
276 </td>
277 </tr>
278 ))}
279 </tbody>
280 </table>
281 </div>
282 )}
283 </div>
284
285 {/* Edit Invoice Modal */}
286 {editingInvoice && (
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}
292 </h2>
293 <button
294 onClick={() => setEditingInvoice(null)}
295 className="text-muted hover:text-text text-2xl"
296 >
297 ×
298 </button>
299 </div>
300
301 <form onSubmit={async (e) => {
302 e.preventDefault();
303 const formData = new FormData(e.currentTarget);
304
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,
312 };
313
314 try {
315 const response = await fetch(`/api/v1/elink/sales/${editingInvoice.sid}`, {
316 method: 'PUT',
317 headers: { 'Content-Type': 'application/json' },
318 body: JSON.stringify(updatedInvoice)
319 });
320
321 const result = await response.json();
322
323 if (result.success) {
324 alert('✅ Sale updated successfully');
325 setEditingInvoice(null);
326 loadInvoices();
327 } else {
328 alert(`❌ Failed to update sale: ${result.error}`);
329 }
330 } catch (error) {
331 console.error('Update failed:', error);
332 alert(`❌ Failed to update sale: ${error instanceof Error ? error.message : 'Unknown error'}`);
333 }
334 }}>
335 <div className="space-y-4">
336 <div>
337 <label className="block text-sm font-medium text-muted mb-2">
338 Invoice ID (Read-only)
339 </label>
340 <input
341 type="text"
342 value={editingInvoice.sid}
343 disabled
344 className="w-full px-4 py-2 bg-surface-2 border border-border rounded-lg text-muted"
345 />
346 </div>
347
348 <div>
349 <label className="block text-sm font-medium text-muted mb-2">
350 External ID / Receipt Number
351 </label>
352 <input
353 type="text"
354 name="externalId"
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"
357 />
358 </div>
359
360 <div>
361 <label className="block text-sm font-medium text-muted mb-2">
362 Customer Name
363 </label>
364 <input
365 type="text"
366 name="customerName"
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"
369 />
370 </div>
371
372 <div>
373 <label className="block text-sm font-medium text-muted mb-2">
374 Customer ID
375 </label>
376 <input
377 type="text"
378 name="customerId"
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"
381 />
382 </div>
383
384 <div>
385 <label className="block text-sm font-medium text-muted mb-2">
386 Total Amount
387 </label>
388 <input
389 type="number"
390 name="total"
391 step="0.01"
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"
394 required
395 />
396 </div>
397
398 <div>
399 <label className="block text-sm font-medium text-muted mb-2">
400 Status / Phase
401 </label>
402 <select
403 name="phase"
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"
406 >
407 <option value="0">Draft</option>
408 <option value="1">Complete</option>
409 <option value="200">Pending</option>
410 <option value="300">Void</option>
411 </select>
412 </div>
413 </div>
414
415 <div className="mt-6 flex justify-end gap-3">
416 <button
417 type="button"
418 onClick={() => setEditingInvoice(null)}
419 className="px-6 py-2 bg-surface-2 text-text rounded hover:bg-surface"
420 >
421 Cancel
422 </button>
423 <button
424 type="submit"
425 className="px-6 py-2 bg-brand text-surface rounded hover:bg-brand2 flex items-center gap-2"
426 >
427 <Icon name="save" size={18} /> Save Changes
428 </button>
429 </div>
430 </form>
431 </div>
432 </div>
433 )}
434 </div>
435 </div>
436 );
437}