3import { useState, useEffect } from 'react';
4import { apiClient } from '@/lib/client/apiClient';
11interface StoreSalesData {
24export default function SalesByStorePage() {
25 const [fromDate, setFromDate] = useState(() => {
27 d.setDate(1); // First of month
28 return d.toISOString().split('T')[0];
30 const [toDate, setToDate] = useState(() => {
32 return d.toISOString().split('T')[0];
34 const [storeData, setStoreData] = useState<StoreSalesData[]>([]);
35 const [loading, setLoading] = useState(false);
36 const [sortColumn, setSortColumn] = useState<keyof StoreSalesData>('storename');
37 const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('asc');
39 const loadReport = async () => {
42 // Get trading summary grouped by location
43 const params = new URLSearchParams({
48 const response = await apiClient.request(`/v1/reports/trading-summary?${params.toString()}`);
50 if (response.success && (response.data as any)?.DATS) {
51 const results: StoreSalesData[] = (response.data as any).DATS.map((item: any) => {
52 const sales = Number(item.f120 || 0); // Revenue
53 const salecnt = Number(item.f121 || 0); // Transaction count
54 const units = Number(item.f107 || 0); // Units sold
57 storeid: String(item.f100 || 0),
58 storename: item.f101 || 'Unknown Store',
60 salesInc: sales, // Same as sales (tax included)
61 lastcost: 0, // Not available in this API
62 waccost: 0, // Not available in this API
65 avgrev: salecnt !== 0 ? sales / salecnt : 0,
66 gp: 0 // GP calculation not available without cost data
68 }).filter((store: StoreSalesData) => store.sales > 0);
70 setStoreData(results);
73 console.error('Error loading report:', error);
79 const formatCurrency = (value: number): string => {
80 return new Intl.NumberFormat('en-NZ', {
83 minimumFractionDigits: 2
87 const handleSort = (column: keyof StoreSalesData) => {
88 if (sortColumn === column) {
89 setSortDirection(sortDirection === 'asc' ? 'desc' : 'asc');
91 setSortColumn(column);
92 setSortDirection('asc');
96 const sortedData = [...storeData].sort((a, b) => {
97 const aVal = a[sortColumn];
98 const bVal = b[sortColumn];
100 if (aVal === undefined || bVal === undefined) return 0;
102 if (typeof aVal === 'number' && typeof bVal === 'number') {
103 return sortDirection === 'asc' ? aVal - bVal : bVal - aVal;
106 const aStr = String(aVal);
107 const bStr = String(bVal);
108 return sortDirection === 'asc'
109 ? aStr.localeCompare(bStr)
110 : bStr.localeCompare(aStr);
113 const SortIcon = ({ column }: { column: keyof StoreSalesData }) => {
114 if (sortColumn !== column) return <span className="text-muted/50">⇅</span>;
115 return <span>{sortDirection === 'asc' ? '↑' : '↓'}</span>;
119 <div className="p-6">
121 <div className="mb-6">
122 <h1 className="text-3xl font-bold mb-2">Sales by Store 🏪</h1>
123 <p className="text-sm text-muted mb-2">
124 Summary trading information by store location
126 <div className="text-sm text-muted">
127 <a href="/pages/reports/sales-reports" className="text-brand hover:underline">Sales</a>
129 <span>Sales by Store</span>
134 <div className="bg-surface rounded-lg shadow p-6 mb-6">
136 onSubmit={(e) => { e.preventDefault(); loadReport(); }}
137 className="flex flex-wrap items-end gap-4"
140 <label className="block text-sm font-medium text-text mb-2">
141 From Date (including):
146 onChange={(e) => setFromDate(e.target.value)}
147 className="border rounded px-3 py-2"
152 <label className="block text-sm font-medium text-text mb-2">
158 onChange={(e) => setToDate(e.target.value)}
159 className="border rounded px-3 py-2"
167 className="bg-brand text-white px-6 py-2 rounded hover:bg-brand2 disabled:bg-muted/50"
169 {loading ? 'Loading...' : 'Submit'}
175 {/* Results Table */}
176 <div className="bg-surface rounded-lg shadow overflow-hidden">
178 <div className="p-8 text-center">
179 <div className="inline-block animate-spin rounded-full h-8 w-8 border-b-2 border-info"></div>
180 <p className="mt-2 text-muted">Loading store sales data...</p>
184 <div className="overflow-x-auto">
185 <table className="w-full text-sm">
186 <thead className="bg-surface-2 sticky top-0">
189 className="px-4 py-3 text-left font-semibold cursor-pointer hover:bg-surface-2/80"
190 onClick={() => handleSort('storename')}
192 Store <SortIcon column="storename" />
195 className="px-4 py-3 text-right font-semibold cursor-pointer hover:bg-surface-2/80"
196 onClick={() => handleSort('sales')}
198 Sales <SortIcon column="sales" />
201 className="px-4 py-3 text-right font-semibold cursor-pointer hover:bg-surface-2/80"
202 onClick={() => handleSort('salecnt')}
204 Num Sales <SortIcon column="salecnt" />
207 className="px-4 py-3 text-right font-semibold cursor-pointer hover:bg-surface-2/80"
208 onClick={() => handleSort('units')}
210 Num Units <SortIcon column="units" />
213 className="px-4 py-3 text-right font-semibold cursor-pointer hover:bg-surface-2/80"
214 onClick={() => handleSort('avgrev')}
216 Avg Sale <SortIcon column="avgrev" />
220 <tbody className="divide-y divide-border">
221 {sortedData.length === 0 ? (
223 <td colSpan={5} className="px-4 py-8 text-center text-muted">
224 {loading ? 'Loading...' : 'No sales data found for the selected period. Click Submit to load data.'}
228 sortedData.map((store, idx) => (
229 <tr key={idx} className="hover:bg-surface-2">
230 <td className="px-4 py-3 font-medium">
233 <td className="px-4 py-3 text-right font-semibold text-green-600">
234 {formatCurrency(store.sales)}
236 <td className="px-4 py-3 text-right">
239 <td className="px-4 py-3 text-right">
242 <td className="px-4 py-3 text-right">
243 {formatCurrency(store.avgrev)}
252 {storeData.length > 0 && (
253 <div className="px-4 py-3 bg-surface-2 border-t">
254 <p className="text-sm text-muted">
255 Showing {storeData.length} store{storeData.length !== 1 ? 's' : ''} with sales during the selected period.