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";
2
3import { useState } from "react";
4import { Icon } from '@/contexts/IconContext';
5import { useStore } from "@/contexts/StoreContext";
6
7interface SaleRow {
8 f101: number; // Sale ID
9 f102: string; // Date
10 f107: number; // Total sale
11 f104: string; // Teller name
12 f200: number; // Product ID
13 f201: string; // Product name
14 f202: number; // Qty
15 f203: number; // Line price
16 f301?: string; // Paid by
17}
18
19export default function PosSalesReportPage() {
20 const { session } = useStore();
21 const [data, setData] = useState<SaleRow[]>([]);
22 const [loading, setLoading] = useState(false);
23 const [error, setError] = useState<string | null>(null);
24
25 // Date filters - default to today
26 const [fromDate, setFromDate] = useState(() => {
27 const d = new Date();
28 d.setHours(0, 0, 0, 0);
29 return d.toISOString().split("T")[0];
30 });
31 const [toDate, setToDate] = useState(() => {
32 const d = new Date();
33 d.setDate(d.getDate() + 1);
34 return d.toISOString().split("T")[0];
35 });
36
37 const loadReport = async () => {
38 if (!session?.store) {
39 setError("No store session found");
40 return;
41 }
42
43 setLoading(true);
44 setError(null);
45 setData([]);
46
47 try {
48 // Call API endpoint that uses ELINK
49 const response = await fetch(`/api/v1/elink/sales-report?fromDate=${fromDate}&toDate=${toDate}`, {
50 method: 'GET',
51 credentials: 'include',
52 });
53
54 if (!response.ok) {
55 const errorData = await response.json();
56 throw new Error(errorData.error || `HTTP ${response.status}`);
57 }
58
59 const result = await response.json();
60
61 if (result.success && result.data?.APPT && Array.isArray(result.data.APPT)) {
62 setData(result.data.APPT);
63 } else {
64 setData([]);
65 }
66 } catch (err: any) {
67 console.error("Error loading sales report:", err);
68 setError(err.message || "Failed to load sales report");
69 setData([]);
70 } finally {
71 setLoading(false);
72 }
73 };
74
75 const formatCurrency = (value: number | undefined) => {
76 if (value === undefined || value === null) return "$0.00";
77 return new Intl.NumberFormat("en-US", {
78 style: "currency",
79 currency: "USD",
80 minimumFractionDigits: 2,
81 }).format(value);
82 };
83
84 const formatDate = (dateStr: string | undefined) => {
85 if (!dateStr) return "";
86 try {
87 // Handle pipe-delimited format: "2026|1|6|9|46|0||" (year|month|day|hour|minute|second||)
88 if (dateStr.includes('|')) {
89 const parts = dateStr.split('|').filter(p => p !== '');
90 if (parts.length >= 3) {
91 const [year, month, day, hour = '0', minute = '0', second = '0'] = parts;
92 const date = new Date(
93 parseInt(year),
94 parseInt(month) - 1, // JavaScript months are 0-indexed
95 parseInt(day),
96 parseInt(hour),
97 parseInt(minute),
98 parseInt(second)
99 );
100 return date.toLocaleString('en-US', {
101 year: 'numeric',
102 month: 'short',
103 day: 'numeric',
104 hour: '2-digit',
105 minute: '2-digit'
106 });
107 }
108 }
109 // Fallback to standard date parsing
110 const date = new Date(dateStr);
111 return date.toLocaleDateString();
112 } catch {
113 return dateStr;
114 }
115 };
116
117 // Calculate totals
118 const totalQty = data.reduce((sum, row) => sum + (row.f202 || 0), 0);
119 const totalSales = data.reduce((sum, row) => sum + (row.f203 || 0), 0);
120 const uniqueSales = new Set(data.map(row => row.f101)).size;
121
122 return (
123 <div className="min-h-screen p-4" style={{ background: 'var(--bg)' }}>
124 <div className="max-w-[1400px] mx-auto">
125 {/* Header */}
126 <div className="bg-surface rounded-lg shadow-sm p-6 mb-6 border border-border">
127 <div className="flex items-center justify-between mb-4">
128 <div>
129 <h1 className="text-2xl font-bold text-text">Sales Report</h1>
130 {session?.store && (
131 <p className="text-sm text-muted mt-1">
132 {session.store.type === 'store' ? '🏪' : '🏢'} {session.store.name}
133 </p>
134 )}
135 </div>
136 <div className="text-right">
137 <div className="text-sm text-muted">Today's Sales</div>
138 <div className="text-2xl font-bold text-brand">{formatCurrency(totalSales)}</div>
139 </div>
140 </div>
141
142 {/* Date Range Filter */}
143 <div className="flex gap-4 items-end">
144 <div className="flex-1">
145 <label className="block text-sm font-medium text-text mb-1">
146 From Date
147 </label>
148 <input
149 type="date"
150 value={fromDate}
151 onChange={(e) => setFromDate(e.target.value)}
152 className="w-full px-3 py-2 border border-border rounded-lg focus:ring-2 focus:ring-brand focus:border-transparent bg-surface text-text"
153 />
154 </div>
155 <div className="flex-1">
156 <label className="block text-sm font-medium text-text mb-1">
157 To Date
158 </label>
159 <input
160 type="date"
161 value={toDate}
162 onChange={(e) => setToDate(e.target.value)}
163 className="w-full px-3 py-2 border border-border rounded-lg focus:ring-2 focus:ring-brand focus:border-transparent bg-surface text-text"
164 />
165 </div>
166 <button
167 onClick={loadReport}
168 disabled={loading}
169 className="px-6 py-2 bg-brand text-white rounded-lg hover:opacity-90 disabled:opacity-50 disabled:cursor-not-allowed transition font-medium"
170 >
171 {loading ? "Loading..." : "Load Report"}
172 </button>
173 </div>
174 </div>
175
176 {/* Error Message */}
177 {error && (
178 <div className="bg-danger/10 border border-danger rounded-lg p-4 mb-6">
179 <div className="flex items-center gap-2 text-danger">
180 <span className="text-xl">⚠️</span>
181 <div>
182 <div className="font-semibold">Error</div>
183 <div className="text-sm">{error}</div>
184 </div>
185 </div>
186 </div>
187 )}
188
189 {/* Summary Cards */}
190 {data.length > 0 && (
191 <div className="grid grid-cols-3 gap-4 mb-6">
192 <div className="bg-surface rounded-lg shadow-sm p-4 border border-border">
193 <div className="text-sm text-muted">Total Sales</div>
194 <div className="text-2xl font-bold text-text">{uniqueSales}</div>
195 </div>
196 <div className="bg-surface rounded-lg shadow-sm p-4 border border-border">
197 <div className="text-sm text-muted">Items Sold</div>
198 <div className="text-2xl font-bold text-text">{totalQty}</div>
199 </div>
200 <div className="bg-surface rounded-lg shadow-sm p-4 border border-border">
201 <div className="text-sm text-muted">Total Amount</div>
202 <div className="text-2xl font-bold text-brand">{formatCurrency(totalSales)}</div>
203 </div>
204 </div>
205 )}
206
207 {/* Results Table */}
208 {loading && (
209 <div className="bg-surface rounded-lg shadow-sm p-12 text-center border border-border">
210 <div className="inline-block animate-spin rounded-full h-12 w-12 border-b-2 border-brand"></div>
211 <p className="mt-4 text-muted">Loading sales data...</p>
212 </div>
213 )}
214
215 {!loading && data.length === 0 && !error && (
216 <div className="bg-surface rounded-lg shadow-sm p-12 text-center border border-border">
217 <Icon name="assessment" size={64} className="inline-block mb-4 text-muted" />
218 <h3 className="text-lg font-semibold text-text mb-2">No Sales Data</h3>
219 <p className="text-muted">Select a date range and click "Load Report" to view sales.</p>
220 </div>
221 )}
222
223 {!loading && data.length > 0 && (
224 <div className="bg-surface rounded-lg shadow-sm overflow-hidden border border-border">
225 <div className="overflow-x-auto">
226 <table className="w-full">
227 <thead className="bg-[var(--brand)] text-surface">
228 <tr>
229 <th className="px-4 py-3 text-left text-xs font-semibold text-muted uppercase tracking-wider">
230 Sale ID
231 </th>
232 <th className="px-4 py-3 text-left text-xs font-semibold text-muted uppercase tracking-wider">
233 Date
234 </th>
235 <th className="px-4 py-3 text-left text-xs font-semibold text-muted uppercase tracking-wider">
236 Product
237 </th>
238 <th className="px-4 py-3 text-right text-xs font-semibold text-muted uppercase tracking-wider">
239 Qty
240 </th>
241 <th className="px-4 py-3 text-right text-xs font-semibold text-muted uppercase tracking-wider">
242 Amount
243 </th>
244 <th className="px-4 py-3 text-left text-xs font-semibold text-muted uppercase tracking-wider">
245 Teller
246 </th>
247 <th className="px-4 py-3 text-left text-xs font-semibold text-muted uppercase tracking-wider">
248 Payment
249 </th>
250 </tr>
251 </thead>
252 <tbody className="divide-y divide-border">
253 {data.map((row, idx) => (
254 <tr key={idx} className="hover:bg-surface-2 transition">
255 <td className="px-4 py-3 text-sm text-text">
256 {row.f101}
257 </td>
258 <td className="px-4 py-3 text-sm text-muted">
259 {formatDate(row.f102)}
260 </td>
261 <td className="px-4 py-3 text-sm text-text">
262 <div className="font-medium">{row.f201 || 'Unknown Product'}</div>
263 <div className="text-xs text-muted">PID: {row.f200}</div>
264 </td>
265 <td className="px-4 py-3 text-sm text-text text-right">
266 {row.f202}
267 </td>
268 <td className="px-4 py-3 text-sm font-medium text-text text-right">
269 {formatCurrency(row.f203)}
270 </td>
271 <td className="px-4 py-3 text-sm text-muted">
272 {row.f104 || '-'}
273 </td>
274 <td className="px-4 py-3 text-sm text-muted">
275 {row.f301 || '-'}
276 </td>
277 </tr>
278 ))}
279 </tbody>
280 <tfoot className="bg-surface-2 border-t-2 border-border">
281 <tr>
282 <td colSpan={3} className="px-4 py-3 text-sm font-bold text-text">
283 Totals
284 </td>
285 <td className="px-4 py-3 text-sm font-bold text-text text-right">
286 {totalQty}
287 </td>
288 <td className="px-4 py-3 text-sm font-bold text-brand text-right">
289 {formatCurrency(totalSales)}
290 </td>
291 <td colSpan={2}></td>
292 </tr>
293 </tfoot>
294 </table>
295 </div>
296 </div>
297 )}
298
299 {/* Help Text */}
300 <div className="mt-6 bg-brand-2 border border-brand rounded-lg p-4">
301 <div className="flex items-start gap-2">
302 <span className="text-brand text-xl">ℹ️</span>
303 <div className="text-sm text-text">
304 <div className="font-semibold mb-1">About This Report</div>
305 <div>
306 This report shows all sale items for the selected date range at your store.
307 Each row represents one product line from a sale. To see sales grouped by receipt,
308 use the Sale Header Review report.
309 </div>
310 </div>
311 </div>
312 </div>
313 </div>
314 </div>
315 );
316}