3import React, { useEffect, useMemo, useState } from "react";
4import { apiClient } from "@/lib/client/apiClient";
11 paymentHistory?: string;
12 lastStatement?: string;
15interface PaymentType {
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);
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>(() => {
31 return d.toISOString().slice(0, 10);
33 const [note, setNote] = useState<string>("");
35 const [saving, setSaving] = useState(false);
36 const [statusMessage, setStatusMessage] = useState<string>("");
37 const [statusVariant, setStatusVariant] = useState<"success" | "error" | "info" | "">("");
39 // Load payment types once
41 const loadPaymentTypes = async () => {
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 }));
55 setPaymentTypes(filtered);
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);
66 console.error("Failed to load payment types", err);
73 // Search accounts with debounce
75 if (searchTerm.trim().length < 2) {
80 const handle = setTimeout(() => {
81 const doSearch = async () => {
82 setSearchLoading(true);
84 const response = await apiClient.getAccounts({
86 search: searchTerm.trim()
88 if (response.success && response.data) {
89 const mapped: Account[] = response.data.map((item: any) => ({
90 id: Number(item.f100),
92 balance: Number(item.f114) || 0,
94 paymentHistory: item.f1105,
95 lastStatement: item.f1106,
97 setSearchResults(mapped);
100 console.error("Account search failed", err);
102 setSearchLoading(false);
109 return () => clearTimeout(handle);
112 const formatCurrency = (value: number) => {
113 return new Intl.NumberFormat("en-US", {
116 minimumFractionDigits: 2,
120 const canSubmit = useMemo(() => {
121 return !!selectedAccount && Number(amount) > 0 && paymentTypeId !== "0" && !saving;
122 }, [selectedAccount, amount, paymentTypeId, saving]);
124 const handleSelectAccount = (account: Account) => {
125 setSelectedAccount(account);
126 setStatusMessage("");
129 const handleSubmit = async () => {
130 if (!selectedAccount) {
131 setStatusMessage("Please select an account");
132 setStatusVariant("error");
135 if (Number(amount) <= 0) {
136 setStatusMessage("Please enter a positive amount");
137 setStatusVariant("error");
140 if (paymentTypeId === "0") {
141 setStatusMessage("Please select a payment type");
142 setStatusVariant("error");
147 setStatusMessage("Saving payment...");
148 setStatusVariant("info");
150 // Persist last payment type
151 if (typeof window !== "undefined") {
152 window.localStorage.setItem("AccPay.LastPayType", paymentTypeId);
157 "<f8_s>retailmax.elink.account.request</f8_s>",
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>` : "",
169 const response = await fetch("/DATI", {
172 "Content-Type": "application/xml",
173 Accept: "application/json",
175 credentials: "include",
180 throw new Error(`HTTP ${response.status}`);
183 setStatusMessage("Payment recorded successfully.");
184 setStatusVariant("success");
187 console.error("Failed to save payment", err);
188 setStatusMessage("Payment failed. Please try again.");
189 setStatusVariant("error");
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.
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"
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">
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"
231 {searchLoading && <span className="text-sm text-muted">Searching...</span>}
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) => (
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" : ""
244 <p className="text-sm font-medium text-text">{acc.name}</p>
245 <p className="text-xs text-muted">Id: {acc.id}</p>
247 <div className="text-sm font-semibold text-text">{formatCurrency(acc.balance)}</div>
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>
257 <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
259 <label className="block text-sm font-medium text-text mb-1">Amount</label>
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"
272 <label className="block text-sm font-medium text-text mb-1">Payment Type</label>
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"
278 <option value="0">-- Select payment type --</option>
279 {paymentTypes.map((pt) => (
280 <option key={pt.id} value={pt.id}>{pt.name}</option>
286 <label className="block text-sm font-medium text-text mb-1">Date</label>
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"
296 <label className="block text-sm font-medium text-text mb-1">Reference / Note (optional)</label>
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"
307 <div className="flex gap-3 pt-2">
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"
313 {saving ? "Saving..." : "Save Payment"}
317 setSelectedAccount(null);
320 setPaymentTypeId("0");
322 setStatusMessage("");
323 setStatusVariant("");
325 className="px-4 py-2 border border-border rounded-md text-sm text-text hover:bg-surface-2"
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>
337 <div className="space-y-3">
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>
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>
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}
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>
365 {selectedAccount.paymentHistory && (
367 <p className="text-sm text-muted mb-1">Payment History</p>
369 className="text-sm text-text bg-surface-2 rounded p-3"
370 dangerouslySetInnerHTML={{ __html: selectedAccount.paymentHistory }}
376 <p className="text-sm text-muted">Select an account to see details.</p>