Skip to content

Commit 918972b

Browse files
committed
feat(dashboard): add empty state and optimize UserPaymentLogs rendering
Compute visibleEntries once (filter + slice) and return a clear empty-state when no payment-related entries exist. Refactor mapping to use visibleEntries for readability and to avoid repeated filtering. Minor layout/formatting cleanup.
1 parent f556f84 commit 918972b

File tree

1 file changed

+160
-153
lines changed

1 file changed

+160
-153
lines changed

src/react/pages/dashboard/UserPaymentLogs.tsx

Lines changed: 160 additions & 153 deletions
Original file line numberDiff line numberDiff line change
@@ -167,175 +167,182 @@ export default function UserPaymentLogs({ logs: initialLogs, maxItems = 50, clas
167167
// -------------------------------------
168168
// RENDER
169169
// -------------------------------------
170+
// compute visible entries once so we can show an empty-state if none match
171+
const visibleEntries = (logs || [])
172+
.filter((e) => e.package_buy || e.payment)
173+
.slice(0, maxItems)
174+
.filter((entry) => {
175+
const pkg = entry.package_buy;
176+
const pay = entry.payment;
177+
const package_type = String(pkg?.action_type ?? '').toUpperCase();
178+
const payment_type = String(pay?.action_type ?? '').toUpperCase();
179+
if (payment_type === 'PAYMENT') {
180+
return true;
181+
}
182+
if (package_type === 'PACKAGE_BUY') {
183+
return true;
184+
}
185+
return false;
186+
});
187+
188+
if (visibleEntries.length === 0)
189+
return (
190+
<div className={className}>
191+
<p className="text-sm text-gray-500">No payment activity found.</p>
192+
</div>
193+
);
194+
170195
return (
171196
<div className={`${className} w-full`} role="list" aria-label="User payment logs">
172-
{logs
173-
.filter((e) => e.package_buy || e.payment)
174-
.slice(0, maxItems)
175-
.filter((entry) => {
176-
const pkg = entry.package_buy;
177-
const pay = entry.payment;
178-
const package_type = String(pkg?.action_type ?? '').toUpperCase();
179-
const payment_type = String(pay?.action_type ?? '').toUpperCase();
180-
if (payment_type === 'PAYMENT') {
181-
return true;
182-
}
183-
if (package_type === 'PACKAGE_BUY') {
184-
return true;
185-
}
186-
return false;
187-
})
188-
.map((entry, i) => {
189-
const pkg = entry.package_buy;
190-
const pay = entry.payment;
191-
// const package_type = pkg?.action_type;
192-
// const payment_type = pay?.action_type;
193-
// console.log({ package_type, payment_type });
194-
// compute transaction status from payment or package buy details
195-
function getTxStatus(): string | null {
196-
const d = pay?.details ?? pkg?.details ?? pay ?? pkg ?? null;
197-
if (!d) return null;
198-
199-
// normalize if details is string
200-
let details = d;
201-
if (typeof details === 'string') {
202-
try {
203-
details = JSON.parse(details);
204-
} catch {
205-
// leave as-is
206-
}
197+
{visibleEntries.map((entry, i) => {
198+
const pkg = entry.package_buy;
199+
const pay = entry.payment;
200+
// const package_type = pkg?.action_type;
201+
// const payment_type = pay?.action_type;
202+
// console.log({ package_type, payment_type });
203+
// compute transaction status from payment or package buy details
204+
function getTxStatus(): string | null {
205+
const d = pay?.details ?? pkg?.details ?? pay ?? pkg ?? null;
206+
if (!d) return null;
207+
208+
// normalize if details is string
209+
let details = d;
210+
if (typeof details === 'string') {
211+
try {
212+
details = JSON.parse(details);
213+
} catch {
214+
// leave as-is
207215
}
216+
}
208217

209-
// common fields: status, payment_result.error, payment_result.message, error
210-
if (typeof details === 'object') {
211-
if (details.status) return String(details.status);
212-
if (details.payment_result) {
213-
const pr = details.payment_result;
214-
if (pr.error === true) return 'error';
215-
if (pr.error === false && pr.message) return String(pr.message);
216-
if (pr.error === false) return 'successful';
217-
}
218-
if (details.error === true) return 'error';
219-
if (details.error === false) return 'successful';
218+
// common fields: status, payment_result.error, payment_result.message, error
219+
if (typeof details === 'object') {
220+
if (details.status) return String(details.status);
221+
if (details.payment_result) {
222+
const pr = details.payment_result;
223+
if (pr.error === true) return 'error';
224+
if (pr.error === false && pr.message) return String(pr.message);
225+
if (pr.error === false) return 'successful';
220226
}
221-
222-
return null;
227+
if (details.error === true) return 'error';
228+
if (details.error === false) return 'successful';
223229
}
224-
const amount = pay?.amount ?? pkg?.amount ?? undefined;
225-
const when = entry.created_at ?? pay?.created_at ?? pkg?.created_at;
226-
227-
// stable string id for key and openSections (prefer tx, fall back to ids or index)
228-
const id = entry.tx ?? `${pkg?.id ?? pay?.id ?? `no-tx-${i}`}`;
229-
// isp: axis, im3, unknown
230-
const isp = String(pkg?.details?.package_isp ?? pay?.details?.package_isp ?? 'unknown');
231-
// typed lookup to avoid TS indexing errors
232-
const iconSrc = (brandIcons as Record<string, string>)[isp];
233-
234-
return (
235-
<div
236-
key={id}
237-
role="listitem"
238-
className="mb-3 bg-white dark:bg-gray-800 rounded-md shadow-sm dark:shadow-white overflow-hidden">
239-
<div className="px-3 py-2 flex items-center justify-between gap-3">
240-
<div className="flex items-center gap-3 min-w-0">
241-
<div className="flex-shrink-0">
242-
{iconSrc ? (
243-
<div className="h-8 w-8 rounded-full bg-gray-100 dark:bg-gray-700 overflow-hidden flex items-center justify-center">
244-
<img src={iconSrc} alt={`${isp} logo`} className="h-full w-full object-cover" />
245-
</div>
246-
) : (
247-
<div className="h-8 w-8 rounded-full bg-gray-100 dark:bg-gray-700 flex items-center justify-center text-xs text-gray-600">
248-
TX
249-
</div>
250-
)}
251-
</div>
252-
<div className="min-w-0">
253-
<p className="text-sm font-medium text-gray-800 dark:text-gray-100 truncate">{id}</p>
254-
{(() => {
255-
const status = getTxStatus();
256-
return status ? (
257-
<div className="mt-1 flex items-center gap-2">
258-
<span className="text-xs text-gray-500 dark:text-gray-400">Status</span>
259-
<span className="text-xs px-2 py-0.5 rounded-full bg-gray-100 dark:bg-gray-900 text-gray-700 dark:text-gray-200">
260-
{status}
261-
</span>
262-
</div>
263-
) : null;
264-
})()}
265-
</div>
266-
</div>
267230

268-
<div className="flex-shrink-0 text-right">
269-
{typeof amount === 'number' && (
270-
<p className="text-sm font-semibold text-green-600 dark:text-green-400">{Math.round(amount)}</p>
271-
)}
272-
{when && (
273-
<p className="text-xs text-gray-400 dark:text-gray-500">{new Date(when).toLocaleString()}</p>
231+
return null;
232+
}
233+
const amount = pay?.amount ?? pkg?.amount ?? undefined;
234+
const when = entry.created_at ?? pay?.created_at ?? pkg?.created_at;
235+
236+
// stable string id for key and openSections (prefer tx, fall back to ids or index)
237+
const id = entry.tx ?? `${pkg?.id ?? pay?.id ?? `no-tx-${i}`}`;
238+
// isp: axis, im3, unknown
239+
const isp = String(pkg?.details?.package_isp ?? pay?.details?.package_isp ?? 'unknown');
240+
// typed lookup to avoid TS indexing errors
241+
const iconSrc = (brandIcons as Record<string, string>)[isp];
242+
243+
return (
244+
<div
245+
key={id}
246+
role="listitem"
247+
className="mb-3 bg-white dark:bg-gray-800 rounded-md shadow-sm dark:shadow-white overflow-hidden">
248+
<div className="px-3 py-2 flex items-center justify-between gap-3">
249+
<div className="flex items-center gap-3 min-w-0">
250+
<div className="flex-shrink-0">
251+
{iconSrc ? (
252+
<div className="h-8 w-8 rounded-full bg-gray-100 dark:bg-gray-700 overflow-hidden flex items-center justify-center">
253+
<img src={iconSrc} alt={`${isp} logo`} className="h-full w-full object-cover" />
254+
</div>
255+
) : (
256+
<div className="h-8 w-8 rounded-full bg-gray-100 dark:bg-gray-700 flex items-center justify-center text-xs text-gray-600">
257+
TX
258+
</div>
274259
)}
275260
</div>
276-
</div>
277-
278-
{/* Collapsible details section */}
279-
<div className="px-3 pb-3">
280-
<div className="flex items-center justify-between text-[10px] text-gray-400 dark:text-gray-500 mb-1">
281-
<div className="flex items-center gap-3">
282-
{pkg && (
283-
<div className="flex items-center gap-2">
284-
<span>PACKAGE_BUY</span>
285-
<button
286-
type="button"
287-
className="text-xs text-blue-600 dark:text-blue-400"
288-
onClick={() =>
289-
setOpenSections((s) => ({
290-
...s,
291-
[id]: {
292-
pkg: !s[id]?.pkg,
293-
pay: s[id]?.pay ?? false
294-
}
295-
}))
296-
}>
297-
{openSections[id]?.pkg ? 'Hide' : 'Show'}
298-
</button>
299-
</div>
300-
)}
301-
302-
{pay && (
303-
<div className="flex items-center gap-2">
304-
<span>PAYMENT</span>
305-
<button
306-
type="button"
307-
className="text-xs text-blue-600 dark:text-blue-400"
308-
onClick={() =>
309-
setOpenSections((s) => ({
310-
...s,
311-
[id]: {
312-
pay: !s[id]?.pay,
313-
pkg: s[id]?.pkg ?? false
314-
}
315-
}))
316-
}>
317-
{openSections[id]?.pay ? 'Hide' : 'Show'}
318-
</button>
261+
<div className="min-w-0">
262+
<p className="text-sm font-medium text-gray-800 dark:text-gray-100 truncate">{id}</p>
263+
{(() => {
264+
const status = getTxStatus();
265+
return status ? (
266+
<div className="mt-1 flex items-center gap-2">
267+
<span className="text-xs text-gray-500 dark:text-gray-400">Status</span>
268+
<span className="text-xs px-2 py-0.5 rounded-full bg-gray-100 dark:bg-gray-900 text-gray-700 dark:text-gray-200">
269+
{status}
270+
</span>
319271
</div>
320-
)}
321-
</div>
272+
) : null;
273+
})()}
322274
</div>
275+
</div>
323276

324-
{openSections[id]?.pkg && (
325-
<pre className="text-xs text-gray-500 dark:text-gray-400 overflow-auto bg-gray-50 dark:bg-gray-900 p-2 rounded w-full whitespace-pre-wrap max-h-60 mb-2">
326-
{JSON.stringify(pkg, null, 2)}
327-
</pre>
277+
<div className="flex-shrink-0 text-right">
278+
{typeof amount === 'number' && (
279+
<p className="text-sm font-semibold text-green-600 dark:text-green-400">{Math.round(amount)}</p>
328280
)}
281+
{when && <p className="text-xs text-gray-400 dark:text-gray-500">{new Date(when).toLocaleString()}</p>}
282+
</div>
283+
</div>
329284

330-
{openSections[id]?.pay && (
331-
<pre className="text-xs text-gray-500 dark:text-gray-400 overflow-auto bg-gray-50 dark:bg-gray-900 p-2 rounded w-full whitespace-pre-wrap max-h-60">
332-
{JSON.stringify(pay, null, 2)}
333-
</pre>
334-
)}
285+
{/* Collapsible details section */}
286+
<div className="px-3 pb-3">
287+
<div className="flex items-center justify-between text-[10px] text-gray-400 dark:text-gray-500 mb-1">
288+
<div className="flex items-center gap-3">
289+
{pkg && (
290+
<div className="flex items-center gap-2">
291+
<span>PACKAGE_BUY</span>
292+
<button
293+
type="button"
294+
className="text-xs text-blue-600 dark:text-blue-400"
295+
onClick={() =>
296+
setOpenSections((s) => ({
297+
...s,
298+
[id]: {
299+
pkg: !s[id]?.pkg,
300+
pay: s[id]?.pay ?? false
301+
}
302+
}))
303+
}>
304+
{openSections[id]?.pkg ? 'Hide' : 'Show'}
305+
</button>
306+
</div>
307+
)}
308+
309+
{pay && (
310+
<div className="flex items-center gap-2">
311+
<span>PAYMENT</span>
312+
<button
313+
type="button"
314+
className="text-xs text-blue-600 dark:text-blue-400"
315+
onClick={() =>
316+
setOpenSections((s) => ({
317+
...s,
318+
[id]: {
319+
pay: !s[id]?.pay,
320+
pkg: s[id]?.pkg ?? false
321+
}
322+
}))
323+
}>
324+
{openSections[id]?.pay ? 'Hide' : 'Show'}
325+
</button>
326+
</div>
327+
)}
328+
</div>
335329
</div>
330+
331+
{openSections[id]?.pkg && (
332+
<pre className="text-xs text-gray-500 dark:text-gray-400 overflow-auto bg-gray-50 dark:bg-gray-900 p-2 rounded w-full whitespace-pre-wrap max-h-60 mb-2">
333+
{JSON.stringify(pkg, null, 2)}
334+
</pre>
335+
)}
336+
337+
{openSections[id]?.pay && (
338+
<pre className="text-xs text-gray-500 dark:text-gray-400 overflow-auto bg-gray-50 dark:bg-gray-900 p-2 rounded w-full whitespace-pre-wrap max-h-60">
339+
{JSON.stringify(pay, null, 2)}
340+
</pre>
341+
)}
336342
</div>
337-
);
338-
})}
343+
</div>
344+
);
345+
})}
339346
</div>
340347
);
341348
}

0 commit comments

Comments
 (0)