@@ -14,7 +14,7 @@ import BlockIcon from "@mui/icons-material/Block";
1414import ExpandMoreIcon from "@mui/icons-material/ExpandMore" ;
1515import DeleteIcon from "@mui/icons-material/Delete" ;
1616import ContentCopyIcon from "@mui/icons-material/ContentCopy" ;
17- import { useAccount , useReadContract } from "wagmi" ;
17+ import { useAccount } from "wagmi" ;
1818import { useSearchParams } from "next/navigation" ;
1919import { keccak256 } from "viem" ;
2020import Link from "next/link" ;
@@ -26,8 +26,10 @@ import {
2626 useUpdateProgram , useDeactivateProgram ,
2727 useAddRewardType , useRemoveRewardType , useRewardTypes ,
2828 useAddSubType , useRemoveSubType , useSubTypes ,
29+ useDepositTokens , useTransferToSubMember , useTransferToParent , useWithdraw ,
30+ useTokenBalance ,
2931} from "@/hooks/useRewardsProgram" ;
30- import { CONTRACTS , REWARDS_PROGRAM_ABI , MemberRoleLabels , MemberRoleEnum , MemberTypeLabels } from "@/config/contracts" ;
32+ import { MemberRoleLabels , MemberRoleEnum , MemberTypeLabels } from "@/config/contracts" ;
3133import { fromBytes8 , fromBytes12 , fromBytes16 , shortenAddress , formatFula , isValidAddress , formatContractError } from "@/lib/utils" ;
3234import { OnChainDisclaimer } from "@/components/common/OnChainDisclaimer" ;
3335import { QRCodeDisplay } from "@/components/common/QRCodeDisplay" ;
@@ -41,16 +43,10 @@ function generateEditCode(): `0x${string}` {
4143
4244function ProgramRow ( { programId, filterMine, wallet, isAdmin } : { programId : number ; filterMine ?: boolean ; wallet ?: `0x${string } `; isAdmin ?: boolean } ) {
4345 const { data : program } = useProgram ( programId ) ;
44- const { data : member } = useReadContract ( {
45- address : CONTRACTS . rewardsProgram ,
46- abi : REWARDS_PROGRAM_ABI ,
47- functionName : "getMember" ,
48- args : wallet ? [ programId , wallet ] : undefined ,
49- query : { enabled : ! ! filterMine && ! isAdmin && ! ! wallet } ,
50- } ) ;
46+ const { role, isActive } = useMemberRole ( programId ) ;
5147
5248 if ( ! program ) return null ;
53- if ( filterMine && ! isAdmin && ( ! member || ! member . active ) ) return null ;
49+ if ( filterMine && ! isAdmin && ( ! isActive || role === 0 ) ) return null ;
5450
5551 return (
5652 < TableRow hover >
@@ -289,6 +285,27 @@ function ProgramDetail({ programId }: { programId: number }) {
289285 const [ copied , setCopied ] = useState ( false ) ;
290286 const [ copiedUrl , setCopiedUrl ] = useState ( false ) ;
291287
288+ // Token Operations
289+ const { data : walletBalance } = useTokenBalance ( address ) ;
290+ const { deposit : depositTokens , isApproving : isDepApproving , isDepositing : isDepDepositing , isPending : isDepPending , isSuccess : depSuccess , error : depError , reset : resetDep } = useDepositTokens ( ) ;
291+ const { transfer, isPending : isTransPending , isConfirming : isTransConf , isSuccess : transSuccess , error : transError } = useTransferToSubMember ( ) ;
292+ const { transferBack, isPending : isTransBackPending , isConfirming : isTransBackConf , isSuccess : transBackSuccess , error : transBackError } = useTransferToParent ( ) ;
293+ const { withdraw, isPending : isWithPending , isConfirming : isWithConf , isSuccess : withSuccess , error : withError } = useWithdraw ( ) ;
294+ const [ depAmount , setDepAmount ] = useState ( "" ) ;
295+ const [ depNote , setDepNote ] = useState ( "" ) ;
296+ const [ depDisclaimer , setDepDisclaimer ] = useState ( false ) ;
297+ const [ transTo , setTransTo ] = useState ( "" ) ;
298+ const [ transAmount , setTransAmount ] = useState ( "" ) ;
299+ const [ transLocked , setTransLocked ] = useState ( true ) ;
300+ const [ transLockDays , setTransLockDays ] = useState ( "0" ) ;
301+ const [ transDisclaimer , setTransDisclaimer ] = useState ( false ) ;
302+ const [ parentTo , setParentTo ] = useState ( "" ) ;
303+ const [ parentAmount , setParentAmount ] = useState ( "" ) ;
304+ const [ parentDisclaimer , setParentDisclaimer ] = useState ( false ) ;
305+ const [ withAmount , setWithAmount ] = useState ( "" ) ;
306+ const [ withDisclaimer , setWithDisclaimer ] = useState ( false ) ;
307+ const isMember = role > 0 || isAdmin ;
308+
292309 const paWalletValid = ! paWallet || isValidAddress ( paWallet ) ;
293310 const mWalletValid = ! mWallet || isValidAddress ( mWallet ) ;
294311
@@ -467,6 +484,107 @@ function ProgramDetail({ programId }: { programId: number }) {
467484 </ Paper >
468485 ) }
469486
487+ { /* Token Actions */ }
488+ { isMember && program . active && (
489+ < Paper sx = { { p : 2 , mb : 3 } } >
490+ < Typography variant = "h6" gutterBottom > Token Actions</ Typography >
491+ { walletBalance != null && (
492+ < Typography variant = "body2" color = "text.secondary" sx = { { mb : 2 } } >
493+ Wallet Balance: { formatFula ( walletBalance ) } FULA
494+ </ Typography >
495+ ) }
496+ < Grid container spacing = { 3 } >
497+ { /* Deposit */ }
498+ < Grid item xs = { 12 } sm = { 6 } md = { 3 } >
499+ < Typography variant = "subtitle2" gutterBottom > Deposit</ Typography >
500+ < TextField label = "Amount (FULA)" value = { depAmount } onChange = { ( e ) => setDepAmount ( e . target . value ) }
501+ fullWidth size = "small" type = "number" />
502+ < TextField label = "Note (max 128)" value = { depNote }
503+ onChange = { ( e ) => setDepNote ( e . target . value . slice ( 0 , 128 ) ) }
504+ fullWidth size = "small" sx = { { mt : 1 } } inputProps = { { maxLength : 128 } } />
505+ < OnChainDisclaimer accepted = { depDisclaimer } onChange = { setDepDisclaimer } />
506+ < Button size = "small" variant = "contained" fullWidth sx = { { mt : 1 } }
507+ onClick = { ( ) => depositTokens ( programId , depAmount , 0 , depNote ) }
508+ disabled = { isDepPending || ! depAmount || ! depDisclaimer } >
509+ { isDepApproving ? < > < CircularProgress size = { 16 } sx = { { mr : 0.5 } } /> Approving...</ >
510+ : isDepDepositing ? < > < CircularProgress size = { 16 } sx = { { mr : 0.5 } } /> Depositing...</ >
511+ : "Deposit" }
512+ </ Button >
513+ { depSuccess && < Alert severity = "success" sx = { { mt : 1 } } > Deposited!</ Alert > }
514+ { depError && < Alert severity = "error" sx = { { mt : 1 } } > { formatContractError ( depError ) } </ Alert > }
515+ </ Grid >
516+
517+ { /* Transfer to Sub-Member */ }
518+ { ( isAdmin || isPA || role === MemberRoleEnum . TeamLeader ) && (
519+ < Grid item xs = { 12 } sm = { 6 } md = { 3 } >
520+ < Typography variant = "subtitle2" gutterBottom > Transfer to Sub-Member</ Typography >
521+ < TextField label = "Recipient Wallet" value = { transTo } onChange = { ( e ) => setTransTo ( e . target . value ) }
522+ fullWidth size = "small" error = { ! ! transTo && ! isValidAddress ( transTo ) } />
523+ < TextField label = "Amount (FULA)" value = { transAmount } onChange = { ( e ) => setTransAmount ( e . target . value ) }
524+ fullWidth size = "small" type = "number" sx = { { mt : 1 } } />
525+ < FormControl fullWidth size = "small" sx = { { mt : 1 } } >
526+ < InputLabel > Lock</ InputLabel >
527+ < Select value = { transLocked ? "locked" : "unlocked" }
528+ onChange = { ( e ) => setTransLocked ( e . target . value === "locked" ) } label = "Lock" >
529+ < MenuItem value = "locked" > Permanently Locked</ MenuItem >
530+ < MenuItem value = "unlocked" > Unlocked / Time-locked</ MenuItem >
531+ </ Select >
532+ </ FormControl >
533+ { ! transLocked && (
534+ < TextField label = "Lock Days (0 = unlocked)" value = { transLockDays }
535+ onChange = { ( e ) => setTransLockDays ( e . target . value ) }
536+ fullWidth size = "small" type = "number" sx = { { mt : 1 } } />
537+ ) }
538+ < OnChainDisclaimer accepted = { transDisclaimer } onChange = { setTransDisclaimer } />
539+ < Button size = "small" variant = "contained" fullWidth sx = { { mt : 1 } }
540+ onClick = { ( ) => transfer ( programId , transTo as `0x${string } `, transAmount , transLocked , parseInt ( transLockDays ) || 0 ) }
541+ disabled = { isTransPending || isTransConf || ! transTo || ! transAmount || ! isValidAddress ( transTo ) || ! transDisclaimer } >
542+ { isTransPending || isTransConf ? < CircularProgress size = { 16 } /> : "Transfer" }
543+ </ Button >
544+ { transSuccess && < Alert severity = "success" sx = { { mt : 1 } } > Transferred!</ Alert > }
545+ { transError && < Alert severity = "error" sx = { { mt : 1 } } > { formatContractError ( transError ) } </ Alert > }
546+ </ Grid >
547+ ) }
548+
549+ { /* Transfer to Parent */ }
550+ < Grid item xs = { 12 } sm = { 6 } md = { 3 } >
551+ < Typography variant = "subtitle2" gutterBottom > Transfer to Parent</ Typography >
552+ { transferLimit != null && Number ( transferLimit ) > 0 && (
553+ < Alert severity = "info" sx = { { mb : 1 } } > Limit: { String ( transferLimit ) } % of total balance</ Alert >
554+ ) }
555+ < TextField label = "Parent Wallet (empty = direct parent)" value = { parentTo }
556+ onChange = { ( e ) => setParentTo ( e . target . value ) }
557+ fullWidth size = "small" error = { ! ! parentTo && ! isValidAddress ( parentTo ) } />
558+ < TextField label = "Amount (FULA)" value = { parentAmount } onChange = { ( e ) => setParentAmount ( e . target . value ) }
559+ fullWidth size = "small" type = "number" sx = { { mt : 1 } } />
560+ < OnChainDisclaimer accepted = { parentDisclaimer } onChange = { setParentDisclaimer } />
561+ < Button size = "small" variant = "contained" fullWidth sx = { { mt : 1 } }
562+ onClick = { ( ) => transferBack ( programId , ( parentTo || "0x0000000000000000000000000000000000000000" ) as `0x${string } `, parentAmount ) }
563+ disabled = { isTransBackPending || isTransBackConf || ! parentAmount || ! parentDisclaimer } >
564+ { isTransBackPending || isTransBackConf ? < CircularProgress size = { 16 } /> : "Transfer to Parent" }
565+ </ Button >
566+ { transBackSuccess && < Alert severity = "success" sx = { { mt : 1 } } > Transferred!</ Alert > }
567+ { transBackError && < Alert severity = "error" sx = { { mt : 1 } } > { formatContractError ( transBackError ) } </ Alert > }
568+ </ Grid >
569+
570+ { /* Withdraw */ }
571+ < Grid item xs = { 12 } sm = { 6 } md = { 3 } >
572+ < Typography variant = "subtitle2" gutterBottom > Withdraw</ Typography >
573+ < TextField label = "Amount (FULA)" value = { withAmount } onChange = { ( e ) => setWithAmount ( e . target . value ) }
574+ fullWidth size = "small" type = "number" />
575+ < OnChainDisclaimer accepted = { withDisclaimer } onChange = { setWithDisclaimer } />
576+ < Button size = "small" variant = "contained" fullWidth sx = { { mt : 1 } }
577+ onClick = { ( ) => withdraw ( programId , withAmount ) }
578+ disabled = { isWithPending || isWithConf || ! withAmount || ! withDisclaimer } >
579+ { isWithPending || isWithConf ? < CircularProgress size = { 16 } /> : "Withdraw" }
580+ </ Button >
581+ { withSuccess && < Alert severity = "success" sx = { { mt : 1 } } > Withdrawn!</ Alert > }
582+ { withError && < Alert severity = "error" sx = { { mt : 1 } } > { formatContractError ( withError ) } </ Alert > }
583+ </ Grid >
584+ </ Grid >
585+ </ Paper >
586+ ) }
587+
470588 { /* Reward Types & Sub-Types */ }
471589 { ( isAdmin || canManageSubTypes ) && program . active && (
472590 < Paper sx = { { p : 2 , mb : 3 } } >
0 commit comments