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
15export default function AccountAdjustmentPage() {
16 const [searchTerm, setSearchTerm] = useState("");
17 const [searchResults, setSearchResults] = useState<Account[]>([]);
18 const [searchLoading, setSearchLoading] = useState(false);
19 const [selectedAccount, setSelectedAccount] = useState<Account | null>(null);
20
21 const [amount, setAmount] = useState<string>("");
22 const [adjustmentDate, setAdjustmentDate] = useState<string>(() => {
23 const d = new Date();
24 return d.toISOString().slice(0, 10);
25 });
26 const [comments, setComments] = useState<string>("Adjustment");
27
28 const [saving, setSaving] = useState(false);
29 const [statusMessage, setStatusMessage] = useState<string>("");
30 const [statusVariant, setStatusVariant] = useState<"success" | "error" | "info" | "">("");
31
32 useEffect(() => {
33 if (searchTerm.trim().length < 2) {
34 setSearchResults([]);
35 return;
36 }
37
38 const handle = setTimeout(() => {
39 const doSearch = async () => {
40 setSearchLoading(true);
41 try {
42 // Search accounts via API
43 const response = await apiClient.request(`/v1/buck/accounts?search=${encodeURIComponent(searchTerm.trim())}&limit=10`);
44 if (response?.data?.DATS) {
45 const mapped: Account[] = response.data.DATS.map((item: any) => ({
46 id: Number(item.f100),
47 name: item.f101,
48 balance: Number(item.f114) || 0,
49 email: item.f152,
50 paymentHistory: item.f1105,
51 lastStatement: item.f1106,
52 }));
53 setSearchResults(mapped);
54 }
55 } catch (err) {
56 console.error("Account search failed", err);
57 } finally {
58 setSearchLoading(false);
59 }
60 };
61
62 doSearch();
63 }, 300);
64
65 return () => clearTimeout(handle);
66 }, [searchTerm]);
67
68 const formatCurrency = (value: number) => {
69 return new Intl.NumberFormat("en-US", {
70 style: "currency",
71 currency: "USD",
72 minimumFractionDigits: 2,
73 }).format(value);
74 };
75
76 const projectedBalance = useMemo(() => {
77 if (!selectedAccount) return null;
78 const delta = Number(amount);
79 if (Number.isNaN(delta)) return selectedAccount.balance;
80 return selectedAccount.balance + delta;
81 }, [selectedAccount, amount]);
82
83 const canSubmit = useMemo(() => {
84 return !!selectedAccount && Number(amount) !== 0 && !Number.isNaN(Number(amount)) && !saving;
85 }, [selectedAccount, amount, saving]);
86
87 const handleSelectAccount = (account: Account) => {
88 setSelectedAccount(account);
89 setStatusMessage("");
90 };
91
92 const handleSubmit = async () => {
93 if (!selectedAccount) {
94 setStatusMessage("Please select an account");
95 setStatusVariant("error");
96 return;
97 }
98
99 const delta = Number(amount);
100 if (Number.isNaN(delta) || delta === 0) {
101 setStatusMessage("Enter an adjustment amount (positive or negative, not zero)");
102 setStatusVariant("error");
103 return;
104 }
105
106 setSaving(true);
107 setStatusMessage("Saving adjustment...");
108 setStatusVariant("info");
109
110 const xml = [
111 "<DATI>",
112 "<f8_s>retailmax.elink.account.request</f8_s>",
113 "<f11_B>I</f11_B>",
114 `<f100_E>${selectedAccount.id}</f100_E>`,
115 "<f101_E>121</f101_E>",
116 `<f151_s>${amount}</f151_s>`,
117 `<f152_s>${adjustmentDate}</f152_s>`,
118 comments ? `<f153_s>${comments}</f153_s>` : "",
119 "</DATI>",
120 ].join("");
121
122 try {
123 const response = await fetch("/DATI", {
124 method: "POST",
125 headers: {
126 "Content-Type": "application/xml",
127 Accept: "application/json",
128 },
129 credentials: "include",
130 body: xml,
131 });
132
133 if (!response.ok) {
134 throw new Error(`HTTP ${response.status}`);
135 }
136
137 setStatusMessage("Adjustment saved successfully.");
138 setStatusVariant("success");
139 setSaving(false);
140 } catch (err) {
141 console.error("Failed to save adjustment", err);
142 setStatusMessage("Adjustment failed. Please try again.");
143 setStatusVariant("error");
144 setSaving(false);
145 }
146 };
147
148 return (
149 <div className="p-6 max-w-6xl mx-auto space-y-6">
150 <div className="mb-2">
151 <h1 className="text-3xl font-bold text-text mb-2">Record Account Adjustment</h1>
152 <p className="text-muted text-sm">
153 Apply an adjustment to an account balance. Positive amounts increase the balance owing; negative amounts decrease it.
154 </p>
155 </div>
156
157 {statusMessage && (
158 <div
159 className={`rounded-lg px-4 py-3 text-sm border ${
160 statusVariant === "success"
161 ? "bg-green-50 border-green-200 text-green-800"
162 : statusVariant === "error"
163 ? "bg-red-50 border-red-200 text-red-800"
164 : "bg-info/10 border-info/30 text-info"
165 }`}
166 >
167 {statusMessage}
168 </div>
169 )}
170
171 <div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
172 <div className="lg:col-span-2 space-y-4">
173 <div className="bg-surface rounded-lg shadow p-4">
174 <h2 className="text-lg font-semibold text-text mb-3">Select Account</h2>
175 <div className="flex items-center gap-3">
176 <input
177 type="text"
178 value={searchTerm}
179 onChange={(e) => setSearchTerm(e.target.value)}
180 placeholder="Search account by name"
181 className="w-full px-3 py-2 border border-border rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-green-500"
182 />
183 {searchLoading && <span className="text-sm text-muted">Searching...</span>}
184 </div>
185 {searchResults.length > 0 && (
186 <div className="mt-3 border border-border rounded-md divide-y max-h-64 overflow-y-auto">
187 {searchResults.map((acc) => (
188 <button
189 key={acc.id}
190 onClick={() => handleSelectAccount(acc)}
191 className={`w-full text-left px-3 py-2 hover:bg-surface-2 flex justify-between items-center ${
192 selectedAccount?.id === acc.id ? "bg-green-50" : ""
193 }`}
194 >
195 <div>
196 <p className="text-sm font-medium text-text">{acc.name}</p>
197 <p className="text-xs text-muted">Id: {acc.id}</p>
198 </div>
199 <div className="text-sm font-semibold text-text">{formatCurrency(acc.balance)}</div>
200 </button>
201 ))}
202 </div>
203 )}
204 </div>
205
206 <div className="bg-surface rounded-lg shadow p-4 space-y-4">
207 <h2 className="text-lg font-semibold text-text">Adjustment Details</h2>
208
209 <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
210 <div>
211 <label className="block text-sm font-medium text-text mb-1">Amount</label>
212 <input
213 type="number"
214 step="0.01"
215 value={amount}
216 onChange={(e) => setAmount(e.target.value)}
217 className="w-full px-3 py-2 border border-border rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-green-500"
218 placeholder="Use positive or negative values"
219 />
220 </div>
221
222 <div>
223 <label className="block text-sm font-medium text-text mb-1">Date</label>
224 <input
225 type="date"
226 value={adjustmentDate}
227 onChange={(e) => setAdjustmentDate(e.target.value)}
228 className="w-full px-3 py-2 border border-border rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-green-500"
229 />
230 </div>
231
232 <div className="md:col-span-2">
233 <label className="block text-sm font-medium text-text mb-1">Comments (shown to account holder)</label>
234 <input
235 type="text"
236 value={comments}
237 onChange={(e) => setComments(e.target.value)}
238 className="w-full px-3 py-2 border border-border rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-green-500"
239 placeholder="Adjustment description"
240 />
241 </div>
242 </div>
243
244 <div className="flex gap-3 pt-2">
245 <button
246 onClick={handleSubmit}
247 disabled={!canSubmit}
248 className="px-5 py-2 bg-[#00543b] text-white rounded-md font-semibold hover:bg-[#003d2b] disabled:bg-surface-2 disabled:cursor-not-allowed"
249 >
250 {saving ? "Saving..." : "Save Adjustment"}
251 </button>
252 <button
253 onClick={() => {
254 setSelectedAccount(null);
255 setSearchTerm("");
256 setAmount("");
257 setComments("Adjustment");
258 setStatusMessage("");
259 setStatusVariant("");
260 }}
261 className="px-4 py-2 border border-border rounded-md text-sm text-text hover:bg-surface-2"
262 >
263 Clear
264 </button>
265 </div>
266 </div>
267 </div>
268
269 <div className="bg-surface rounded-lg shadow p-4">
270 <h2 className="text-lg font-semibold text-text mb-3">Account Summary</h2>
271 {selectedAccount ? (
272 <div className="space-y-3">
273 <div>
274 <p className="text-sm text-muted">Account</p>
275 <p className="text-lg font-semibold text-text">{selectedAccount.name}</p>
276 <p className="text-sm text-muted">Id: {selectedAccount.id}</p>
277 </div>
278
279 <div className="flex items-center justify-between">
280 <span className="text-sm text-muted">Current Balance</span>
281 <span className="text-lg font-bold text-text">{formatCurrency(selectedAccount.balance)}</span>
282 </div>
283
284 {typeof projectedBalance === "number" && (
285 <div className="flex items-center justify-between">
286 <span className="text-sm text-muted">New Balance (after adjustment)</span>
287 <span className="text-lg font-bold text-text">{formatCurrency(projectedBalance)}</span>
288 </div>
289 )}
290
291 {selectedAccount.email && (
292 <div className="flex items-center justify-between">
293 <span className="text-sm text-muted">Email</span>
294 <a href={`mailto:${selectedAccount.email}`} className="text-sm text-[#00543b] hover:underline">
295 {selectedAccount.email}
296 </a>
297 </div>
298 )}
299
300 {selectedAccount.lastStatement && (
301 <div className="flex items-center justify-between text-sm text-muted">
302 <span>Last Statement</span>
303 <span>{selectedAccount.lastStatement}</span>
304 </div>
305 )}
306
307 {selectedAccount.paymentHistory && (
308 <div>
309 <p className="text-sm text-muted mb-1">Payment History</p>
310 <div
311 className="text-sm text-text bg-surface-2 rounded p-3"
312 dangerouslySetInnerHTML={{ __html: selectedAccount.paymentHistory }}
313 />
314 </div>
315 )}
316 </div>
317 ) : (
318 <p className="text-sm text-muted">Select an account to see details.</p>
319 )}
320 </div>
321 </div>
322 </div>
323 );
324}