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, { useEffect, useMemo, useState } from "react";
4import { apiClient } from "@/lib/client/apiClient";
5
6interface Account {
7 id: number;
8 name: string;
9 balance: number;
10 email?: string;
11 paymentHistory?: string;
12 lastStatement?: string;
13}
14
15interface PaymentType {
16 id: number;
17 name: string;
18}
19
20export default function EnterPaymentPage() {
21 const [searchTerm, setSearchTerm] = useState("");
22 const [searchResults, setSearchResults] = useState<Account[]>([]);
23 const [searchLoading, setSearchLoading] = useState(false);
24 const [selectedAccount, setSelectedAccount] = useState<Account | null>(null);
25
26 const [paymentTypes, setPaymentTypes] = useState<PaymentType[]>([]);
27 const [paymentTypeId, setPaymentTypeId] = useState<string>("0");
28 const [amount, setAmount] = useState<string>("");
29 const [paymentDate, setPaymentDate] = useState<string>(() => {
30 const d = new Date();
31 return d.toISOString().slice(0, 10);
32 });
33 const [note, setNote] = useState<string>("");
34
35 const [saving, setSaving] = useState(false);
36 const [statusMessage, setStatusMessage] = useState<string>("");
37 const [statusVariant, setStatusVariant] = useState<"success" | "error" | "info" | "">("");
38
39 // Load payment types once
40 useEffect(() => {
41 const loadPaymentTypes = async () => {
42 try {
43 const response = await apiClient.getPaymentTypesList();
44 if (response.success && response.data) {
45 const filtered = response.data.filter((rec: any) => {
46 // Align with legacy rules: exclude account debits, invisible, negative effects, rounding, sale discount
47 const invisible = rec.f211 !== 0;
48 const isAccountDebit = rec.f202 !== 0;
49 const negativeEffect = rec.f210 !== 0;
50 const rounding = rec.f100 === 7;
51 const saleDiscount = rec.f100 === 11;
52 return !invisible && !isAccountDebit && !negativeEffect && !rounding && !saleDiscount;
53 }).map((rec: any) => ({ id: rec.f100 as number, name: rec.f101 as string }));
54
55 setPaymentTypes(filtered);
56
57 // Restore last selection from localStorage if present
58 if (typeof window !== "undefined") {
59 const last = window.localStorage.getItem("AccPay.LastPayType");
60 if (last && filtered.some((p: any) => p.id.toString() === last)) {
61 setPaymentTypeId(last);
62 }
63 }
64 }
65 } catch (err) {
66 console.error("Failed to load payment types", err);
67 }
68 };
69
70 loadPaymentTypes();
71 }, []);
72
73 // Search accounts with debounce
74 useEffect(() => {
75 if (searchTerm.trim().length < 2) {
76 setSearchResults([]);
77 return;
78 }
79
80 const handle = setTimeout(() => {
81 const doSearch = async () => {
82 setSearchLoading(true);
83 try {
84 const response = await apiClient.getAccounts({
85 type: 'search',
86 search: searchTerm.trim()
87 });
88 if (response.success && response.data) {
89 const mapped: Account[] = response.data.map((item: any) => ({
90 id: Number(item.f100),
91 name: item.f101,
92 balance: Number(item.f114) || 0,
93 email: item.f152,
94 paymentHistory: item.f1105,
95 lastStatement: item.f1106,
96 }));
97 setSearchResults(mapped);
98 }
99 } catch (err) {
100 console.error("Account search failed", err);
101 } finally {
102 setSearchLoading(false);
103 }
104 };
105
106 doSearch();
107 }, 300);
108
109 return () => clearTimeout(handle);
110 }, [searchTerm]);
111
112 const formatCurrency = (value: number) => {
113 return new Intl.NumberFormat("en-US", {
114 style: "currency",
115 currency: "USD",
116 minimumFractionDigits: 2,
117 }).format(value);
118 };
119
120 const canSubmit = useMemo(() => {
121 return !!selectedAccount && Number(amount) > 0 && paymentTypeId !== "0" && !saving;
122 }, [selectedAccount, amount, paymentTypeId, saving]);
123
124 const handleSelectAccount = (account: Account) => {
125 setSelectedAccount(account);
126 setStatusMessage("");
127 };
128
129 const handleSubmit = async () => {
130 if (!selectedAccount) {
131 setStatusMessage("Please select an account");
132 setStatusVariant("error");
133 return;
134 }
135 if (Number(amount) <= 0) {
136 setStatusMessage("Please enter a positive amount");
137 setStatusVariant("error");
138 return;
139 }
140 if (paymentTypeId === "0") {
141 setStatusMessage("Please select a payment type");
142 setStatusVariant("error");
143 return;
144 }
145
146 setSaving(true);
147 setStatusMessage("Saving payment...");
148 setStatusVariant("info");
149
150 // Persist last payment type
151 if (typeof window !== "undefined") {
152 window.localStorage.setItem("AccPay.LastPayType", paymentTypeId);
153 }
154
155 const xml = [
156 "<DATI>",
157 "<f8_s>retailmax.elink.account.request</f8_s>",
158 "<f11_B>I</f11_B>",
159 `<f100_E>${selectedAccount.id}</f100_E>`,
160 "<f101_E>120</f101_E>",
161 `<f150_E>${paymentTypeId}</f150_E>`,
162 `<f151_s>${amount}</f151_s>`,
163 `<f152_s>${paymentDate}</f152_s>`,
164 note ? `<f160_s>${note}</f160_s>` : "",
165 "</DATI>",
166 ].join("");
167
168 try {
169 const response = await fetch("/DATI", {
170 method: "POST",
171 headers: {
172 "Content-Type": "application/xml",
173 Accept: "application/json",
174 },
175 credentials: "include",
176 body: xml,
177 });
178
179 if (!response.ok) {
180 throw new Error(`HTTP ${response.status}`);
181 }
182
183 setStatusMessage("Payment recorded successfully.");
184 setStatusVariant("success");
185 setSaving(false);
186 } catch (err) {
187 console.error("Failed to save payment", err);
188 setStatusMessage("Payment failed. Please try again.");
189 setStatusVariant("error");
190 setSaving(false);
191 }
192 };
193
194 return (
195 <div className="p-6 max-w-6xl mx-auto space-y-6">
196 <div className="mb-2">
197 <h1 className="text-3xl font-bold text-text mb-2">Record Account Payment</h1>
198 <p className="text-muted text-sm">
199 Record a payment received against a customer account. Payments are saved directly to POS.
200 </p>
201 </div>
202
203 {/* Status */}
204 {statusMessage && (
205 <div
206 className={`rounded-lg px-4 py-3 text-sm border ${
207 statusVariant === "success"
208 ? "bg-green-50 border-green-200 text-green-800"
209 : statusVariant === "error"
210 ? "bg-red-50 border-red-200 text-red-800"
211 : "bg-info/10 border-info/30 text-info"
212 }`}
213 >
214 {statusMessage}
215 </div>
216 )}
217
218 <div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
219 {/* Left: Search & form */}
220 <div className="lg:col-span-2 space-y-4">
221 <div className="bg-surface rounded-lg shadow p-4">
222 <h2 className="text-lg font-semibold text-text mb-3">Select Account</h2>
223 <div className="flex items-center gap-3">
224 <input
225 type="text"
226 value={searchTerm}
227 onChange={(e) => setSearchTerm(e.target.value)}
228 placeholder="Search account by name"
229 className="w-full px-3 py-2 border border-border rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-green-500"
230 />
231 {searchLoading && <span className="text-sm text-muted">Searching...</span>}
232 </div>
233 {searchResults.length > 0 && (
234 <div className="mt-3 border border-border rounded-md divide-y max-h-64 overflow-y-auto">
235 {searchResults.map((acc) => (
236 <button
237 key={acc.id}
238 onClick={() => handleSelectAccount(acc)}
239 className={`w-full text-left px-3 py-2 hover:bg-surface-2 flex justify-between items-center ${
240 selectedAccount?.id === acc.id ? "bg-green-50" : ""
241 }`}
242 >
243 <div>
244 <p className="text-sm font-medium text-text">{acc.name}</p>
245 <p className="text-xs text-muted">Id: {acc.id}</p>
246 </div>
247 <div className="text-sm font-semibold text-text">{formatCurrency(acc.balance)}</div>
248 </button>
249 ))}
250 </div>
251 )}
252 </div>
253
254 <div className="bg-surface rounded-lg shadow p-4 space-y-4">
255 <h2 className="text-lg font-semibold text-text">Payment Details</h2>
256
257 <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
258 <div>
259 <label className="block text-sm font-medium text-text mb-1">Amount</label>
260 <input
261 type="number"
262 min="0"
263 step="0.01"
264 value={amount}
265 onChange={(e) => setAmount(e.target.value)}
266 className="w-full px-3 py-2 border border-border rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-green-500"
267 placeholder="0.00"
268 />
269 </div>
270
271 <div>
272 <label className="block text-sm font-medium text-text mb-1">Payment Type</label>
273 <select
274 value={paymentTypeId}
275 onChange={(e) => setPaymentTypeId(e.target.value)}
276 className="w-full px-3 py-2 border border-border rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-green-500"
277 >
278 <option value="0">-- Select payment type --</option>
279 {paymentTypes.map((pt) => (
280 <option key={pt.id} value={pt.id}>{pt.name}</option>
281 ))}
282 </select>
283 </div>
284
285 <div>
286 <label className="block text-sm font-medium text-text mb-1">Date</label>
287 <input
288 type="date"
289 value={paymentDate}
290 onChange={(e) => setPaymentDate(e.target.value)}
291 className="w-full px-3 py-2 border border-border rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-green-500"
292 />
293 </div>
294
295 <div>
296 <label className="block text-sm font-medium text-text mb-1">Reference / Note (optional)</label>
297 <input
298 type="text"
299 value={note}
300 onChange={(e) => setNote(e.target.value)}
301 className="w-full px-3 py-2 border border-border rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-green-500"
302 placeholder="Reference or note"
303 />
304 </div>
305 </div>
306
307 <div className="flex gap-3 pt-2">
308 <button
309 onClick={handleSubmit}
310 disabled={!canSubmit}
311 className="px-5 py-2 bg-[#00543b] text-white rounded-md font-semibold hover:bg-[#003d2b] disabled:bg-surface-2 disabled:cursor-not-allowed"
312 >
313 {saving ? "Saving..." : "Save Payment"}
314 </button>
315 <button
316 onClick={() => {
317 setSelectedAccount(null);
318 setSearchTerm("");
319 setAmount("");
320 setPaymentTypeId("0");
321 setNote("");
322 setStatusMessage("");
323 setStatusVariant("");
324 }}
325 className="px-4 py-2 border border-border rounded-md text-sm text-text hover:bg-surface-2"
326 >
327 Clear
328 </button>
329 </div>
330 </div>
331 </div>
332
333 {/* Right: Account summary */}
334 <div className="bg-surface rounded-lg shadow p-4">
335 <h2 className="text-lg font-semibold text-text mb-3">Account Summary</h2>
336 {selectedAccount ? (
337 <div className="space-y-3">
338 <div>
339 <p className="text-sm text-muted">Account</p>
340 <p className="text-lg font-semibold text-text">{selectedAccount.name}</p>
341 <p className="text-sm text-muted">Id: {selectedAccount.id}</p>
342 </div>
343
344 <div className="flex items-center justify-between">
345 <span className="text-sm text-muted">Current Balance</span>
346 <span className="text-lg font-bold text-text">{formatCurrency(selectedAccount.balance)}</span>
347 </div>
348
349 {selectedAccount.email && (
350 <div className="flex items-center justify-between">
351 <span className="text-sm text-muted">Email</span>
352 <a href={`mailto:${selectedAccount.email}`} className="text-sm text-[#00543b] hover:underline">
353 {selectedAccount.email}
354 </a>
355 </div>
356 )}
357
358 {selectedAccount.lastStatement && (
359 <div className="flex items-center justify-between text-sm text-muted">
360 <span>Last Statement</span>
361 <span>{selectedAccount.lastStatement}</span>
362 </div>
363 )}
364
365 {selectedAccount.paymentHistory && (
366 <div>
367 <p className="text-sm text-muted mb-1">Payment History</p>
368 <div
369 className="text-sm text-text bg-surface-2 rounded p-3"
370 dangerouslySetInnerHTML={{ __html: selectedAccount.paymentHistory }}
371 />
372 </div>
373 )}
374 </div>
375 ) : (
376 <p className="text-sm text-muted">Select an account to see details.</p>
377 )}
378 </div>
379 </div>
380 </div>
381 );
382}