Skip to content

Commit c8499d2

Browse files
committed
rsolved reporting issues
1 parent a94c997 commit c8499d2

File tree

5 files changed

+397
-166
lines changed

5 files changed

+397
-166
lines changed

.env.example

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,8 @@ NEXT_PUBLIC_FULA_TOKEN_ADDRESS=0x9e12735d77c72c5C3670636D428f2F3815d8A4cB
66
# WalletConnect project ID — register at https://cloud.walletconnect.com
77
NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID=YOUR_PROJECT_ID
88

9+
# Block number when the RewardsProgram contract was deployed (avoids scanning empty blocks)
10+
NEXT_PUBLIC_DEPLOYMENT_BLOCK=0
11+
912
# Chain: "base" | "baseSepolia" | "hardhat"
1013
NEXT_PUBLIC_DEFAULT_CHAIN=base

.env.production

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,8 @@ NEXT_PUBLIC_FULA_TOKEN_ADDRESS=0x9e12735d77c72c5C3670636D428f2F3815d8A4cB
66
# WalletConnect
77
NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID=192a8f5e8d1742ea923be485e60f2612
88

9+
# Deployment block (avoids scanning empty blocks)
10+
NEXT_PUBLIC_DEPLOYMENT_BLOCK=44007100
11+
912
# Chain
1013
NEXT_PUBLIC_DEFAULT_CHAIN=base

src/app/reports/page.tsx

Lines changed: 106 additions & 166 deletions
Original file line numberDiff line numberDiff line change
@@ -1,158 +1,47 @@
11
"use client";
22

3-
import { useState, useEffect, useCallback } from "react";
3+
import { useState } from "react";
44
import {
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";
1313
import { 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-
2715
export 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>

src/config/contracts.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@ export const MemberTypeLabels: Record<number, string> = {
3737
3: "PS Partner",
3838
};
3939

40+
// Deployment block — avoids scanning empty blocks before the contract existed
41+
export const DEPLOYMENT_BLOCK = BigInt(process.env.NEXT_PUBLIC_DEPLOYMENT_BLOCK || "0");
42+
4043
// RewardsProgram ABI (minimal - key functions only)
4144
export const REWARDS_PROGRAM_ABI = [
4245
// Read functions

0 commit comments

Comments
 (0)