@@ -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