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 { useParams, useRouter } from 'next/navigation';
4import { Icon } from '@/contexts/IconContext';
5
6interface LineItem {
7 f105?: number; // Sale ID
8 f200?: number; // Product ID
9 f202?: number; // Quantity
10 f203?: number; // Line total
11 f205?: number; // Unit price
12 f212?: string; // Product description
13}
14
15interface SaleDetails {
16 f100?: number; // Sale ID
17 f101?: string; // Created date
18 f102?: number; // Total ex tax
19 f103?: number; // Total inc tax
20 f109?: string; // Completed date
21 f117?: number; // Phase
22 LINE?: LineItem[];
23 LOCN?: any[];
24 PAYM?: any[];
25}
26
27export default function InvoiceDetailPage() {
28 const params = useParams();
29 const router = useRouter();
30 const saleId = params.id as string;
31
32 const [saleDetails, setSaleDetails] = useState<SaleDetails | null>(null);
33 const [loading, setLoading] = useState(true);
34 const [error, setError] = useState<string | null>(null);
35
36 useEffect(() => {
37 if (saleId) {
38 loadSaleDetails(parseInt(saleId));
39 }
40 }, [saleId]);
41
42 async function loadSaleDetails(id: number) {
43 try {
44 setLoading(true);
45 setError(null);
46
47 console.log('[Sale Details] Fetching details for sale:', id);
48
49 const response = await fetch(`/api/v1/elink/sales/${id}`);
50
51 if (!response.ok) {
52 throw new Error(`HTTP error! status: ${response.status}`);
53 }
54
55 const result = await response.json();
56
57 if (result.success && result.data) {
58 console.log('[Sale Details] Loaded:', result.data);
59 setSaleDetails(result.data);
60 } else {
61 throw new Error(result.error || 'Failed to load sale details');
62 }
63 } catch (err) {
64 console.error('[Sale Details] Failed to load:', err);
65 setError(`Failed to load sale details: ${err instanceof Error ? err.message : 'Unknown error'}`);
66 } finally {
67 setLoading(false);
68 }
69 }
70
71 async function deleteSale() {
72 if (!confirm(`Are you sure you want to void/delete sale #${saleId}?`)) {
73 return;
74 }
75
76 try {
77 const response = await fetch(`/api/v1/elink/sales/${saleId}`, {
78 method: 'DELETE'
79 });
80 const result = await response.json();
81
82 if (result.success) {
83 alert('✅ Sale voided successfully');
84 router.push('/pages/invoices');
85 } else {
86 alert(`❌ Failed to void sale: ${result.error || result.details}`);
87 }
88 } catch (error) {
89 console.error('Delete failed:', error);
90 alert(`❌ Failed to void sale: ${error instanceof Error ? error.message : 'Unknown error'}`);
91 }
92 }
93
94 function formatDate(dateStr?: string) {
95 if (!dateStr) return 'N/A';
96 try {
97 // Handle ELINK pipe-delimited format: "YYYY|M|DD|HH|MM|SS||"
98 if (dateStr.includes('|')) {
99 const parts = dateStr.split('|').filter(p => p !== '');
100 if (parts.length >= 6) {
101 const [year, month, day, hour, minute, second] = parts.map(p => parseInt(p));
102 const date = new Date(year, month - 1, day, hour, minute, second);
103 return date.toLocaleString('en-US', {
104 year: 'numeric',
105 month: 'short',
106 day: 'numeric',
107 hour: '2-digit',
108 minute: '2-digit'
109 });
110 }
111 }
112 // Fallback to standard date parsing
113 return new Date(dateStr).toLocaleString();
114 } catch {
115 return dateStr;
116 }
117 }
118
119 if (loading) {
120 return (
121 <div className="p-8 h-full overflow-y-auto bg-bg">
122 <div className="max-w-7xl mx-auto">
123 <div className="text-center py-16">
124 <Icon name="hourglass_empty" size={48} className="mx-auto text-muted" />
125 <p className="mt-4 text-xl text-muted">Loading sale details...</p>
126 </div>
127 </div>
128 </div>
129 );
130 }
131
132 if (error || !saleDetails) {
133 return (
134 <div className="p-8 h-full overflow-y-auto bg-bg">
135 <div className="max-w-7xl mx-auto">
136 <button
137 onClick={() => router.push('/pages/invoices')}
138 className="mb-6 px-4 py-2 bg-surface text-text rounded-lg hover:bg-surface-2 inline-flex items-center gap-2"
139 >
140 <Icon name="arrow_back" size={20} /> Back to Sales History
141 </button>
142
143 <div className="text-center py-16">
144 <Icon name="error" size={48} className="mx-auto text-danger" />
145 <p className="mt-4 text-xl text-text">Failed to load sale details</p>
146 <p className="mt-2 text-muted">{error}</p>
147 </div>
148 </div>
149 </div>
150 );
151 }
152
153 return (
154 <div className="p-8 h-full overflow-y-auto bg-bg">
155 <div className="max-w-7xl mx-auto">
156 {/* Header */}
157 <div className="flex justify-between items-center mb-6">
158 <div className="flex items-center gap-4">
159 <button
160 onClick={() => router.push('/pages/invoices')}
161 className="px-4 py-2 bg-surface text-text rounded-lg hover:bg-surface-2 inline-flex items-center gap-2"
162 >
163 <Icon name="arrow_back" size={20} /> Back
164 </button>
165 <h1 className="text-3xl font-bold text-text">Sale #{saleDetails.f100}</h1>
166 </div>
167 <div className="flex gap-3">
168 <button
169 onClick={() => window.print()}
170 className="px-4 py-2 bg-info text-surface rounded-lg hover:bg-info/80 inline-flex items-center gap-2"
171 >
172 <Icon name="print" size={20} /> Print
173 </button>
174 <button
175 onClick={deleteSale}
176 className="px-4 py-2 bg-danger text-surface rounded-lg hover:bg-danger/80 inline-flex items-center gap-2"
177 >
178 <Icon name="delete" size={20} /> Void Sale
179 </button>
180 </div>
181 </div>
182
183 {/* Sale Details Card */}
184 <div className="bg-surface rounded-lg shadow-sm p-8 mb-6">
185 <div className="grid grid-cols-1 md:grid-cols-3 gap-6">
186 <div>
187 <label className="block text-sm font-medium text-muted mb-2">
188 Sale ID
189 </label>
190 <div className="text-2xl font-bold text-text">#{saleDetails.f100}</div>
191 </div>
192
193 <div>
194 <label className="block text-sm font-medium text-muted mb-2">
195 Created Date
196 </label>
197 <div className="text-lg text-text">{formatDate(saleDetails.f101)}</div>
198 </div>
199
200 <div>
201 <label className="block text-sm font-medium text-muted mb-2">
202 Completed Date
203 </label>
204 <div className="text-lg text-text">{formatDate(saleDetails.f109)}</div>
205 </div>
206
207 <div>
208 <label className="block text-sm font-medium text-muted mb-2">
209 Total (Ex Tax)
210 </label>
211 <div className="text-2xl font-semibold text-text">
212 ${saleDetails.f102?.toFixed(2) || '0.00'}
213 </div>
214 </div>
215
216 <div>
217 <label className="block text-sm font-medium text-muted mb-2">
218 Total (Inc Tax)
219 </label>
220 <div className="text-2xl font-bold text-brand">
221 ${saleDetails.f103?.toFixed(2) || '0.00'}
222 </div>
223 </div>
224
225 <div>
226 <label className="block text-sm font-medium text-muted mb-2">
227 Status
228 </label>
229 <div>
230 <span
231 className={`px-4 py-2 text-sm font-semibold rounded inline-block ${
232 saleDetails.f117 === 1
233 ? 'bg-success/20 text-success'
234 : saleDetails.f117 === 200
235 ? 'bg-warn/20 text-warn'
236 : 'bg-surface-2 text-muted'
237 }`}
238 >
239 {saleDetails.f117 === 1
240 ? 'Complete'
241 : saleDetails.f117 === 200
242 ? 'Pending'
243 : `Phase ${saleDetails.f117 || 'Unknown'}`}
244 </span>
245 </div>
246 </div>
247 </div>
248 </div>
249
250 {/* Line Items */}
251 {saleDetails.LINE && saleDetails.LINE.length > 0 && (
252 <div className="bg-surface rounded-lg shadow-sm p-8 mb-6">
253 <h2 className="text-2xl font-bold text-text mb-6">Line Items</h2>
254 <div className="overflow-x-auto">
255 <table className="w-full">
256 <thead className="bg-surface-2">
257 <tr>
258 <th className="px-6 py-3 text-left text-xs font-medium text-muted uppercase tracking-wider">
259 Product
260 </th>
261 <th className="px-6 py-3 text-right text-xs font-medium text-muted uppercase tracking-wider">
262 Quantity
263 </th>
264 <th className="px-6 py-3 text-right text-xs font-medium text-muted uppercase tracking-wider">
265 Unit Price
266 </th>
267 <th className="px-6 py-3 text-right text-xs font-medium text-muted uppercase tracking-wider">
268 Line Total
269 </th>
270 </tr>
271 </thead>
272 <tbody className="divide-y divide-border">
273 {saleDetails.LINE.map((line, idx) => (
274 <tr key={idx} className="hover:bg-surface-2">
275 <td className="px-6 py-4">
276 <div className="text-sm font-medium text-text">
277 {line.f212 || 'Unknown Product'}
278 </div>
279 <div className="text-xs text-muted">
280 Product ID: {line.f200 || 'N/A'}
281 </div>
282 </td>
283 <td className="px-6 py-4 text-right text-sm text-text">
284 {line.f202 || 0}
285 </td>
286 <td className="px-6 py-4 text-right text-sm text-text">
287 ${line.f205?.toFixed(2) || '0.00'}
288 </td>
289 <td className="px-6 py-4 text-right text-sm font-semibold text-text">
290 ${line.f203?.toFixed(2) || '0.00'}
291 </td>
292 </tr>
293 ))}
294 <tr className="bg-surface-2 font-bold">
295 <td colSpan={3} className="px-6 py-4 text-right text-text">
296 Total:
297 </td>
298 <td className="px-6 py-4 text-right text-lg text-brand">
299 ${saleDetails.LINE.reduce((sum, line) => sum + (line.f203 || 0), 0).toFixed(2)}
300 </td>
301 </tr>
302 </tbody>
303 </table>
304 </div>
305 </div>
306 )}
307
308 {/* Payments */}
309 {saleDetails.PAYM && saleDetails.PAYM.length > 0 && (
310 <div className="bg-surface rounded-lg shadow-sm p-8 mb-6">
311 <h2 className="text-2xl font-bold text-text mb-6">Payments</h2>
312 <div className="space-y-4">
313 {saleDetails.PAYM.map((payment, idx) => (
314 <div key={idx} className="flex justify-between items-center p-4 bg-surface-2 rounded-lg">
315 <div>
316 <div className="font-medium text-text">
317 {payment.f900 || payment.f200 || 'Payment Method'}
318 </div>
319 <div className="text-sm text-muted">
320 Payment ID: {payment.f100 || 'N/A'}
321 </div>
322 </div>
323 <div className="text-xl font-semibold text-text">
324 ${payment.f201?.toFixed(2) || '0.00'}
325 </div>
326 </div>
327 ))}
328 </div>
329 </div>
330 )}
331
332 {/* Location Info */}
333 {saleDetails.LOCN && saleDetails.LOCN.length > 0 && (
334 <div className="bg-surface rounded-lg shadow-sm p-8">
335 <h2 className="text-2xl font-bold text-text mb-6">Location</h2>
336 <div className="space-y-4">
337 {saleDetails.LOCN.map((location, idx) => (
338 <div key={idx} className="p-4 bg-surface-2 rounded-lg">
339 <div className="font-medium text-text">
340 {location.f101 || 'Unknown Location'}
341 </div>
342 <div className="text-sm text-muted">
343 Location ID: {location.f100 || 'N/A'}
344 </div>
345 </div>
346 ))}
347 </div>
348 </div>
349 )}
350 </div>
351 </div>
352 );
353}