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 React, { useState, useEffect } from 'react';
4import { apiClient } from '@/lib/client/apiClient';
5import { fieldpineApi } from "@/lib/client/fieldpineApi";
6
7interface PaymentRecord {
8 f101: string; // DateTime
9 f100: number; // Type
10 f900: string; // Type Description
11 f103: number; // Amount
12 f104: number; // Sale ID
13 f151: number; // Customer ID
14 f153: string; // Customer Name
15 f152: string; // Purchased item
16}
17
18const PAYMENT_TYPES = [
19 { id: 1, name: 'Cash' },
20 { id: 2, name: 'Cheque' },
21 { id: 3, name: 'EFTPOS' },
22 { id: 4, name: 'Verified Cheque' },
23 { id: 5, name: 'EFTPOS failure' },
24 { id: 6, name: 'Change' },
25 { id: 7, name: 'Rounding' },
26 { id: 8, name: 'Return Refund' },
27 { id: 9, name: 'Credit Card' },
28 { id: 10, name: 'Credit Card failure' },
29 { id: 11, name: 'Sale discount' },
30 { id: 20, name: 'Account' },
31 { id: 21, name: 'Voucher' },
32 { id: 22, name: 'Direct Debit' },
33 { id: 200110, name: 'Afterpay' },
34];
35
36export default function PaymentListPage() {
37 const [fromDate, setFromDate] = useState('');
38 const [toDate, setToDate] = useState('');
39 const [paymentType, setPaymentType] = useState('0');
40 const [maxRows, setMaxRows] = useState('100');
41 const [loading, setLoading] = useState(false);
42 const [loadingMessage, setLoadingMessage] = useState('');
43 const [payments, setPayments] = useState<PaymentRecord[]>([]);
44 const [sortConfig, setSortConfig] = useState<{ key: keyof PaymentRecord; direction: 'asc' | 'desc' } | null>(null);
45
46 // Set default dates (yesterday to today)
47 useEffect(() => {
48 const yesterday = new Date();
49 yesterday.setDate(yesterday.getDate() - 1);
50 setFromDate(yesterday.toISOString().split('T')[0]);
51
52 const today = new Date();
53 setToDate(today.toISOString().split('T')[0]);
54 }, []);
55
56 const formatDateForAPI = (date: string) => {
57 if (!date) return '';
58 const d = new Date(date + 'T00:00:00');
59 const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
60 return `${d.getDate()}${months[d.getMonth()]}${d.getFullYear()}`;
61 };
62
63 const formatDateTime = (dateStr: string) => {
64 if (!dateStr) return '';
65 try {
66 const date = new Date(dateStr);
67 return new Intl.DateTimeFormat('en-NZ', {
68 year: 'numeric',
69 month: 'short',
70 day: 'numeric',
71 hour: '2-digit',
72 minute: '2-digit',
73 second: '2-digit'
74 }).format(date);
75 } catch {
76 return dateStr;
77 }
78 };
79
80 const formatCurrency = (value: number) => {
81 return new Intl.NumberFormat('en-NZ', {
82 style: 'currency',
83 currency: 'NZD',
84 minimumFractionDigits: 2,
85 maximumFractionDigits: 2
86 }).format(value);
87 };
88
89 const loadPayments = async () => {
90 const apiKey = sessionStorage.getItem("fieldpine_apikey");
91 if (!apiKey) {
92 alert("Please log in first");
93 return;
94 }
95
96 if (!fromDate || !toDate) {
97 alert("Please select date range");
98 return;
99 }
100
101 setLoading(true);
102 setPayments([]);
103 setLoadingMessage('Loading...');
104
105 try {
106 await loadPaymentsBatch(apiKey, '1', 1);
107 } catch (error) {
108 console.error('Error loading payments:', error);
109 alert('Error loading payments. Please try again.');
110 } finally {
111 setLoading(false);
112 setLoadingMessage('');
113 }
114 };
115
116 const loadPaymentsBatch = async (apiKey: string, stats47: string, batchCount: number) => {
117 let predicates = `&47=${stats47}&7=150-153`;
118
119 // Add payment type filter
120 if (Number(paymentType) > 0) {
121 predicates += `&9=f100,0,${paymentType}`;
122 }
123
124 // Add date filters
125 const startDate = formatDateForAPI(fromDate);
126 const endDate = formatDateForAPI(toDate);
127 predicates += `&9=f101,4,${startDate}`;
128 predicates += `&9=f101,1,${endDate}`;
129
130 // Add max results
131 predicates += `&5=${maxRows}`;
132
133 const response = await fieldpineApi({
134 endpoint: `/buck?3=retailmax.elink.sale.paymentlist${predicates}`,
135 apiKey: apiKey
136 });
137
138 if (response?.data) {
139 const data = response.data;
140
141 // Check if there's more data to load (f47 indicates continuation)
142 if (data.f47 && batchCount < 50) { // Safety limit of 50 batches
143 setLoadingMessage(`Loading... (${batchCount})`);
144
145 // Process current batch
146 if (data.APP && Array.isArray(data.APP)) {
147 setPayments(prev => [...prev, ...data.APP]);
148 }
149
150 // Load next batch after delay
151 setTimeout(() => {
152 loadPaymentsBatch(apiKey, data.f47, batchCount + 1);
153 }, 3000);
154
155 return;
156 }
157
158 // Final batch
159 if (data.APP && Array.isArray(data.APP)) {
160 setPayments(prev => [...prev, ...data.APP]);
161 }
162 }
163 };
164
165 const handleSort = (key: keyof PaymentRecord) => {
166 let direction: 'asc' | 'desc' = 'asc';
167 if (sortConfig && sortConfig.key === key && sortConfig.direction === 'asc') {
168 direction = 'desc';
169 }
170 setSortConfig({ key, direction });
171 };
172
173 const getSortedPayments = () => {
174 if (!sortConfig) return payments;
175
176 return [...payments].sort((a, b) => {
177 const aVal = a[sortConfig.key];
178 const bVal = b[sortConfig.key];
179
180 if (aVal === undefined || aVal === null) return 1;
181 if (bVal === undefined || bVal === null) return -1;
182
183 if (typeof aVal === 'number' && typeof bVal === 'number') {
184 return sortConfig.direction === 'asc' ? aVal - bVal : bVal - aVal;
185 }
186
187 const aStr = String(aVal).toLowerCase();
188 const bStr = String(bVal).toLowerCase();
189
190 if (aStr < bStr) return sortConfig.direction === 'asc' ? -1 : 1;
191 if (aStr > bStr) return sortConfig.direction === 'asc' ? 1 : -1;
192 return 0;
193 });
194 };
195
196 const sortedPayments = getSortedPayments();
197
198 return (
199 <div className="p-6">
200 <h1 className="text-3xl font-bold mb-6">Payments List</h1>
201
202 {/* Filters */}
203 <div className="bg-surface p-4 rounded-lg shadow mb-6">
204 <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
205 <div>
206 <label className="block text-sm font-medium mb-1">
207 From (including) <span className="text-red-600">*</span>
208 </label>
209 <input
210 type="date"
211 value={fromDate}
212 onChange={(e) => setFromDate(e.target.value)}
213 className="w-full border rounded px-3 py-2"
214 />
215 </div>
216
217 <div>
218 <label className="block text-sm font-medium mb-1">
219 To (excluding) <span className="text-red-600">*</span>
220 </label>
221 <input
222 type="date"
223 value={toDate}
224 onChange={(e) => setToDate(e.target.value)}
225 className="w-full border rounded px-3 py-2"
226 />
227 </div>
228
229 <div>
230 <label className="block text-sm font-medium mb-1">Payment Type</label>
231 <select
232 value={paymentType}
233 onChange={(e) => setPaymentType(e.target.value)}
234 className="w-full border rounded px-3 py-2"
235 >
236 <option value="0">--Select--</option>
237 {PAYMENT_TYPES.map(pt => (
238 <option key={pt.id} value={pt.id}>{pt.name}</option>
239 ))}
240 </select>
241 </div>
242
243 <div>
244 <label className="block text-sm font-medium mb-1">Rows</label>
245 <div className="flex gap-2">
246 <input
247 type="number"
248 value={maxRows}
249 onChange={(e) => setMaxRows(e.target.value)}
250 className="w-20 border rounded px-3 py-2"
251 min="1"
252 max="10000"
253 />
254 <button
255 onClick={loadPayments}
256 disabled={loading}
257 className="bg-brand text-white px-6 py-2 rounded hover:bg-brand2 disabled:bg-muted/50 flex-1"
258 >
259 {loading ? 'Loading...' : 'Display'}
260 </button>
261 </div>
262 </div>
263 </div>
264 </div>
265
266 {/* Loading Message */}
267 {loadingMessage && (
268 <div className="mb-4 p-3 bg-info/10 border border-info/30 rounded flex items-center gap-2">
269 <div className="animate-spin rounded-full h-4 w-4 border-b-2 border-info"></div>
270 <span>{loadingMessage}</span>
271 </div>
272 )}
273
274 {/* Results Table */}
275 {sortedPayments.length > 0 ? (
276 <div className="bg-surface rounded-lg shadow overflow-hidden">
277 <div className="overflow-x-auto">
278 <table className="min-w-full divide-y divide-border">
279 <thead className="bg-surface-2">
280 <tr>
281 <th
282 onClick={() => handleSort('f101')}
283 className="px-6 py-3 text-left text-xs font-medium text-muted uppercase tracking-wider cursor-pointer hover:bg-surface-2/80"
284 >
285 DateTime {sortConfig?.key === 'f101' && (sortConfig.direction === 'asc' ? '↑' : '↓')}
286 </th>
287 <th
288 onClick={() => handleSort('f100')}
289 className="px-6 py-3 text-left text-xs font-medium text-muted uppercase tracking-wider cursor-pointer hover:bg-surface-2/80"
290 >
291 Type {sortConfig?.key === 'f100' && (sortConfig.direction === 'asc' ? '↑' : '↓')}
292 </th>
293 <th
294 onClick={() => handleSort('f900')}
295 className="px-6 py-3 text-left text-xs font-medium text-muted uppercase tracking-wider cursor-pointer hover:bg-surface-2/80"
296 >
297 Type Description {sortConfig?.key === 'f900' && (sortConfig.direction === 'asc' ? '↑' : '↓')}
298 </th>
299 <th
300 onClick={() => handleSort('f103')}
301 className="px-6 py-3 text-right text-xs font-medium text-muted uppercase tracking-wider cursor-pointer hover:bg-surface-2/80"
302 >
303 Amount {sortConfig?.key === 'f103' && (sortConfig.direction === 'asc' ? '↑' : '↓')}
304 </th>
305 <th
306 onClick={() => handleSort('f104')}
307 className="px-6 py-3 text-left text-xs font-medium text-muted uppercase tracking-wider cursor-pointer hover:bg-surface-2/80"
308 >
309 Sale {sortConfig?.key === 'f104' && (sortConfig.direction === 'asc' ? '↑' : '↓')}
310 </th>
311 <th
312 onClick={() => handleSort('f153')}
313 className="px-6 py-3 text-left text-xs font-medium text-muted uppercase tracking-wider cursor-pointer hover:bg-surface-2/80"
314 >
315 Customer {sortConfig?.key === 'f153' && (sortConfig.direction === 'asc' ? '↑' : '↓')}
316 </th>
317 <th
318 onClick={() => handleSort('f152')}
319 className="px-6 py-3 text-left text-xs font-medium text-muted uppercase tracking-wider cursor-pointer hover:bg-surface-2/80"
320 >
321 Purchased (main item) {sortConfig?.key === 'f152' && (sortConfig.direction === 'asc' ? '↑' : '↓')}
322 </th>
323 </tr>
324 </thead>
325 <tbody className="bg-surface divide-y divide-border">
326 {sortedPayments.map((payment, idx) => (
327 <tr key={idx} className="hover:bg-surface-2">
328 <td className="px-6 py-4 whitespace-nowrap text-sm text-text">
329 {formatDateTime(payment.f101)}
330 </td>
331 <td className="px-6 py-4 whitespace-nowrap text-sm text-text">
332 {payment.f100}
333 </td>
334 <td className="px-6 py-4 whitespace-nowrap text-sm text-text">
335 {payment.f900}
336 </td>
337 <td className="px-6 py-4 whitespace-nowrap text-sm text-text text-right font-mono">
338 {formatCurrency(payment.f103 || 0)}
339 </td>
340 <td className="px-6 py-4 whitespace-nowrap text-sm">
341 {payment.f104 ? (
342 <a
343 href={`/report/pos/sales/fieldpine/singlesale.htm?sid=${payment.f104}`}
344 target="_blank"
345 className="text-brand hover:underline"
346 >
347 {payment.f104}
348 </a>
349 ) : (
350 ''
351 )}
352 </td>
353 <td className="px-6 py-4 whitespace-nowrap text-sm">
354 {payment.f151 && payment.f153 ? (
355 <a
356 href={`/report/pos/customer/fieldpine/customer_single_overview.htm?cid=${payment.f151}`}
357 target="_blank"
358 className="text-brand hover:underline"
359 >
360 {payment.f153}
361 </a>
362 ) : (
363 payment.f153 || ''
364 )}
365 </td>
366 <td className="px-6 py-4 text-sm text-text">
367 {payment.f152}
368 </td>
369 </tr>
370 ))}
371 </tbody>
372 </table>
373 </div>
374
375 {/* Footer */}
376 <div className="bg-surface-2 px-6 py-3 border-t border-border">
377 <p className="text-sm text-text">
378 Showing {sortedPayments.length} payment{sortedPayments.length !== 1 ? 's' : ''}
379 </p>
380 </div>
381 </div>
382 ) : (
383 !loading && (
384 <div className="bg-surface rounded-lg shadow p-8 text-center text-muted">
385 No payments found. Select date range and click Display to load payments.
386 </div>
387 )
388 )}
389 </div>
390 );
391}