11"use client" ;
22
3- import { useState , useEffect , useCallback } from "react" ;
3+ import { useState } from "react" ;
44import {
55 Typography , Box , Paper , Grid , Table , TableBody , TableCell ,
66 TableContainer , TableHead , TableRow , TextField , Button ,
77 Select , MenuItem , FormControl , InputLabel , Alert , Chip ,
8+ LinearProgress , TablePagination , useMediaQuery , useTheme ,
89} from "@mui/material" ;
9- import { usePublicClient } from "wagmi" ;
10- import { parseAbiItem } from "viem" ;
11- import { CONTRACTS , MemberTypeLabels } from "@/config/contracts" ;
12- import { useProgramCount , useRewardTypes } from "@/hooks/useRewardsProgram" ;
10+ import { CONTRACTS } from "@/config/contracts" ;
11+ import { useRewardTypes } from "@/hooks/useRewardsProgram" ;
12+ import { useChunkedEventLogs , type TimeRange } from "@/hooks/useChunkedEventLogs" ;
1313import { formatFula , shortenAddress , fromBytes16 } from "@/lib/utils" ;
1414
15- type EventRow = {
16- type : string ;
17- depositId ?: string ;
18- programId : number ;
19- wallet : string ;
20- amount : bigint ;
21- rewardType ?: number ;
22- note ?: string ;
23- blockNumber : bigint ;
24- txHash : string ;
25- } ;
26-
2715export default function ReportsPage ( ) {
28- const publicClient = usePublicClient ( ) ;
29- const { data : programCount } = useProgramCount ( ) ;
16+ const theme = useTheme ( ) ;
17+ const isMobile = useMediaQuery ( theme . breakpoints . down ( "sm" ) ) ;
3018 const { data : rewardTypesData } = useRewardTypes ( ) ;
3119
3220 const [ filterProgramId , setFilterProgramId ] = useState ( "" ) ;
33- const [ filterMemberType , setFilterMemberType ] = useState < number | "" > ( "" ) ;
3421 const [ filterRewardType , setFilterRewardType ] = useState < number | "" > ( "" ) ;
35- const [ events , setEvents ] = useState < EventRow [ ] > ( [ ] ) ;
36- const [ loading , setLoading ] = useState ( false ) ;
37- const [ error , setError ] = useState ( "" ) ;
38- const [ fetched , setFetched ] = useState ( false ) ;
22+ const [ timeRange , setTimeRange ] = useState < TimeRange > ( "7d" ) ;
23+ const [ trigger , setTrigger ] = useState ( 0 ) ;
24+ const [ page , setPage ] = useState ( 0 ) ;
25+ const [ rowsPerPage , setRowsPerPage ] = useState ( 25 ) ;
26+
27+ const {
28+ events, loading, progress, totalChunks, completedChunks, error, cancel,
29+ } = useChunkedEventLogs ( {
30+ address : CONTRACTS . rewardsProgram ,
31+ programId : filterProgramId ? Number ( filterProgramId ) : undefined ,
32+ timeRange,
33+ trigger,
34+ } ) ;
35+
36+ // Client-side reward type filter
37+ const filteredEvents = filterRewardType !== ""
38+ ? events . filter ( r => r . type !== "Deposit" || r . rewardType === filterRewardType )
39+ : events ;
3940
4041 // Summary stats
41- const totalDeposits = events . filter ( e => e . type === "Deposit" ) . reduce ( ( s , e ) => s + e . amount , BigInt ( 0 ) ) ;
42- const totalTransfers = events . filter ( e => e . type === "Transfer" || e . type === "TransferToParent" ) . reduce ( ( s , e ) => s + e . amount , BigInt ( 0 ) ) ;
43- const totalWithdrawals = events . filter ( e => e . type === "Withdrawal" ) . reduce ( ( s , e ) => s + e . amount , BigInt ( 0 ) ) ;
44-
45- const fetchEvents = useCallback ( async ( ) => {
46- if ( ! publicClient ) return ;
47- setLoading ( true ) ;
48- setError ( "" ) ;
49- setFetched ( true ) ;
50-
51- try {
52- const pid = filterProgramId ? BigInt ( filterProgramId ) : undefined ;
53- const address = CONTRACTS . rewardsProgram ;
54-
55- // Fetch all event types in parallel
56- const [ deposits , transfers , parentTransfers , withdrawals ] = await Promise . all ( [
57- publicClient . getLogs ( {
58- address,
59- event : parseAbiItem ( "event TokensDeposited(uint256 indexed depositId, uint32 indexed programId, address indexed wallet, uint256 amount, uint8 rewardType, string note)" ) ,
60- args : pid ? { programId : Number ( pid ) } : undefined ,
61- fromBlock : BigInt ( 0 ) ,
62- toBlock : "latest" ,
63- } ) ,
64- publicClient . getLogs ( {
65- address,
66- event : parseAbiItem ( "event TokensTransferredToMember(uint32 indexed programId, address indexed from, address indexed to, uint256 amount, bool locked, uint32 lockTimeDays)" ) ,
67- args : pid ? { programId : Number ( pid ) } : undefined ,
68- fromBlock : BigInt ( 0 ) ,
69- toBlock : "latest" ,
70- } ) ,
71- publicClient . getLogs ( {
72- address,
73- event : parseAbiItem ( "event TokensTransferredToParent(uint32 indexed programId, address indexed from, address indexed to, uint256 amount)" ) ,
74- args : pid ? { programId : Number ( pid ) } : undefined ,
75- fromBlock : BigInt ( 0 ) ,
76- toBlock : "latest" ,
77- } ) ,
78- publicClient . getLogs ( {
79- address,
80- event : parseAbiItem ( "event TokensWithdrawn(uint32 indexed programId, address indexed wallet, uint256 amount)" ) ,
81- args : pid ? { programId : Number ( pid ) } : undefined ,
82- fromBlock : BigInt ( 0 ) ,
83- toBlock : "latest" ,
84- } ) ,
85- ] ) ;
86-
87- const rows : EventRow [ ] = [ ] ;
88-
89- for ( const log of deposits ) {
90- if ( ! log . args ) continue ;
91- rows . push ( {
92- type : "Deposit" ,
93- depositId : log . args . depositId ?. toString ( ) ,
94- programId : Number ( log . args . programId ) ,
95- wallet : log . args . wallet || "" ,
96- amount : log . args . amount || BigInt ( 0 ) ,
97- rewardType : log . args . rewardType ,
98- note : log . args . note ,
99- blockNumber : log . blockNumber ,
100- txHash : log . transactionHash ,
101- } ) ;
102- }
103-
104- for ( const log of transfers ) {
105- if ( ! log . args ) continue ;
106- rows . push ( {
107- type : "Transfer" ,
108- programId : Number ( log . args . programId ) ,
109- wallet : log . args . from || "" ,
110- amount : log . args . amount || BigInt ( 0 ) ,
111- blockNumber : log . blockNumber ,
112- txHash : log . transactionHash ,
113- } ) ;
114- }
115-
116- for ( const log of parentTransfers ) {
117- if ( ! log . args ) continue ;
118- rows . push ( {
119- type : "TransferToParent" ,
120- programId : Number ( log . args . programId ) ,
121- wallet : log . args . from || "" ,
122- amount : log . args . amount || BigInt ( 0 ) ,
123- blockNumber : log . blockNumber ,
124- txHash : log . transactionHash ,
125- } ) ;
126- }
127-
128- for ( const log of withdrawals ) {
129- if ( ! log . args ) continue ;
130- rows . push ( {
131- type : "Withdrawal" ,
132- programId : Number ( log . args . programId ) ,
133- wallet : log . args . wallet || "" ,
134- amount : log . args . amount || BigInt ( 0 ) ,
135- blockNumber : log . blockNumber ,
136- txHash : log . transactionHash ,
137- } ) ;
138- }
139-
140- // Filter by reward type
141- let filtered = rows ;
142- if ( filterRewardType !== "" ) {
143- filtered = filtered . filter ( r => r . type !== "Deposit" || r . rewardType === filterRewardType ) ;
144- }
145-
146- // Sort by block number descending
147- filtered . sort ( ( a , b ) => Number ( b . blockNumber - a . blockNumber ) ) ;
148-
149- setEvents ( filtered ) ;
150- } catch ( err ) {
151- setError ( err instanceof Error ? err . message : "Failed to fetch events" ) ;
152- } finally {
153- setLoading ( false ) ;
154- }
155- } , [ publicClient , filterProgramId , filterRewardType ] ) ;
42+ const totalDeposits = filteredEvents . filter ( e => e . type === "Deposit" ) . reduce ( ( s , e ) => s + e . amount , BigInt ( 0 ) ) ;
43+ const totalTransfers = filteredEvents . filter ( e => e . type === "Transfer" || e . type === "TransferToParent" ) . reduce ( ( s , e ) => s + e . amount , BigInt ( 0 ) ) ;
44+ const totalWithdrawals = filteredEvents . filter ( e => e . type === "Withdrawal" ) . reduce ( ( s , e ) => s + e . amount , BigInt ( 0 ) ) ;
15645
15746 const rewardTypeNames : Record < number , string > = { } ;
15847 if ( rewardTypesData ) {
@@ -162,6 +51,21 @@ export default function ReportsPage() {
16251 } ) ;
16352 }
16453
54+ const handleGenerate = ( ) => {
55+ if ( loading ) {
56+ cancel ( ) ;
57+ } else {
58+ setPage ( 0 ) ;
59+ setTrigger ( t => t + 1 ) ;
60+ }
61+ } ;
62+
63+ // Pagination
64+ const paginatedEvents = filteredEvents . slice (
65+ page * rowsPerPage ,
66+ page * rowsPerPage + rowsPerPage
67+ ) ;
68+
16569 return (
16670 < Box >
16771 < Typography variant = "h4" gutterBottom > Reports</ Typography >
@@ -176,12 +80,12 @@ export default function ReportsPage() {
17680 </ Grid >
17781 < Grid item xs = { 12 } sm = { 3 } >
17882 < FormControl fullWidth size = "small" >
179- < InputLabel > Member Type </ InputLabel >
180- < Select value = { filterMemberType } onChange = { ( e ) => setFilterMemberType ( e . target . value as number | "" ) } label = "Member Type " >
181- < MenuItem value = "" > All </ MenuItem >
182- { Object . entries ( MemberTypeLabels ) . map ( ( [ k , v ] ) => (
183- < MenuItem key = { k } value = { Number ( k ) } > { v } </ MenuItem >
184- ) ) }
83+ < InputLabel > Time Range </ InputLabel >
84+ < Select value = { timeRange } onChange = { ( e ) => setTimeRange ( e . target . value as TimeRange ) } label = "Time Range " >
85+ < MenuItem value = "7d" > Last 7 days </ MenuItem >
86+ < MenuItem value = "30d" > Last 30 days </ MenuItem >
87+ < MenuItem value = "90d" > Last 90 days </ MenuItem >
88+ < MenuItem value = "all" > All time </ MenuItem >
18589 </ Select >
18690 </ FormControl >
18791 </ Grid >
@@ -197,37 +101,60 @@ export default function ReportsPage() {
197101 </ FormControl >
198102 </ Grid >
199103 < Grid item xs = { 12 } sm = { 3 } >
200- < Button variant = "contained" onClick = { fetchEvents } disabled = { loading } fullWidth >
201- { loading ? "Loading..." : "Generate Report" }
104+ < Button
105+ variant = "contained"
106+ onClick = { handleGenerate }
107+ fullWidth
108+ color = { loading ? "error" : "primary" }
109+ >
110+ { loading ? "Cancel" : "Generate Report" }
202111 </ Button >
203112 </ Grid >
204113 </ Grid >
205114 </ Paper >
206115
116+ { loading && (
117+ < Paper sx = { { p : 2 , mb : 3 } } >
118+ < Box sx = { { display : "flex" , justifyContent : "space-between" , mb : 1 } } >
119+ < Typography variant = "body2" > Fetching events...</ Typography >
120+ < Typography variant = "body2" color = "text.secondary" >
121+ { completedChunks } / { totalChunks } chunks
122+ </ Typography >
123+ </ Box >
124+ < LinearProgress variant = "determinate" value = { progress * 100 } />
125+ </ Paper >
126+ ) }
127+
207128 { error && < Alert severity = "error" sx = { { mb : 3 } } > { error } </ Alert > }
208129
209- { fetched && events . length > 0 && (
130+ { filteredEvents . length > 0 && (
210131 < >
211132 < Grid container spacing = { 2 } sx = { { mb : 3 } } >
212133 < Grid item xs = { 12 } sm = { 4 } >
213134 < Paper sx = { { p : 2 , textAlign : "center" } } >
214135 < Typography color = "text.secondary" variant = "body2" > Total Deposits</ Typography >
215136 < Typography variant = "h6" color = "success.main" > { formatFula ( totalDeposits ) } FULA</ Typography >
216- < Typography variant = "caption" color = "text.secondary" > { events . filter ( e => e . type === "Deposit" ) . length } transactions</ Typography >
137+ < Typography variant = "caption" color = "text.secondary" >
138+ { filteredEvents . filter ( e => e . type === "Deposit" ) . length } transactions
139+ </ Typography >
217140 </ Paper >
218141 </ Grid >
219142 < Grid item xs = { 12 } sm = { 4 } >
220143 < Paper sx = { { p : 2 , textAlign : "center" } } >
221144 < Typography color = "text.secondary" variant = "body2" > Total Transfers</ Typography >
222145 < Typography variant = "h6" color = "info.main" > { formatFula ( totalTransfers ) } FULA</ Typography >
223- < Typography variant = "caption" color = "text.secondary" > { events . filter ( e => e . type === "Transfer" || e . type === "TransferToParent" ) . length } transactions</ Typography >
146+ < Typography variant = "caption" color = "text.secondary" >
147+ { filteredEvents . filter ( e => e . type === "Transfer" || e . type === "TransferToParent" ) . length } transactions
148+ </ Typography >
224149 </ Paper >
225150 </ Grid >
226151 < Grid item xs = { 12 } sm = { 4 } >
227152 < Paper sx = { { p : 2 , textAlign : "center" } } >
228153 < Typography color = "text.secondary" variant = "body2" > Total Withdrawals</ Typography >
229154 < Typography variant = "h6" color = "warning.main" > { formatFula ( totalWithdrawals ) } FULA</ Typography >
230- < Typography variant = "caption" color = "text.secondary" > { events . filter ( e => e . type === "Withdrawal" ) . length } transactions</ Typography >
155+ < Typography variant = "caption" color = "text.secondary" >
156+ { filteredEvents . filter ( e => e . type === "Withdrawal" ) . length } transactions
157+ </ Typography >
231158 </ Paper >
232159 </ Grid >
233160 </ Grid >
@@ -241,13 +168,13 @@ export default function ReportsPage() {
241168 < TableCell > Wallet</ TableCell >
242169 < TableCell > Amount (FULA)</ TableCell >
243170 < TableCell > Reward Type</ TableCell >
244- < TableCell > Note</ TableCell >
245- < TableCell > Block</ TableCell >
171+ { ! isMobile && < TableCell > Note</ TableCell > }
172+ { ! isMobile && < TableCell > Block</ TableCell > }
246173 </ TableRow >
247174 </ TableHead >
248175 < TableBody >
249- { events . slice ( 0 , 100 ) . map ( ( row , i ) => (
250- < TableRow key = { i } hover >
176+ { paginatedEvents . map ( ( row , i ) => (
177+ < TableRow key = { ` ${ row . txHash } - ${ i } ` } hover >
251178 < TableCell >
252179 < Chip
253180 label = { row . type }
@@ -256,27 +183,40 @@ export default function ReportsPage() {
256183 />
257184 </ TableCell >
258185 < TableCell > { row . programId } </ TableCell >
259- < TableCell sx = { { fontFamily : "monospace" , fontSize : "0.85rem" } } > { shortenAddress ( row . wallet ) } </ TableCell >
186+ < TableCell sx = { { fontFamily : "monospace" , fontSize : "0.85rem" } } >
187+ { shortenAddress ( row . wallet ) }
188+ </ TableCell >
260189 < TableCell > { formatFula ( row . amount ) } </ TableCell >
261- < TableCell > { row . rewardType !== undefined ? ( rewardTypeNames [ row . rewardType ] || row . rewardType ) : "-" } </ TableCell >
262- < TableCell sx = { { maxWidth : 200 , overflow : "hidden" , textOverflow : "ellipsis" , whiteSpace : "nowrap" } } >
263- { row . note || "-" }
190+ < TableCell >
191+ { row . rewardType !== undefined ? ( rewardTypeNames [ row . rewardType ] || row . rewardType ) : "-" }
264192 </ TableCell >
265- < TableCell > { row . blockNumber . toString ( ) } </ TableCell >
193+ { ! isMobile && (
194+ < TableCell sx = { { maxWidth : 200 , overflow : "hidden" , textOverflow : "ellipsis" , whiteSpace : "nowrap" } } >
195+ { row . note || "-" }
196+ </ TableCell >
197+ ) }
198+ { ! isMobile && < TableCell > { row . blockNumber . toString ( ) } </ TableCell > }
266199 </ TableRow >
267200 ) ) }
268201 </ TableBody >
269202 </ Table >
203+ < TablePagination
204+ component = "div"
205+ count = { filteredEvents . length }
206+ page = { page }
207+ onPageChange = { ( _ , p ) => setPage ( p ) }
208+ rowsPerPage = { rowsPerPage }
209+ onRowsPerPageChange = { ( e ) => {
210+ setRowsPerPage ( parseInt ( e . target . value , 10 ) ) ;
211+ setPage ( 0 ) ;
212+ } }
213+ rowsPerPageOptions = { [ 10 , 25 , 50 , 100 ] }
214+ />
270215 </ TableContainer >
271- { events . length > 100 && (
272- < Typography variant = "caption" color = "text.secondary" sx = { { mt : 1 , display : "block" } } >
273- Showing first 100 of { events . length } events.
274- </ Typography >
275- ) }
276216 </ >
277217 ) }
278218
279- { fetched && events . length === 0 && ! loading && ! error && (
219+ { trigger > 0 && ! loading && filteredEvents . length === 0 && ! error && (
280220 < Alert severity = "info" > No events found for the selected filters.</ Alert >
281221 ) }
282222 </ Box >
0 commit comments