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 Department {
13 f100: number;
14 f101: string;
15}
16
17interface EditedSale {
18 f101: string; // Sale ID
19 f102: string; // Date
20 f103?: string; // Teller ID
21 f104?: string; // Teller Name
22 f105?: string; // Location ID
23 f106?: string; // Store Name
24 f107?: number; // Total Sale
25 f108?: string; // Customer ID
26 f200?: number; // Product ID
27 f201?: string; // Product Name
28 f202?: number; // Quantity
29 f203?: number; // Line Price
30 f206?: number; // Price Cause Code
31 f208?: number; // Line Cost
32 f209?: number; // Gross Profit
33 f301?: string; // Payment Method
34 PROD?: any[]; // Product details
35 CUST?: any[]; // Customer details
36}
37
38export default function EditedSalesPage() {
39 const [locations, setLocations] = useState<Location[]>([]);
40 const [departments, setDepartments] = useState<Department[]>([]);
41 const [fromDate, setFromDate] = useState(() => {
42 const d = new Date();
43 d.setDate(1); // First of month
44 return d.toISOString().split('T')[0];
45 });
46 const [toDate, setToDate] = useState(() => {
47 const d = new Date();
48 d.setDate(d.getDate() + 1); // Tomorrow
49 return d.toISOString().split('T')[0];
50 });
51 const [selectedLocation, setSelectedLocation] = useState('0');
52 const [selectedDepartment, setSelectedDepartment] = useState('0');
53 const [maxRows, setMaxRows] = useState('200');
54 const [sales, setSales] = useState<EditedSale[]>([]);
55 const [loading, setLoading] = useState(false);
56 const [sortColumn, setSortColumn] = useState<keyof EditedSale>('f102');
57 const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('desc');
58
59 useEffect(() => {
60 loadLocations();
61 loadDepartments();
62 loadEditedSales();
63 }, []);
64
65 const loadLocations = async () => {
66 try {
67 const apiKey = sessionStorage.getItem("fieldpine_apikey");
68 if (!apiKey) return;
69
70 const response = await fieldpineApi({
71 endpoint: '/buck?3=retailmax.elink.locations&100=1',
72 apiKey
73 });
74
75 if (response.DATS) {
76 setLocations(response.DATS);
77 }
78 } catch (error) {
79 console.error('Error loading locations:', error);
80 }
81 };
82
83 const loadDepartments = async () => {
84 try {
85 const apiKey = sessionStorage.getItem("fieldpine_apikey");
86 if (!apiKey) return;
87
88 const response = await fieldpineApi({
89 endpoint: '/buck?3=retailmax.elink.departments',
90 apiKey
91 });
92
93 if (response.DATS) {
94 setDepartments(response.DATS);
95 }
96 } catch (error) {
97 console.error('Error loading departments:', error);
98 }
99 };
100
101 const loadEditedSales = async () => {
102 setLoading(true);
103 try {
104 const apiKey = sessionStorage.getItem("fieldpine_apikey");
105 if (!apiKey) {
106 console.error("No API key found");
107 setLoading(false);
108 return;
109 }
110
111 let predicates = '';
112
113 // Date range
114 const fromFormatted = formatDateForAPI(fromDate);
115 const toFormatted = formatDateForAPI(toDate);
116 predicates += `&9=f102,4,${fromFormatted}`;
117 predicates += `&9=f102,1,${toFormatted}`;
118
119 // Filter for edited sales (f117 flag with value 65536)
120 predicates += '&9=f117,0,65536';
121
122 // Add location filter
123 if (selectedLocation !== '0' && selectedLocation !== '') {
124 predicates += `&9=f105,0,${selectedLocation}`;
125 }
126
127 // Add department filter
128 if (selectedDepartment !== '0' && selectedDepartment !== '') {
129 predicates += `&9=f801,0,${selectedDepartment}`;
130 }
131
132 // Full product and customer packets
133 predicates += '&103=1&104=1';
134
135 // Add max rows limit
136 predicates += `&8=${maxRows}`;
137
138 const response = await fieldpineApi({
139 endpoint: `/buck?3=retailmax.elink.saleflat.list${predicates}`,
140 apiKey
141 });
142
143 if (response.APPT) {
144 setSales(response.APPT);
145 } else {
146 setSales([]);
147 }
148 } catch (error) {
149 console.error('Error loading edited sales:', error);
150 setSales([]);
151 } finally {
152 setLoading(false);
153 }
154 };
155
156 const formatDateForAPI = (dateStr: string): string => {
157 const date = new Date(dateStr);
158 const day = date.getDate();
159 const month = date.getMonth() + 1;
160 const year = date.getFullYear();
161 return `${day}-${month}-${year}`;
162 };
163
164 const handleSearch = () => {
165 loadEditedSales();
166 };
167
168 const getPriceCause = (code?: number): string => {
169 if (!code || code === 0) return '';
170 switch (code) {
171 case 1: return 'SpecOffer';
172 case 2: return 'Reward';
173 case 3: return 'Combo';
174 case 4: return 'QtyDisc';
175 case 5: return 'StoreDisc';
176 case 6: return 'DiscManual';
177 case 7: return 'PriceMap';
178 case 8: return 'CustDisc';
179 case 9: return 'TellerAction';
180 case 11: return 'DiscVoucher';
181 default: return String(code);
182 }
183 };
184
185 const formatDate = (dateStr?: string): string => {
186 if (!dateStr) return '';
187 const date = new Date(dateStr);
188 return date.toLocaleDateString('en-NZ', {
189 year: 'numeric',
190 month: 'short',
191 day: 'numeric'
192 });
193 };
194
195 const formatCurrency = (value?: number): string => {
196 if (!value) return '$0.00';
197 return new Intl.NumberFormat('en-NZ', {
198 style: 'currency',
199 currency: 'NZD'
200 }).format(value);
201 };
202
203 const handleSort = (column: keyof EditedSale) => {
204 if (sortColumn === column) {
205 setSortDirection(sortDirection === 'asc' ? 'desc' : 'asc');
206 } else {
207 setSortColumn(column);
208 setSortDirection('asc');
209 }
210 };
211
212 const sortedSales = [...sales].sort((a, b) => {
213 const aVal = a[sortColumn];
214 const bVal = b[sortColumn];
215
216 if (aVal === undefined || bVal === undefined) return 0;
217
218 if (typeof aVal === 'number' && typeof bVal === 'number') {
219 return sortDirection === 'asc' ? aVal - bVal : bVal - aVal;
220 }
221
222 const aStr = String(aVal);
223 const bStr = String(bVal);
224 return sortDirection === 'asc'
225 ? aStr.localeCompare(bStr)
226 : bStr.localeCompare(aStr);
227 });
228
229 const SortIcon = ({ column }: { column: keyof EditedSale }) => {
230 if (sortColumn !== column) return <span className="text-muted/50">⇅</span>;
231 return <span>{sortDirection === 'asc' ? '↑' : '↓'}</span>;
232 };
233
234 const getCustomerName = (sale: EditedSale): string => {
235 if (sale.CUST && sale.CUST[0] && sale.CUST[0].f101) {
236 return sale.CUST[0].f101;
237 }
238 return '';
239 };
240
241 return (
242 <div className="p-6">
243 {/* Header */}
244 <div className="mb-6">
245 <h1 className="text-3xl font-bold mb-2">Edited Sales ✏️</h1>
246 <p className="text-sm text-muted mb-2">
247 Sales that were altered after completion. To see details of the change made, click the sale id# and read the "Support Sale Log" section.
248 </p>
249 <div className="text-sm text-muted">
250 <a href="/pages/reports/sales-reports" className="text-brand hover:underline">Sales</a>
251 {' > '}
252 <span>Edited Sales</span>
253 </div>
254 </div>
255
256 {/* Filters */}
257 <div className="bg-surface rounded-lg shadow p-6 mb-6">
258 <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 mb-4">
259 <div>
260 <label className="block text-sm font-medium text-text mb-2">
261 From Date (including):
262 </label>
263 <input
264 type="date"
265 value={fromDate}
266 onChange={(e) => setFromDate(e.target.value)}
267 className="w-full border rounded px-3 py-2"
268 />
269 </div>
270
271 <div>
272 <label className="block text-sm font-medium text-text mb-2">
273 To Date (excluding):
274 </label>
275 <input
276 type="date"
277 value={toDate}
278 onChange={(e) => setToDate(e.target.value)}
279 className="w-full border rounded px-3 py-2"
280 />
281 </div>
282
283 <div>
284 <label className="block text-sm font-medium text-text mb-2">
285 Store:
286 </label>
287 <select
288 value={selectedLocation}
289 onChange={(e) => setSelectedLocation(e.target.value)}
290 className="w-full border rounded px-3 py-2"
291 >
292 <option value="0">All Stores</option>
293 {locations.map(loc => (
294 <option key={loc.f100} value={loc.f100}>
295 {loc.f101}
296 </option>
297 ))}
298 </select>
299 </div>
300
301 <div>
302 <label className="block text-sm font-medium text-text mb-2">
303 Department:
304 </label>
305 <select
306 value={selectedDepartment}
307 onChange={(e) => setSelectedDepartment(e.target.value)}
308 className="w-full border rounded px-3 py-2"
309 >
310 <option value="0">--Select--</option>
311 {departments.map(dept => (
312 <option key={dept.f100} value={dept.f100}>
313 {dept.f101}
314 </option>
315 ))}
316 </select>
317 </div>
318
319 <div>
320 <label className="block text-sm font-medium text-text mb-2">
321 Max Rows:
322 </label>
323 <input
324 type="number"
325 value={maxRows}
326 onChange={(e) => setMaxRows(e.target.value)}
327 className="w-full border rounded px-3 py-2"
328 />
329 </div>
330
331 <div className="flex items-end">
332 <button
333 onClick={handleSearch}
334 disabled={loading}
335 className="w-full bg-brand text-white px-6 py-2 rounded hover:bg-brand2 disabled:bg-muted/50"
336 >
337 {loading ? 'Loading...' : 'Display'}
338 </button>
339 </div>
340 </div>
341 </div>
342
343 {/* Results Table */}
344 <div className="bg-surface rounded-lg shadow overflow-hidden">
345 {loading ? (
346 <div className="p-8 text-center">
347 <div className="inline-block animate-spin rounded-full h-8 w-8 border-b-2 border-info"></div>
348 <p className="mt-2 text-muted">Loading edited sales...</p>
349 </div>
350 ) : (
351 <>
352 <div className="overflow-x-auto max-h-[600px] overflow-y-auto">
353 <table className="w-full text-sm">
354 <thead className="bg-surface-2 sticky top-0">
355 <tr>
356 <th
357 className="px-4 py-3 text-left font-semibold cursor-pointer hover:bg-surface-2/80"
358 onClick={() => handleSort('f102')}
359 >
360 Date <SortIcon column="f102" />
361 </th>
362 <th
363 className="px-4 py-3 text-left font-semibold cursor-pointer hover:bg-surface-2/80"
364 onClick={() => handleSort('f106')}
365 >
366 Store <SortIcon column="f106" />
367 </th>
368 <th
369 className="px-4 py-3 text-left font-semibold cursor-pointer hover:bg-surface-2/80"
370 onClick={() => handleSort('f101')}
371 >
372 Sale# <SortIcon column="f101" />
373 </th>
374 <th
375 className="px-4 py-3 text-right font-semibold cursor-pointer hover:bg-surface-2/80"
376 onClick={() => handleSort('f107')}
377 >
378 Total Sale <SortIcon column="f107" />
379 </th>
380 <th
381 className="px-4 py-3 text-left font-semibold cursor-pointer hover:bg-surface-2/80"
382 onClick={() => handleSort('f104')}
383 >
384 Teller <SortIcon column="f104" />
385 </th>
386 <th className="px-4 py-3 text-left font-semibold">
387 Customer
388 </th>
389 <th
390 className="px-4 py-3 text-left font-semibold cursor-pointer hover:bg-surface-2/80"
391 onClick={() => handleSort('f201')}
392 >
393 Product <SortIcon column="f201" />
394 </th>
395 <th
396 className="px-4 py-3 text-right font-semibold cursor-pointer hover:bg-surface-2/80"
397 onClick={() => handleSort('f202')}
398 >
399 Qty <SortIcon column="f202" />
400 </th>
401 <th
402 className="px-4 py-3 text-right font-semibold cursor-pointer hover:bg-surface-2/80"
403 onClick={() => handleSort('f203')}
404 >
405 Line Price <SortIcon column="f203" />
406 </th>
407 <th
408 className="px-4 py-3 text-right font-semibold cursor-pointer hover:bg-surface-2/80"
409 onClick={() => handleSort('f209')}
410 >
411 GP <SortIcon column="f209" />
412 </th>
413 <th className="px-4 py-3 text-center font-semibold">
414 C
415 </th>
416 </tr>
417 </thead>
418 <tbody className="divide-y divide-border">
419 {sortedSales.length === 0 ? (
420 <tr>
421 <td colSpan={11} className="px-4 py-8 text-center text-muted">
422 No edited sales found
423 </td>
424 </tr>
425 ) : (
426 sortedSales.map((sale, idx) => (
427 <tr key={idx} className="hover:bg-surface-2">
428 <td className="px-4 py-3 whitespace-nowrap">
429 {formatDate(sale.f102)}
430 </td>
431 <td className="px-4 py-3">
432 {sale.f106 || '-'}
433 </td>
434 <td className="px-4 py-3">
435 <a
436 href={`/report/pos/sales/fieldpine/SingleSale.htm?sid=${sale.f101}`}
437 className="text-brand hover:underline"
438 target="_blank"
439 rel="noopener noreferrer"
440 >
441 {sale.f101}
442 </a>
443 </td>
444 <td className="px-4 py-3 text-right font-semibold">
445 {formatCurrency(sale.f107)}
446 </td>
447 <td className="px-4 py-3">
448 {sale.f104 || '-'}
449 </td>
450 <td className="px-4 py-3">
451 {getCustomerName(sale) || '-'}
452 </td>
453 <td className="px-4 py-3">
454 {sale.f200 ? (
455 <a
456 href={`/report/pos/stock/fieldpine/singlepid.htm?pid=${sale.f200}`}
457 className="text-brand hover:underline"
458 target="_blank"
459 rel="noopener noreferrer"
460 >
461 {sale.f201 || sale.f200}
462 </a>
463 ) : (
464 sale.f201 || '-'
465 )}
466 </td>
467 <td className="px-4 py-3 text-right">
468 {sale.f202 || 0}
469 </td>
470 <td className="px-4 py-3 text-right">
471 {formatCurrency(sale.f203)}
472 </td>
473 <td className="px-4 py-3 text-right">
474 {formatCurrency(sale.f209)}
475 </td>
476 <td className="px-4 py-3 text-center text-xs">
477 {getPriceCause(sale.f206)}
478 </td>
479 </tr>
480 ))
481 )}
482 </tbody>
483 </table>
484 </div>
485
486 {sales.length > 0 && (
487 <div className="px-4 py-3 bg-surface-2 border-t text-sm text-muted">
488 Showing {sales.length} edited sale{sales.length !== 1 ? 's' : ''}
489 </div>
490 )}
491 </>
492 )}
493 </div>
494 </div>
495 );
496}