3import { useState } from "react";
4import { Icon } from '@/contexts/IconContext';
5import { useStore } from "@/contexts/StoreContext";
8 f101: number; // Sale ID
10 f107: number; // Total sale
11 f104: string; // Teller name
12 f200: number; // Product ID
13 f201: string; // Product name
15 f203: number; // Line price
16 f301?: string; // Paid by
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);
25 // Date filters - default to today
26 const [fromDate, setFromDate] = useState(() => {
28 d.setHours(0, 0, 0, 0);
29 return d.toISOString().split("T")[0];
31 const [toDate, setToDate] = useState(() => {
33 d.setDate(d.getDate() + 1);
34 return d.toISOString().split("T")[0];
37 const loadReport = async () => {
38 if (!session?.store) {
39 setError("No store session found");
48 // Call API endpoint that uses ELINK
49 const response = await fetch(`/api/v1/elink/sales-report?fromDate=${fromDate}&toDate=${toDate}`, {
51 credentials: 'include',
55 const errorData = await response.json();
56 throw new Error(errorData.error || `HTTP ${response.status}`);
59 const result = await response.json();
61 if (result.success && result.data?.APPT && Array.isArray(result.data.APPT)) {
62 setData(result.data.APPT);
67 console.error("Error loading sales report:", err);
68 setError(err.message || "Failed to load sales report");
75 const formatCurrency = (value: number | undefined) => {
76 if (value === undefined || value === null) return "$0.00";
77 return new Intl.NumberFormat("en-US", {
80 minimumFractionDigits: 2,
84 const formatDate = (dateStr: string | undefined) => {
85 if (!dateStr) return "";
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(
94 parseInt(month) - 1, // JavaScript months are 0-indexed
100 return date.toLocaleString('en-US', {
109 // Fallback to standard date parsing
110 const date = new Date(dateStr);
111 return date.toLocaleDateString();
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;
123 <div className="min-h-screen p-4" style={{ background: 'var(--bg)' }}>
124 <div className="max-w-[1400px] mx-auto">
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">
129 <h1 className="text-2xl font-bold text-text">Sales Report</h1>
131 <p className="text-sm text-muted mt-1">
132 {session.store.type === 'store' ? '🏪' : '🏢'} {session.store.name}
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>
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">
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"
155 <div className="flex-1">
156 <label className="block text-sm font-medium text-text mb-1">
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"
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"
171 {loading ? "Loading..." : "Load Report"}
176 {/* Error Message */}
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>
182 <div className="font-semibold">Error</div>
183 <div className="text-sm">{error}</div>
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>
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>
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>
207 {/* Results Table */}
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>
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>
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">
229 <th className="px-4 py-3 text-left text-xs font-semibold text-muted uppercase tracking-wider">
232 <th className="px-4 py-3 text-left text-xs font-semibold text-muted uppercase tracking-wider">
235 <th className="px-4 py-3 text-left text-xs font-semibold text-muted uppercase tracking-wider">
238 <th className="px-4 py-3 text-right text-xs font-semibold text-muted uppercase tracking-wider">
241 <th className="px-4 py-3 text-right text-xs font-semibold text-muted uppercase tracking-wider">
244 <th className="px-4 py-3 text-left text-xs font-semibold text-muted uppercase tracking-wider">
247 <th className="px-4 py-3 text-left text-xs font-semibold text-muted uppercase tracking-wider">
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">
258 <td className="px-4 py-3 text-sm text-muted">
259 {formatDate(row.f102)}
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>
265 <td className="px-4 py-3 text-sm text-text text-right">
268 <td className="px-4 py-3 text-sm font-medium text-text text-right">
269 {formatCurrency(row.f203)}
271 <td className="px-4 py-3 text-sm text-muted">
274 <td className="px-4 py-3 text-sm text-muted">
280 <tfoot className="bg-surface-2 border-t-2 border-border">
282 <td colSpan={3} className="px-4 py-3 text-sm font-bold text-text">
285 <td className="px-4 py-3 text-sm font-bold text-text text-right">
288 <td className="px-4 py-3 text-sm font-bold text-brand text-right">
289 {formatCurrency(totalSales)}
291 <td colSpan={2}></td>
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>
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.