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, useEffect } from 'react';
4import { apiClient } from '@/lib/client/apiClient';
5
6interface Location {
7 f100: number;
8 f101: string;
9}
10
11interface StoreSalesData {
12 storeid: string;
13 storename: string;
14 sales: number;
15 salesInc: number;
16 lastcost: number;
17 waccost: number;
18 salecnt: number;
19 units: number;
20 avgrev: number;
21 gp: number;
22}
23
24export default function SalesByStorePage() {
25 const [fromDate, setFromDate] = useState(() => {
26 const d = new Date();
27 d.setDate(1); // First of month
28 return d.toISOString().split('T')[0];
29 });
30 const [toDate, setToDate] = useState(() => {
31 const d = new Date();
32 return d.toISOString().split('T')[0];
33 });
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');
38
39 const loadReport = async () => {
40 setLoading(true);
41 try {
42 // Get trading summary grouped by location
43 const params = new URLSearchParams({
44 dateFrom: fromDate,
45 dateTo: toDate,
46 groupBy: 'location'
47 });
48 const response = await apiClient.request(`/v1/reports/trading-summary?${params.toString()}`);
49
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
55
56 return {
57 storeid: String(item.f100 || 0),
58 storename: item.f101 || 'Unknown Store',
59 sales: sales,
60 salesInc: sales, // Same as sales (tax included)
61 lastcost: 0, // Not available in this API
62 waccost: 0, // Not available in this API
63 salecnt: salecnt,
64 units: units,
65 avgrev: salecnt !== 0 ? sales / salecnt : 0,
66 gp: 0 // GP calculation not available without cost data
67 };
68 }).filter((store: StoreSalesData) => store.sales > 0);
69
70 setStoreData(results);
71 }
72 } catch (error) {
73 console.error('Error loading report:', error);
74 } finally {
75 setLoading(false);
76 }
77 };
78
79 const formatCurrency = (value: number): string => {
80 return new Intl.NumberFormat('en-NZ', {
81 style: 'currency',
82 currency: 'NZD',
83 minimumFractionDigits: 2
84 }).format(value);
85 };
86
87 const handleSort = (column: keyof StoreSalesData) => {
88 if (sortColumn === column) {
89 setSortDirection(sortDirection === 'asc' ? 'desc' : 'asc');
90 } else {
91 setSortColumn(column);
92 setSortDirection('asc');
93 }
94 };
95
96 const sortedData = [...storeData].sort((a, b) => {
97 const aVal = a[sortColumn];
98 const bVal = b[sortColumn];
99
100 if (aVal === undefined || bVal === undefined) return 0;
101
102 if (typeof aVal === 'number' && typeof bVal === 'number') {
103 return sortDirection === 'asc' ? aVal - bVal : bVal - aVal;
104 }
105
106 const aStr = String(aVal);
107 const bStr = String(bVal);
108 return sortDirection === 'asc'
109 ? aStr.localeCompare(bStr)
110 : bStr.localeCompare(aStr);
111 });
112
113 const SortIcon = ({ column }: { column: keyof StoreSalesData }) => {
114 if (sortColumn !== column) return <span className="text-muted/50">⇅</span>;
115 return <span>{sortDirection === 'asc' ? '↑' : '↓'}</span>;
116 };
117
118 return (
119 <div className="p-6">
120 {/* Header */}
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
125 </p>
126 <div className="text-sm text-muted">
127 <a href="/pages/reports/sales-reports" className="text-brand hover:underline">Sales</a>
128 {' > '}
129 <span>Sales by Store</span>
130 </div>
131 </div>
132
133 {/* Filters */}
134 <div className="bg-surface rounded-lg shadow p-6 mb-6">
135 <form
136 onSubmit={(e) => { e.preventDefault(); loadReport(); }}
137 className="flex flex-wrap items-end gap-4"
138 >
139 <div>
140 <label className="block text-sm font-medium text-text mb-2">
141 From Date (including):
142 </label>
143 <input
144 type="date"
145 value={fromDate}
146 onChange={(e) => setFromDate(e.target.value)}
147 className="border rounded px-3 py-2"
148 />
149 </div>
150
151 <div>
152 <label className="block text-sm font-medium text-text mb-2">
153 To Date (excluding):
154 </label>
155 <input
156 type="date"
157 value={toDate}
158 onChange={(e) => setToDate(e.target.value)}
159 className="border rounded px-3 py-2"
160 />
161 </div>
162
163 <div>
164 <button
165 type="submit"
166 disabled={loading}
167 className="bg-brand text-white px-6 py-2 rounded hover:bg-brand2 disabled:bg-muted/50"
168 >
169 {loading ? 'Loading...' : 'Submit'}
170 </button>
171 </div>
172 </form>
173 </div>
174
175 {/* Results Table */}
176 <div className="bg-surface rounded-lg shadow overflow-hidden">
177 {loading ? (
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>
181 </div>
182 ) : (
183 <>
184 <div className="overflow-x-auto">
185 <table className="w-full text-sm">
186 <thead className="bg-surface-2 sticky top-0">
187 <tr>
188 <th
189 className="px-4 py-3 text-left font-semibold cursor-pointer hover:bg-surface-2/80"
190 onClick={() => handleSort('storename')}
191 >
192 Store <SortIcon column="storename" />
193 </th>
194 <th
195 className="px-4 py-3 text-right font-semibold cursor-pointer hover:bg-surface-2/80"
196 onClick={() => handleSort('sales')}
197 >
198 Sales <SortIcon column="sales" />
199 </th>
200 <th
201 className="px-4 py-3 text-right font-semibold cursor-pointer hover:bg-surface-2/80"
202 onClick={() => handleSort('salecnt')}
203 >
204 Num Sales <SortIcon column="salecnt" />
205 </th>
206 <th
207 className="px-4 py-3 text-right font-semibold cursor-pointer hover:bg-surface-2/80"
208 onClick={() => handleSort('units')}
209 >
210 Num Units <SortIcon column="units" />
211 </th>
212 <th
213 className="px-4 py-3 text-right font-semibold cursor-pointer hover:bg-surface-2/80"
214 onClick={() => handleSort('avgrev')}
215 >
216 Avg Sale <SortIcon column="avgrev" />
217 </th>
218 </tr>
219 </thead>
220 <tbody className="divide-y divide-border">
221 {sortedData.length === 0 ? (
222 <tr>
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.'}
225 </td>
226 </tr>
227 ) : (
228 sortedData.map((store, idx) => (
229 <tr key={idx} className="hover:bg-surface-2">
230 <td className="px-4 py-3 font-medium">
231 {store.storename}
232 </td>
233 <td className="px-4 py-3 text-right font-semibold text-green-600">
234 {formatCurrency(store.sales)}
235 </td>
236 <td className="px-4 py-3 text-right">
237 {store.salecnt}
238 </td>
239 <td className="px-4 py-3 text-right">
240 {store.units}
241 </td>
242 <td className="px-4 py-3 text-right">
243 {formatCurrency(store.avgrev)}
244 </td>
245 </tr>
246 ))
247 )}
248 </tbody>
249 </table>
250 </div>
251
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.
256 </p>
257 </div>
258 )}
259 </>
260 )}
261 </div>
262 </div>
263 );
264}