3import { useState, useEffect } from 'react';
4import { apiClient } from '@/lib/client/apiClient';
5import { fieldpineApi } from "@/lib/client/fieldpineApi";
13 f101: string; // Sale ID
15 f103?: string; // Teller ID
16 f104?: string; // Teller Name
17 f105?: string; // Location ID
18 f106?: string; // Store Name
19 f107?: number; // Total Sale
20 f108?: string; // Customer ID
21 f200?: number; // Product ID
22 f201?: string; // Product Name
23 f202?: number; // Quantity
24 f203?: number; // Line Price
25 f206?: number; // Price Cause Code
26 f301?: string; // Payment Method
29export default function LaybySalesPage() {
30 const [locations, setLocations] = useState<Location[]>([]);
31 const [selectedLocation, setSelectedLocation] = useState('0');
32 const [tellerSearch, setTellerSearch] = useState('');
33 const [customerSearch, setCustomerSearch] = useState('');
34 const [productSearch, setProductSearch] = useState('');
35 const [maxRows, setMaxRows] = useState('200');
36 const [sales, setSales] = useState<LaybySale[]>([]);
37 const [loading, setLoading] = useState(false);
38 const [sortColumn, setSortColumn] = useState<keyof LaybySale>('f102');
39 const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('desc');
46 const loadLocations = async () => {
48 const apiKey = sessionStorage.getItem("fieldpine_apikey");
51 const response = await fieldpineApi({
52 endpoint: '/buck?3=retailmax.elink.locations&100=1',
57 setLocations(response.DATS);
60 console.error('Error loading locations:', error);
64 const loadLaybySales = async () => {
67 const apiKey = sessionStorage.getItem("fieldpine_apikey");
69 console.error("No API key found");
76 // Filter for layby sales (f109 = 3)
77 predicates += '&9=f109,0,3';
79 // Add location filter
80 if (selectedLocation !== '0' && selectedLocation !== '') {
81 predicates += `&9=f105,0,${selectedLocation}`;
85 predicates += `&8=${maxRows}`;
87 const response = await fieldpineApi({
88 endpoint: `/buck?3=retailmax.elink.saleflat.list${predicates}`,
93 setSales(response.APPT);
98 console.error('Error loading layby sales:', error);
105 const handleSearch = () => {
109 const getPriceCause = (code?: number): string => {
110 if (!code || code === 0) return '';
112 case 1: return 'SpecOffer';
113 case 2: return 'Reward';
114 case 3: return 'Combo';
115 case 4: return 'QtyDisc';
116 case 5: return 'StoreDisc';
117 case 6: return 'DiscManual';
118 case 7: return 'PriceMap';
119 case 8: return 'CustDisc';
120 case 9: return 'TellerAction';
121 case 11: return 'DiscVoucher';
122 default: return String(code);
126 const formatDate = (dateStr?: string): string => {
127 if (!dateStr) return '';
128 const date = new Date(dateStr);
129 return date.toLocaleDateString('en-NZ', {
136 const formatCurrency = (value?: number): string => {
137 if (!value) return '$0.00';
138 return new Intl.NumberFormat('en-NZ', {
144 const handleSort = (column: keyof LaybySale) => {
145 if (sortColumn === column) {
146 setSortDirection(sortDirection === 'asc' ? 'desc' : 'asc');
148 setSortColumn(column);
149 setSortDirection('asc');
153 const sortedSales = [...sales].sort((a, b) => {
154 const aVal = a[sortColumn];
155 const bVal = b[sortColumn];
157 if (aVal === undefined || bVal === undefined) return 0;
159 if (typeof aVal === 'number' && typeof bVal === 'number') {
160 return sortDirection === 'asc' ? aVal - bVal : bVal - aVal;
163 const aStr = String(aVal);
164 const bStr = String(bVal);
165 return sortDirection === 'asc'
166 ? aStr.localeCompare(bStr)
167 : bStr.localeCompare(aStr);
170 const SortIcon = ({ column }: { column: keyof LaybySale }) => {
171 if (sortColumn !== column) return <span className="text-muted/50">⇅</span>;
172 return <span>{sortDirection === 'asc' ? '↑' : '↓'}</span>;
176 <div className="p-6">
178 <div className="mb-6">
179 <h1 className="text-3xl font-bold mb-2">Open Layby Sales Review 🛒</h1>
180 <div className="text-sm text-muted">
181 <a href="/pages/reports/sales-reports" className="text-brand hover:underline">Sales</a>
183 <span>Open Layby Sales</span>
188 <div className="bg-surface rounded-lg shadow p-6 mb-6">
189 <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
191 <label className="block text-sm font-medium text-text mb-2">
195 value={selectedLocation}
196 onChange={(e) => setSelectedLocation(e.target.value)}
197 className="w-full border rounded px-3 py-2"
199 <option value="0">All Stores</option>
200 {locations.map(loc => (
201 <option key={loc.f100} value={loc.f100}>
209 <label className="block text-sm font-medium text-text mb-2">
215 onChange={(e) => setTellerSearch(e.target.value)}
216 placeholder="Search teller..."
217 className="w-full border rounded px-3 py-2"
223 <label className="block text-sm font-medium text-text mb-2">
228 value={customerSearch}
229 onChange={(e) => setCustomerSearch(e.target.value)}
230 placeholder="Search customer..."
231 className="w-full border rounded px-3 py-2"
237 <label className="block text-sm font-medium text-text mb-2">
242 value={productSearch}
243 onChange={(e) => setProductSearch(e.target.value)}
244 placeholder="Search product..."
245 className="w-full border rounded px-3 py-2"
251 <div className="flex items-end gap-4">
253 <label className="block text-sm font-medium text-text mb-2">
259 onChange={(e) => setMaxRows(e.target.value)}
260 className="border rounded px-3 py-2 w-24"
264 onClick={handleSearch}
266 className="bg-brand text-white px-6 py-2 rounded hover:bg-brand2 disabled:bg-muted/50"
268 {loading ? 'Loading...' : 'Display'}
273 {/* Results Table */}
274 <div className="bg-surface rounded-lg shadow overflow-hidden">
276 <div className="p-8 text-center">
277 <div className="inline-block animate-spin rounded-full h-8 w-8 border-b-2 border-info"></div>
278 <p className="mt-2 text-muted">Loading layby sales...</p>
282 <div className="overflow-x-auto max-h-[600px] overflow-y-auto">
283 <table className="w-full text-sm">
284 <thead className="bg-surface-2 sticky top-0">
287 className="px-4 py-3 text-left font-semibold cursor-pointer hover:bg-surface-2/80"
288 onClick={() => handleSort('f102')}
290 Date <SortIcon column="f102" />
293 className="px-4 py-3 text-left font-semibold cursor-pointer hover:bg-surface-2/80"
294 onClick={() => handleSort('f106')}
296 Store <SortIcon column="f106" />
299 className="px-4 py-3 text-left font-semibold cursor-pointer hover:bg-surface-2/80"
300 onClick={() => handleSort('f101')}
302 Sale# <SortIcon column="f101" />
305 className="px-4 py-3 text-right font-semibold cursor-pointer hover:bg-surface-2/80"
306 onClick={() => handleSort('f107')}
308 Total Sale <SortIcon column="f107" />
311 className="px-4 py-3 text-left font-semibold cursor-pointer hover:bg-surface-2/80"
312 onClick={() => handleSort('f104')}
314 Teller <SortIcon column="f104" />
317 className="px-4 py-3 text-left font-semibold cursor-pointer hover:bg-surface-2/80"
318 onClick={() => handleSort('f201')}
320 Product <SortIcon column="f201" />
323 className="px-4 py-3 text-right font-semibold cursor-pointer hover:bg-surface-2/80"
324 onClick={() => handleSort('f202')}
326 Qty <SortIcon column="f202" />
329 className="px-4 py-3 text-right font-semibold cursor-pointer hover:bg-surface-2/80"
330 onClick={() => handleSort('f203')}
332 Line Price <SortIcon column="f203" />
334 <th className="px-4 py-3 text-center font-semibold">
338 className="px-4 py-3 text-left font-semibold cursor-pointer hover:bg-surface-2/80"
339 onClick={() => handleSort('f301')}
341 Paid By <SortIcon column="f301" />
345 <tbody className="divide-y divide-border">
346 {sortedSales.length === 0 ? (
348 <td colSpan={10} className="px-4 py-8 text-center text-muted">
353 sortedSales.map((sale, idx) => (
354 <tr key={idx} className="hover:bg-surface-2">
355 <td className="px-4 py-3 whitespace-nowrap">
356 {formatDate(sale.f102)}
358 <td className="px-4 py-3">
361 <td className="px-4 py-3">
363 href={`/report/pos/sales/fieldpine/SingleSale.htm?sid=${sale.f101}`}
364 className="text-brand hover:underline"
366 rel="noopener noreferrer"
371 <td className="px-4 py-3 text-right font-semibold">
372 {formatCurrency(sale.f107)}
374 <td className="px-4 py-3">
377 <td className="px-4 py-3">
380 href={`/report/pos/stock/fieldpine/singlepid.htm?pid=${sale.f200}`}
381 className="text-brand hover:underline"
383 rel="noopener noreferrer"
385 {sale.f201 || sale.f200}
391 <td className="px-4 py-3 text-right">
394 <td className="px-4 py-3 text-right">
395 {formatCurrency(sale.f203)}
397 <td className="px-4 py-3 text-center text-xs">
398 {getPriceCause(sale.f206)}
400 <td className="px-4 py-3">
410 {sales.length > 0 && (
411 <div className="px-4 py-3 bg-surface-2 border-t text-sm text-muted">
412 Showing {sales.length} layby sale{sales.length !== 1 ? 's' : ''}