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 TellerSales {
12 tid: number;
13 name: string;
14 sales: number;
15 lastcost: number;
16 waccost: number;
17 units: number;
18 numsales: number;
19 avesale: number;
20 itemspersale: number;
21 gp: number;
22 percenttotal: number;
23}
24
25export default function SalesByTellerPage() {
26 const [tellerData, setTellerData] = useState<TellerSales[]>([]);
27 const [loading, setLoading] = useState(false);
28 const [totalPeriodRev, setTotalPeriodRev] = useState(0);
29 const [sortColumn, setSortColumn] = useState<keyof TellerSales>('sales');
30 const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('desc');
31
32 const loadReport = async () => {
33 setLoading(true);
34 try {
35 const response = await apiClient.request('/v1/sales/stats?type=staff');
36
37 if (response.success && (response.data as any)?.DATS) {
38 let totalRev = 0;
39 const results: TellerSales[] = (response.data as any).DATS.map((item: any) => {
40 const sales = Number(item.f120 || 0); // Revenue
41 const units = Number(item.f107 || 0); // Units
42 const numsales = Number(item.f121 || 0); // Transaction count
43
44 totalRev += sales;
45
46 const avesale = numsales > 0 ? sales / numsales : 0;
47 const itemspersale = numsales > 0 ? units / numsales : 0;
48
49 return {
50 tid: Number(item.f100 || 0),
51 name: String(item.f101 || 'Unknown Staff'),
52 sales,
53 lastcost: 0, // Not available in this API
54 waccost: 0, // Not available in this API
55 units,
56 numsales,
57 avesale,
58 itemspersale,
59 gp: 0, // GP not available without cost data
60 percenttotal: 0
61 };
62 });
63
64 // Calculate percentages
65 results.forEach(item => {
66 item.percenttotal = totalRev > 0 ? (item.sales / totalRev) * 100 : 0;
67 });
68
69 setTotalPeriodRev(totalRev);
70 setTellerData(results);
71 } else {
72 setTellerData([]);
73 setTotalPeriodRev(0);
74 }
75 } catch (error) {
76 console.error('Error loading report:', error);
77 setTellerData([]);
78 } finally {
79 setLoading(false);
80 }
81 };
82
83 const formatCurrency = (value: number): string => {
84 return new Intl.NumberFormat('en-NZ', {
85 style: 'currency',
86 currency: 'NZD',
87 minimumFractionDigits: 2
88 }).format(value);
89 };
90
91 const handleSort = (column: keyof TellerSales) => {
92 if (sortColumn === column) {
93 setSortDirection(sortDirection === 'asc' ? 'desc' : 'asc');
94 } else {
95 setSortColumn(column);
96 setSortDirection('asc');
97 }
98 };
99
100 const sortedData = [...tellerData].sort((a, b) => {
101 const aVal = a[sortColumn];
102 const bVal = b[sortColumn];
103
104 if (aVal === undefined || bVal === undefined) return 0;
105
106 if (typeof aVal === 'number' && typeof bVal === 'number') {
107 return sortDirection === 'asc' ? aVal - bVal : bVal - aVal;
108 }
109
110 const aStr = String(aVal);
111 const bStr = String(bVal);
112 return sortDirection === 'asc'
113 ? aStr.localeCompare(bStr)
114 : bStr.localeCompare(aStr);
115 });
116
117 const SortIcon = ({ column }: { column: keyof TellerSales }) => {
118 if (sortColumn !== column) return <span className="text-muted/50">⇅</span>;
119 return <span>{sortDirection === 'asc' ? '↑' : '↓'}</span>;
120 };
121
122 const GPBar = ({ value }: { value: number }) => {
123 const percentage = Math.max(0, Math.min(100, Math.round(value)));
124
125 if (percentage === 0) return null;
126
127 // Color coding: red < 40%, blue 40-60%, green > 60%
128 let colorClass = 'bg-brand';
129 if (percentage < 40) colorClass = 'bg-red-500';
130 if (percentage > 60) colorClass = 'bg-green-500';
131
132 return (
133 <div className="flex items-center gap-2">
134 <div className="flex-1 bg-surface-2 rounded-full h-4 overflow-hidden">
135 <div
136 className={`${colorClass} h-full rounded-full transition-all duration-300`}
137 style={{ width: `${percentage}%` }}
138 ></div>
139 </div>
140 <span className="text-xs font-medium w-12 text-right">{percentage}%</span>
141 </div>
142 );
143 };
144
145 const PercentBar = ({ value }: { value: number }) => {
146 const percentage = Math.max(0, Math.min(100, Math.round(value)));
147
148 if (percentage === 0) return null;
149
150 return (
151 <div className="flex items-center gap-2">
152 <div className="flex-1 bg-surface-2 rounded-full h-4 overflow-hidden">
153 <div
154 className="bg-green-500 h-full rounded-full transition-all duration-300"
155 style={{ width: `${percentage}%` }}
156 ></div>
157 </div>
158 <span className="text-xs font-medium w-12 text-right">{percentage.toFixed(0)}%</span>
159 </div>
160 );
161 };
162
163 return (
164 <div className="p-6">
165 {/* Header */}
166 <div className="mb-6">
167 <h1 className="text-3xl font-bold mb-2">Sales by Teller 👤</h1>
168 <p className="text-sm text-muted mb-2">
169 Staff sales performance summary
170 </p>
171 <div className="text-sm text-muted">
172 <a href="/pages/reports/sales-reports" className="text-brand hover:underline">Sales</a>
173 {' > '}
174 <span>Sales by Teller</span>
175 </div>
176 </div>
177
178 {/* Filters */}
179 <div className="bg-surface rounded-lg shadow p-6 mb-6">
180 <div className="mb-4 p-3 bg-info/10 border-l-4 border-info">
181 <p className="text-sm text-info">
182 <strong>Note:</strong> This report shows current staff sales data. Date and location filtering is not available in this API.
183 </p>
184 </div>
185 <form
186 onSubmit={(e) => { e.preventDefault(); loadReport(); }}
187 className="flex items-end gap-4"
188 >
189 <div>
190 <button
191 type="submit"
192 disabled={loading}
193 className="bg-brand text-white px-6 py-2 rounded hover:bg-brand2 disabled:bg-muted/50"
194 >
195 {loading ? 'Loading...' : 'Load Staff Sales'}
196 </button>
197 </div>
198 </form>
199 </div>
200
201 {/* Results Table */}
202 <div className="bg-surface rounded-lg shadow overflow-hidden">
203 {loading ? (
204 <div className="p-8 text-center">
205 <div className="inline-block animate-spin rounded-full h-8 w-8 border-b-2 border-info"></div>
206 <p className="mt-2 text-muted">Loading sales data...</p>
207 </div>
208 ) : (
209 <>
210 <div className="overflow-x-auto">
211 <table className="w-full text-sm">
212 <thead className="bg-[var(--brand)] text-surface sticky top-0">
213 <tr>
214 <th
215 className="px-3 py-2 text-left font-semibold cursor-pointer hover:bg-surface-2/80"
216 onClick={() => handleSort('tid')}
217 >
218 Tid# <SortIcon column="tid" />
219 </th>
220 <th
221 className="px-3 py-2 text-left font-semibold cursor-pointer hover:bg-surface-2/80"
222 onClick={() => handleSort('name')}
223 >
224 Name <SortIcon column="name" />
225 </th>
226 <th
227 className="px-3 py-2 text-right font-semibold cursor-pointer hover:bg-surface-2/80"
228 onClick={() => handleSort('numsales')}
229 >
230 #Sales <SortIcon column="numsales" />
231 </th>
232 <th className="px-3 py-2 text-left font-semibold">
233 % of Sales
234 </th>
235 <th
236 className="px-3 py-2 text-right font-semibold cursor-pointer hover:bg-surface-2/80"
237 onClick={() => handleSort('units')}
238 >
239 Items Sold <SortIcon column="units" />
240 </th>
241 <th
242 className="px-3 py-2 text-right font-semibold cursor-pointer hover:bg-surface-2/80"
243 onClick={() => handleSort('sales')}
244 >
245 Sales <SortIcon column="sales" />
246 </th>
247 <th
248 className="px-3 py-2 text-right font-semibold cursor-pointer hover:bg-surface-2/80"
249 onClick={() => handleSort('avesale')}
250 >
251 Avg. Sale$ <SortIcon column="avesale" />
252 </th>
253 <th
254 className="px-3 py-2 text-right font-semibold cursor-pointer hover:bg-surface-2/80"
255 onClick={() => handleSort('itemspersale')}
256 >
257 Avg #Items/Sale <SortIcon column="itemspersale" />
258 </th>
259 <th
260 className="px-3 py-2 text-right font-semibold cursor-pointer hover:bg-surface-2/80"
261 onClick={() => handleSort('lastcost')}
262 >
263 Last Cost <SortIcon column="lastcost" />
264 </th>
265 <th
266 className="px-3 py-2 text-left font-semibold cursor-pointer hover:bg-surface-2/80"
267 onClick={() => handleSort('gp')}
268 >
269 GP% <SortIcon column="gp" />
270 </th>
271 </tr>
272 </thead>
273 <tbody className="divide-y divide-border">
274 {sortedData.length === 0 ? (
275 <tr>
276 <td colSpan={10} className="px-4 py-8 text-center text-muted">
277 No sales data found for the selected period
278 </td>
279 </tr>
280 ) : (
281 sortedData.map((teller, idx) => (
282 <tr key={idx} className="hover:bg-surface-2">
283 <td className="px-3 py-2">
284 {teller.tid || '-'}
285 </td>
286 <td className="px-3 py-2 font-medium">
287 {teller.name || 'Unknown'}
288 </td>
289 <td className="px-3 py-2 text-right">
290 {teller.numsales}
291 </td>
292 <td className="px-3 py-2" style={{ minWidth: '150px' }}>
293 <PercentBar value={teller.percenttotal} />
294 </td>
295 <td className="px-3 py-2 text-right">
296 {teller.units}
297 </td>
298 <td className="px-3 py-2 text-right font-semibold">
299 {formatCurrency(teller.sales)}
300 </td>
301 <td className="px-3 py-2 text-right">
302 {formatCurrency(teller.avesale)}
303 </td>
304 <td className="px-3 py-2 text-right">
305 {teller.itemspersale.toFixed(1)}
306 </td>
307 <td className="px-3 py-2 text-right">
308 {formatCurrency(teller.lastcost)}
309 </td>
310 <td className="px-3 py-2" style={{ minWidth: '150px' }}>
311 <GPBar value={teller.gp} />
312 </td>
313 </tr>
314 ))
315 )}
316 </tbody>
317 </table>
318 </div>
319
320 {tellerData.length > 0 && (
321 <div className="px-4 py-3 bg-surface-2 border-t">
322 <div className="text-sm text-muted">
323 <strong>Total Period Revenue:</strong> {formatCurrency(totalPeriodRev)}
324 {' | '}
325 Showing {tellerData.length} teller{tellerData.length !== 1 ? 's' : ''}
326 </div>
327 </div>
328 )}
329 </>
330 )}
331 </div>
332 </div>
333 );
334}