3import { useState, useEffect } from 'react';
4import { apiClient } from '@/lib/client/apiClient';
11interface TellerSales {
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');
32 const loadReport = async () => {
35 const response = await apiClient.request('/v1/sales/stats?type=staff');
37 if (response.success && (response.data as any)?.DATS) {
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
46 const avesale = numsales > 0 ? sales / numsales : 0;
47 const itemspersale = numsales > 0 ? units / numsales : 0;
50 tid: Number(item.f100 || 0),
51 name: String(item.f101 || 'Unknown Staff'),
53 lastcost: 0, // Not available in this API
54 waccost: 0, // Not available in this API
59 gp: 0, // GP not available without cost data
64 // Calculate percentages
65 results.forEach(item => {
66 item.percenttotal = totalRev > 0 ? (item.sales / totalRev) * 100 : 0;
69 setTotalPeriodRev(totalRev);
70 setTellerData(results);
76 console.error('Error loading report:', error);
83 const formatCurrency = (value: number): string => {
84 return new Intl.NumberFormat('en-NZ', {
87 minimumFractionDigits: 2
91 const handleSort = (column: keyof TellerSales) => {
92 if (sortColumn === column) {
93 setSortDirection(sortDirection === 'asc' ? 'desc' : 'asc');
95 setSortColumn(column);
96 setSortDirection('asc');
100 const sortedData = [...tellerData].sort((a, b) => {
101 const aVal = a[sortColumn];
102 const bVal = b[sortColumn];
104 if (aVal === undefined || bVal === undefined) return 0;
106 if (typeof aVal === 'number' && typeof bVal === 'number') {
107 return sortDirection === 'asc' ? aVal - bVal : bVal - aVal;
110 const aStr = String(aVal);
111 const bStr = String(bVal);
112 return sortDirection === 'asc'
113 ? aStr.localeCompare(bStr)
114 : bStr.localeCompare(aStr);
117 const SortIcon = ({ column }: { column: keyof TellerSales }) => {
118 if (sortColumn !== column) return <span className="text-muted/50">⇅</span>;
119 return <span>{sortDirection === 'asc' ? '↑' : '↓'}</span>;
122 const GPBar = ({ value }: { value: number }) => {
123 const percentage = Math.max(0, Math.min(100, Math.round(value)));
125 if (percentage === 0) return null;
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';
133 <div className="flex items-center gap-2">
134 <div className="flex-1 bg-surface-2 rounded-full h-4 overflow-hidden">
136 className={`${colorClass} h-full rounded-full transition-all duration-300`}
137 style={{ width: `${percentage}%` }}
140 <span className="text-xs font-medium w-12 text-right">{percentage}%</span>
145 const PercentBar = ({ value }: { value: number }) => {
146 const percentage = Math.max(0, Math.min(100, Math.round(value)));
148 if (percentage === 0) return null;
151 <div className="flex items-center gap-2">
152 <div className="flex-1 bg-surface-2 rounded-full h-4 overflow-hidden">
154 className="bg-green-500 h-full rounded-full transition-all duration-300"
155 style={{ width: `${percentage}%` }}
158 <span className="text-xs font-medium w-12 text-right">{percentage.toFixed(0)}%</span>
164 <div className="p-6">
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
171 <div className="text-sm text-muted">
172 <a href="/pages/reports/sales-reports" className="text-brand hover:underline">Sales</a>
174 <span>Sales by Teller</span>
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.
186 onSubmit={(e) => { e.preventDefault(); loadReport(); }}
187 className="flex items-end gap-4"
193 className="bg-brand text-white px-6 py-2 rounded hover:bg-brand2 disabled:bg-muted/50"
195 {loading ? 'Loading...' : 'Load Staff Sales'}
201 {/* Results Table */}
202 <div className="bg-surface rounded-lg shadow overflow-hidden">
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>
210 <div className="overflow-x-auto">
211 <table className="w-full text-sm">
212 <thead className="bg-[var(--brand)] text-surface sticky top-0">
215 className="px-3 py-2 text-left font-semibold cursor-pointer hover:bg-surface-2/80"
216 onClick={() => handleSort('tid')}
218 Tid# <SortIcon column="tid" />
221 className="px-3 py-2 text-left font-semibold cursor-pointer hover:bg-surface-2/80"
222 onClick={() => handleSort('name')}
224 Name <SortIcon column="name" />
227 className="px-3 py-2 text-right font-semibold cursor-pointer hover:bg-surface-2/80"
228 onClick={() => handleSort('numsales')}
230 #Sales <SortIcon column="numsales" />
232 <th className="px-3 py-2 text-left font-semibold">
236 className="px-3 py-2 text-right font-semibold cursor-pointer hover:bg-surface-2/80"
237 onClick={() => handleSort('units')}
239 Items Sold <SortIcon column="units" />
242 className="px-3 py-2 text-right font-semibold cursor-pointer hover:bg-surface-2/80"
243 onClick={() => handleSort('sales')}
245 Sales <SortIcon column="sales" />
248 className="px-3 py-2 text-right font-semibold cursor-pointer hover:bg-surface-2/80"
249 onClick={() => handleSort('avesale')}
251 Avg. Sale$ <SortIcon column="avesale" />
254 className="px-3 py-2 text-right font-semibold cursor-pointer hover:bg-surface-2/80"
255 onClick={() => handleSort('itemspersale')}
257 Avg #Items/Sale <SortIcon column="itemspersale" />
260 className="px-3 py-2 text-right font-semibold cursor-pointer hover:bg-surface-2/80"
261 onClick={() => handleSort('lastcost')}
263 Last Cost <SortIcon column="lastcost" />
266 className="px-3 py-2 text-left font-semibold cursor-pointer hover:bg-surface-2/80"
267 onClick={() => handleSort('gp')}
269 GP% <SortIcon column="gp" />
273 <tbody className="divide-y divide-border">
274 {sortedData.length === 0 ? (
276 <td colSpan={10} className="px-4 py-8 text-center text-muted">
277 No sales data found for the selected period
281 sortedData.map((teller, idx) => (
282 <tr key={idx} className="hover:bg-surface-2">
283 <td className="px-3 py-2">
286 <td className="px-3 py-2 font-medium">
287 {teller.name || 'Unknown'}
289 <td className="px-3 py-2 text-right">
292 <td className="px-3 py-2" style={{ minWidth: '150px' }}>
293 <PercentBar value={teller.percenttotal} />
295 <td className="px-3 py-2 text-right">
298 <td className="px-3 py-2 text-right font-semibold">
299 {formatCurrency(teller.sales)}
301 <td className="px-3 py-2 text-right">
302 {formatCurrency(teller.avesale)}
304 <td className="px-3 py-2 text-right">
305 {teller.itemspersale.toFixed(1)}
307 <td className="px-3 py-2 text-right">
308 {formatCurrency(teller.lastcost)}
310 <td className="px-3 py-2" style={{ minWidth: '150px' }}>
311 <GPBar value={teller.gp} />
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)}
325 Showing {tellerData.length} teller{tellerData.length !== 1 ? 's' : ''}