3import React, { useState, useEffect } from 'react';
4import { apiClient } from '@/lib/client/apiClient';
5import { fieldpineApi } from "@/lib/client/fieldpineApi";
7interface PaymentRecord {
8 f101: string; // DateTime
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
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' },
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);
46 // Set default dates (yesterday to today)
48 const yesterday = new Date();
49 yesterday.setDate(yesterday.getDate() - 1);
50 setFromDate(yesterday.toISOString().split('T')[0]);
52 const today = new Date();
53 setToDate(today.toISOString().split('T')[0]);
56 const formatDateForAPI = (date: string) => {
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()}`;
63 const formatDateTime = (dateStr: string) => {
64 if (!dateStr) return '';
66 const date = new Date(dateStr);
67 return new Intl.DateTimeFormat('en-NZ', {
80 const formatCurrency = (value: number) => {
81 return new Intl.NumberFormat('en-NZ', {
84 minimumFractionDigits: 2,
85 maximumFractionDigits: 2
89 const loadPayments = async () => {
90 const apiKey = sessionStorage.getItem("fieldpine_apikey");
92 alert("Please log in first");
96 if (!fromDate || !toDate) {
97 alert("Please select date range");
103 setLoadingMessage('Loading...');
106 await loadPaymentsBatch(apiKey, '1', 1);
108 console.error('Error loading payments:', error);
109 alert('Error loading payments. Please try again.');
112 setLoadingMessage('');
116 const loadPaymentsBatch = async (apiKey: string, stats47: string, batchCount: number) => {
117 let predicates = `&47=${stats47}&7=150-153`;
119 // Add payment type filter
120 if (Number(paymentType) > 0) {
121 predicates += `&9=f100,0,${paymentType}`;
125 const startDate = formatDateForAPI(fromDate);
126 const endDate = formatDateForAPI(toDate);
127 predicates += `&9=f101,4,${startDate}`;
128 predicates += `&9=f101,1,${endDate}`;
131 predicates += `&5=${maxRows}`;
133 const response = await fieldpineApi({
134 endpoint: `/buck?3=retailmax.elink.sale.paymentlist${predicates}`,
138 if (response?.data) {
139 const data = response.data;
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})`);
145 // Process current batch
146 if (data.APP && Array.isArray(data.APP)) {
147 setPayments(prev => [...prev, ...data.APP]);
150 // Load next batch after delay
152 loadPaymentsBatch(apiKey, data.f47, batchCount + 1);
159 if (data.APP && Array.isArray(data.APP)) {
160 setPayments(prev => [...prev, ...data.APP]);
165 const handleSort = (key: keyof PaymentRecord) => {
166 let direction: 'asc' | 'desc' = 'asc';
167 if (sortConfig && sortConfig.key === key && sortConfig.direction === 'asc') {
170 setSortConfig({ key, direction });
173 const getSortedPayments = () => {
174 if (!sortConfig) return payments;
176 return [...payments].sort((a, b) => {
177 const aVal = a[sortConfig.key];
178 const bVal = b[sortConfig.key];
180 if (aVal === undefined || aVal === null) return 1;
181 if (bVal === undefined || bVal === null) return -1;
183 if (typeof aVal === 'number' && typeof bVal === 'number') {
184 return sortConfig.direction === 'asc' ? aVal - bVal : bVal - aVal;
187 const aStr = String(aVal).toLowerCase();
188 const bStr = String(bVal).toLowerCase();
190 if (aStr < bStr) return sortConfig.direction === 'asc' ? -1 : 1;
191 if (aStr > bStr) return sortConfig.direction === 'asc' ? 1 : -1;
196 const sortedPayments = getSortedPayments();
199 <div className="p-6">
200 <h1 className="text-3xl font-bold mb-6">Payments List</h1>
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">
206 <label className="block text-sm font-medium mb-1">
207 From (including) <span className="text-red-600">*</span>
212 onChange={(e) => setFromDate(e.target.value)}
213 className="w-full border rounded px-3 py-2"
218 <label className="block text-sm font-medium mb-1">
219 To (excluding) <span className="text-red-600">*</span>
224 onChange={(e) => setToDate(e.target.value)}
225 className="w-full border rounded px-3 py-2"
230 <label className="block text-sm font-medium mb-1">Payment Type</label>
233 onChange={(e) => setPaymentType(e.target.value)}
234 className="w-full border rounded px-3 py-2"
236 <option value="0">--Select--</option>
237 {PAYMENT_TYPES.map(pt => (
238 <option key={pt.id} value={pt.id}>{pt.name}</option>
244 <label className="block text-sm font-medium mb-1">Rows</label>
245 <div className="flex gap-2">
249 onChange={(e) => setMaxRows(e.target.value)}
250 className="w-20 border rounded px-3 py-2"
255 onClick={loadPayments}
257 className="bg-brand text-white px-6 py-2 rounded hover:bg-brand2 disabled:bg-muted/50 flex-1"
259 {loading ? 'Loading...' : 'Display'}
266 {/* Loading Message */}
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>
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">
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"
285 DateTime {sortConfig?.key === 'f101' && (sortConfig.direction === 'asc' ? '↑' : '↓')}
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"
291 Type {sortConfig?.key === 'f100' && (sortConfig.direction === 'asc' ? '↑' : '↓')}
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"
297 Type Description {sortConfig?.key === 'f900' && (sortConfig.direction === 'asc' ? '↑' : '↓')}
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"
303 Amount {sortConfig?.key === 'f103' && (sortConfig.direction === 'asc' ? '↑' : '↓')}
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"
309 Sale {sortConfig?.key === 'f104' && (sortConfig.direction === 'asc' ? '↑' : '↓')}
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"
315 Customer {sortConfig?.key === 'f153' && (sortConfig.direction === 'asc' ? '↑' : '↓')}
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"
321 Purchased (main item) {sortConfig?.key === 'f152' && (sortConfig.direction === 'asc' ? '↑' : '↓')}
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)}
331 <td className="px-6 py-4 whitespace-nowrap text-sm text-text">
334 <td className="px-6 py-4 whitespace-nowrap text-sm text-text">
337 <td className="px-6 py-4 whitespace-nowrap text-sm text-text text-right font-mono">
338 {formatCurrency(payment.f103 || 0)}
340 <td className="px-6 py-4 whitespace-nowrap text-sm">
343 href={`/report/pos/sales/fieldpine/singlesale.htm?sid=${payment.f104}`}
345 className="text-brand hover:underline"
353 <td className="px-6 py-4 whitespace-nowrap text-sm">
354 {payment.f151 && payment.f153 ? (
356 href={`/report/pos/customer/fieldpine/customer_single_overview.htm?cid=${payment.f151}`}
358 className="text-brand hover:underline"
366 <td className="px-6 py-4 text-sm text-text">
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' : ''}
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.