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';
5import { fieldpineApi } from "@/lib/client/fieldpineApi";
6
7interface Location {
8 f100: number;
9 f101: string;
10}
11
12interface LaybySale {
13 f101: string; // Sale ID
14 f102: string; // Date
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
27}
28
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');
40
41 useEffect(() => {
42 loadLocations();
43 loadLaybySales();
44 }, []);
45
46 const loadLocations = async () => {
47 try {
48 const apiKey = sessionStorage.getItem("fieldpine_apikey");
49 if (!apiKey) return;
50
51 const response = await fieldpineApi({
52 endpoint: '/buck?3=retailmax.elink.locations&100=1',
53 apiKey
54 });
55
56 if (response.DATS) {
57 setLocations(response.DATS);
58 }
59 } catch (error) {
60 console.error('Error loading locations:', error);
61 }
62 };
63
64 const loadLaybySales = async () => {
65 setLoading(true);
66 try {
67 const apiKey = sessionStorage.getItem("fieldpine_apikey");
68 if (!apiKey) {
69 console.error("No API key found");
70 setLoading(false);
71 return;
72 }
73
74 let predicates = '';
75
76 // Filter for layby sales (f109 = 3)
77 predicates += '&9=f109,0,3';
78
79 // Add location filter
80 if (selectedLocation !== '0' && selectedLocation !== '') {
81 predicates += `&9=f105,0,${selectedLocation}`;
82 }
83
84 // Add max rows limit
85 predicates += `&8=${maxRows}`;
86
87 const response = await fieldpineApi({
88 endpoint: `/buck?3=retailmax.elink.saleflat.list${predicates}`,
89 apiKey
90 });
91
92 if (response.APPT) {
93 setSales(response.APPT);
94 } else {
95 setSales([]);
96 }
97 } catch (error) {
98 console.error('Error loading layby sales:', error);
99 setSales([]);
100 } finally {
101 setLoading(false);
102 }
103 };
104
105 const handleSearch = () => {
106 loadLaybySales();
107 };
108
109 const getPriceCause = (code?: number): string => {
110 if (!code || code === 0) return '';
111 switch (code) {
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);
123 }
124 };
125
126 const formatDate = (dateStr?: string): string => {
127 if (!dateStr) return '';
128 const date = new Date(dateStr);
129 return date.toLocaleDateString('en-NZ', {
130 year: 'numeric',
131 month: 'short',
132 day: 'numeric'
133 });
134 };
135
136 const formatCurrency = (value?: number): string => {
137 if (!value) return '$0.00';
138 return new Intl.NumberFormat('en-NZ', {
139 style: 'currency',
140 currency: 'NZD'
141 }).format(value);
142 };
143
144 const handleSort = (column: keyof LaybySale) => {
145 if (sortColumn === column) {
146 setSortDirection(sortDirection === 'asc' ? 'desc' : 'asc');
147 } else {
148 setSortColumn(column);
149 setSortDirection('asc');
150 }
151 };
152
153 const sortedSales = [...sales].sort((a, b) => {
154 const aVal = a[sortColumn];
155 const bVal = b[sortColumn];
156
157 if (aVal === undefined || bVal === undefined) return 0;
158
159 if (typeof aVal === 'number' && typeof bVal === 'number') {
160 return sortDirection === 'asc' ? aVal - bVal : bVal - aVal;
161 }
162
163 const aStr = String(aVal);
164 const bStr = String(bVal);
165 return sortDirection === 'asc'
166 ? aStr.localeCompare(bStr)
167 : bStr.localeCompare(aStr);
168 });
169
170 const SortIcon = ({ column }: { column: keyof LaybySale }) => {
171 if (sortColumn !== column) return <span className="text-muted/50">⇅</span>;
172 return <span>{sortDirection === 'asc' ? '↑' : '↓'}</span>;
173 };
174
175 return (
176 <div className="p-6">
177 {/* Header */}
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>
182 {' > '}
183 <span>Open Layby Sales</span>
184 </div>
185 </div>
186
187 {/* Filters */}
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">
190 <div>
191 <label className="block text-sm font-medium text-text mb-2">
192 Store:
193 </label>
194 <select
195 value={selectedLocation}
196 onChange={(e) => setSelectedLocation(e.target.value)}
197 className="w-full border rounded px-3 py-2"
198 >
199 <option value="0">All Stores</option>
200 {locations.map(loc => (
201 <option key={loc.f100} value={loc.f100}>
202 {loc.f101}
203 </option>
204 ))}
205 </select>
206 </div>
207
208 <div>
209 <label className="block text-sm font-medium text-text mb-2">
210 Teller:
211 </label>
212 <input
213 type="text"
214 value={tellerSearch}
215 onChange={(e) => setTellerSearch(e.target.value)}
216 placeholder="Search teller..."
217 className="w-full border rounded px-3 py-2"
218 disabled
219 />
220 </div>
221
222 <div>
223 <label className="block text-sm font-medium text-text mb-2">
224 Customer:
225 </label>
226 <input
227 type="text"
228 value={customerSearch}
229 onChange={(e) => setCustomerSearch(e.target.value)}
230 placeholder="Search customer..."
231 className="w-full border rounded px-3 py-2"
232 disabled
233 />
234 </div>
235
236 <div>
237 <label className="block text-sm font-medium text-text mb-2">
238 Product:
239 </label>
240 <input
241 type="text"
242 value={productSearch}
243 onChange={(e) => setProductSearch(e.target.value)}
244 placeholder="Search product..."
245 className="w-full border rounded px-3 py-2"
246 disabled
247 />
248 </div>
249 </div>
250
251 <div className="flex items-end gap-4">
252 <div>
253 <label className="block text-sm font-medium text-text mb-2">
254 Max Rows:
255 </label>
256 <input
257 type="number"
258 value={maxRows}
259 onChange={(e) => setMaxRows(e.target.value)}
260 className="border rounded px-3 py-2 w-24"
261 />
262 </div>
263 <button
264 onClick={handleSearch}
265 disabled={loading}
266 className="bg-brand text-white px-6 py-2 rounded hover:bg-brand2 disabled:bg-muted/50"
267 >
268 {loading ? 'Loading...' : 'Display'}
269 </button>
270 </div>
271 </div>
272
273 {/* Results Table */}
274 <div className="bg-surface rounded-lg shadow overflow-hidden">
275 {loading ? (
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>
279 </div>
280 ) : (
281 <>
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">
285 <tr>
286 <th
287 className="px-4 py-3 text-left font-semibold cursor-pointer hover:bg-surface-2/80"
288 onClick={() => handleSort('f102')}
289 >
290 Date <SortIcon column="f102" />
291 </th>
292 <th
293 className="px-4 py-3 text-left font-semibold cursor-pointer hover:bg-surface-2/80"
294 onClick={() => handleSort('f106')}
295 >
296 Store <SortIcon column="f106" />
297 </th>
298 <th
299 className="px-4 py-3 text-left font-semibold cursor-pointer hover:bg-surface-2/80"
300 onClick={() => handleSort('f101')}
301 >
302 Sale# <SortIcon column="f101" />
303 </th>
304 <th
305 className="px-4 py-3 text-right font-semibold cursor-pointer hover:bg-surface-2/80"
306 onClick={() => handleSort('f107')}
307 >
308 Total Sale <SortIcon column="f107" />
309 </th>
310 <th
311 className="px-4 py-3 text-left font-semibold cursor-pointer hover:bg-surface-2/80"
312 onClick={() => handleSort('f104')}
313 >
314 Teller <SortIcon column="f104" />
315 </th>
316 <th
317 className="px-4 py-3 text-left font-semibold cursor-pointer hover:bg-surface-2/80"
318 onClick={() => handleSort('f201')}
319 >
320 Product <SortIcon column="f201" />
321 </th>
322 <th
323 className="px-4 py-3 text-right font-semibold cursor-pointer hover:bg-surface-2/80"
324 onClick={() => handleSort('f202')}
325 >
326 Qty <SortIcon column="f202" />
327 </th>
328 <th
329 className="px-4 py-3 text-right font-semibold cursor-pointer hover:bg-surface-2/80"
330 onClick={() => handleSort('f203')}
331 >
332 Line Price <SortIcon column="f203" />
333 </th>
334 <th className="px-4 py-3 text-center font-semibold">
335 C
336 </th>
337 <th
338 className="px-4 py-3 text-left font-semibold cursor-pointer hover:bg-surface-2/80"
339 onClick={() => handleSort('f301')}
340 >
341 Paid By <SortIcon column="f301" />
342 </th>
343 </tr>
344 </thead>
345 <tbody className="divide-y divide-border">
346 {sortedSales.length === 0 ? (
347 <tr>
348 <td colSpan={10} className="px-4 py-8 text-center text-muted">
349 No layby sales found
350 </td>
351 </tr>
352 ) : (
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)}
357 </td>
358 <td className="px-4 py-3">
359 {sale.f106 || '-'}
360 </td>
361 <td className="px-4 py-3">
362 <a
363 href={`/report/pos/sales/fieldpine/SingleSale.htm?sid=${sale.f101}`}
364 className="text-brand hover:underline"
365 target="_blank"
366 rel="noopener noreferrer"
367 >
368 {sale.f101}
369 </a>
370 </td>
371 <td className="px-4 py-3 text-right font-semibold">
372 {formatCurrency(sale.f107)}
373 </td>
374 <td className="px-4 py-3">
375 {sale.f104 || '-'}
376 </td>
377 <td className="px-4 py-3">
378 {sale.f200 ? (
379 <a
380 href={`/report/pos/stock/fieldpine/singlepid.htm?pid=${sale.f200}`}
381 className="text-brand hover:underline"
382 target="_blank"
383 rel="noopener noreferrer"
384 >
385 {sale.f201 || sale.f200}
386 </a>
387 ) : (
388 sale.f201 || '-'
389 )}
390 </td>
391 <td className="px-4 py-3 text-right">
392 {sale.f202 || 0}
393 </td>
394 <td className="px-4 py-3 text-right">
395 {formatCurrency(sale.f203)}
396 </td>
397 <td className="px-4 py-3 text-center text-xs">
398 {getPriceCause(sale.f206)}
399 </td>
400 <td className="px-4 py-3">
401 {sale.f301 || '-'}
402 </td>
403 </tr>
404 ))
405 )}
406 </tbody>
407 </table>
408 </div>
409
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' : ''}
413 </div>
414 )}
415 </>
416 )}
417 </div>
418 </div>
419 );
420}