Skip to content

Commit 43290ce

Browse files
committed
feat: add ROI percentage update functionality to campaign management
Add UpdateRoiDialog component with form to modify vault ROI percentage. Integrate dialog into RoiView with new state management in useRoi hook. Add dropdown menu to roi-table-row with actions for managing loans, uploading funds, and updating ROI. Implement useUpdateRoiPercentage hook for transaction signing and submission. Add getRoiPercentage and updateRoiPorcentage API endpoints.
1 parent f376450 commit 43290ce

File tree

13 files changed

+1176
-143
lines changed

13 files changed

+1176
-143
lines changed
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
"use client";
2+
3+
import { useEffect, useState } from "react";
4+
import {
5+
Dialog,
6+
DialogContent,
7+
DialogHeader,
8+
DialogTitle,
9+
DialogDescription,
10+
} from "@tokenization/ui/dialog";
11+
import { Button } from "@tokenization/ui/button";
12+
import { Input } from "@tokenization/ui/input";
13+
import { Label } from "@tokenization/ui/label";
14+
import { Loader2 } from "lucide-react";
15+
import { useWalletContext } from "@tokenization/tw-blocks-shared/src/wallet-kit/WalletProvider";
16+
import { useUpdateRoiPercentage } from "@/features/campaigns/hooks/useUpdateRoiPercentage";
17+
import { getRoiPercentage } from "@/features/campaigns/services/campaigns.api";
18+
import { toast } from "sonner";
19+
20+
interface UpdateRoiDialogProps {
21+
open: boolean;
22+
onOpenChange: (open: boolean) => void;
23+
campaignName: string;
24+
vaultId: string;
25+
onUpdated: () => void;
26+
}
27+
28+
export function UpdateRoiDialog({
29+
open,
30+
onOpenChange,
31+
campaignName,
32+
vaultId,
33+
onUpdated,
34+
}: UpdateRoiDialogProps) {
35+
const [percentage, setPercentage] = useState("");
36+
const [isLoadingCurrent, setIsLoadingCurrent] = useState(false);
37+
const { walletAddress } = useWalletContext();
38+
39+
useEffect(() => {
40+
if (!open || !walletAddress) return;
41+
setIsLoadingCurrent(true);
42+
getRoiPercentage(vaultId, walletAddress)
43+
.then(({ roiPercentage }) => setPercentage(roiPercentage))
44+
.catch(() => setPercentage(""))
45+
.finally(() => setIsLoadingCurrent(false));
46+
}, [open, vaultId, walletAddress]);
47+
48+
const { execute, isSubmitting, error } = useUpdateRoiPercentage({
49+
onSuccess: () => {
50+
toast.success("ROI percentage updated successfully");
51+
onUpdated();
52+
onOpenChange(false);
53+
setPercentage("");
54+
},
55+
});
56+
57+
const handleSubmit = (e: React.FormEvent) => {
58+
e.preventDefault();
59+
const parsed = Number(percentage);
60+
if (!Number.isFinite(parsed) || parsed < 0 || parsed > 100) return;
61+
execute(vaultId, parsed);
62+
};
63+
64+
return (
65+
<Dialog open={open} onOpenChange={onOpenChange}>
66+
<DialogContent className="w-full! sm:max-w-lg!">
67+
<DialogHeader>
68+
<DialogTitle>Update ROI Percentage — {campaignName}</DialogTitle>
69+
<DialogDescription>
70+
Set a new ROI percentage for this campaign&apos;s vault. Value must be between 0 and 100.
71+
</DialogDescription>
72+
</DialogHeader>
73+
74+
<form onSubmit={handleSubmit} className="flex flex-col gap-4">
75+
<div className="space-y-2">
76+
<Label htmlFor="roiPercentage">ROI Percentage (%)</Label>
77+
<Input
78+
id="roiPercentage"
79+
type="number"
80+
step="0.01"
81+
min="0"
82+
max="100"
83+
placeholder="e.g. 12"
84+
value={percentage}
85+
onChange={(e) => setPercentage(e.target.value)}
86+
disabled={isSubmitting || isLoadingCurrent}
87+
autoComplete="off"
88+
/>
89+
</div>
90+
91+
<p className="text-xs text-muted-foreground">
92+
Vault:{" "}
93+
<span className="font-mono text-foreground">{vaultId}</span>
94+
</p>
95+
96+
{error ? (
97+
<p className="text-sm text-destructive">{error}</p>
98+
) : null}
99+
100+
<Button
101+
type="submit"
102+
disabled={isSubmitting || isLoadingCurrent || !percentage}
103+
className="w-full cursor-pointer"
104+
>
105+
{isSubmitting ? (
106+
<div className="flex items-center justify-center gap-2">
107+
<Loader2 className="h-4 w-4 animate-spin" />
108+
<span>Updating ROI...</span>
109+
</div>
110+
) : (
111+
"Update ROI Percentage"
112+
)}
113+
</Button>
114+
</form>
115+
</DialogContent>
116+
</Dialog>
117+
);
118+
}

apps/backoffice-tokenization/src/features/campaigns/components/roi/roi-table-row.tsx

Lines changed: 50 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,23 @@ import Link from "next/link";
55
import { TableCell, TableRow } from "@tokenization/ui/table";
66
import { Badge } from "@tokenization/ui/badge";
77
import { Button } from "@tokenization/ui/button";
8+
import {
9+
DropdownMenu,
10+
DropdownMenuTrigger,
11+
DropdownMenuContent,
12+
DropdownMenuItem,
13+
DropdownMenuSeparator,
14+
} from "@tokenization/ui/dropdown-menu";
815
import { cn } from "@tokenization/shared/lib/utils";
9-
import { ArrowUpCircle, Landmark } from "lucide-react";
16+
import { ArrowUpCircle, Landmark, MoreHorizontal, Percent } from "lucide-react";
1017
import { useWalletContext } from "@tokenization/tw-blocks-shared/src/wallet-kit/WalletProvider";
1118
import { CAMPAIGN_STATUS_CONFIG } from "@/features/campaigns/constants/campaign-status";
1219
import { formatCurrency } from "@/lib/utils";
1320
import { getVaultIsEnabled } from "@/features/campaigns/services/campaigns.api";
1421
import { ToggleVaultButton } from "@/features/campaigns/components/roi/ToggleVaultButton";
1522
import type { RoiTableRowProps } from "./types";
1623

17-
export function RoiTableRow({ campaign, balance, onAddFunds }: RoiTableRowProps) {
24+
export function RoiTableRow({ campaign, balance, onAddFunds, onUpdateRoi }: RoiTableRowProps) {
1825
const statusCfg = CAMPAIGN_STATUS_CONFIG[campaign.status];
1926
const { walletAddress } = useWalletContext();
2027
const [vaultEnabled, setVaultEnabled] = useState<boolean | null>(null);
@@ -34,8 +41,10 @@ export function RoiTableRow({ campaign, balance, onAddFunds }: RoiTableRowProps)
3441
<TableRow className="border-border hover:bg-secondary/30 transition-colors">
3542
<TableCell>
3643
<div className="flex flex-col gap-0.5">
37-
<span className="text-sm font-bold text-foreground">{campaign.name}</span>
38-
<span className="text-xs text-text-muted line-clamp-1 max-w-xs">
44+
<span className="text-sm font-bold text-foreground overflow-hidden text-ellipsis whitespace-nowrap">
45+
{campaign.name}
46+
</span>
47+
<span className="text-xs text-text-muted overflow-hidden text-ellipsis whitespace-nowrap max-w-xs">
3948
{campaign.description}
4049
</span>
4150
</div>
@@ -60,18 +69,7 @@ export function RoiTableRow({ campaign, balance, onAddFunds }: RoiTableRowProps)
6069
</TableCell>
6170

6271
<TableCell className="text-right">
63-
<div className="flex items-center justify-end gap-1.5 flex-wrap">
64-
<Button
65-
size="sm"
66-
variant="ghost"
67-
className="cursor-pointer text-primary hover:text-primary/80 gap-1 text-xs font-semibold"
68-
asChild
69-
>
70-
<Link href={`/campaigns/loans/${campaign.escrowId}`}>
71-
<Landmark className="size-3.5" />
72-
Gestionar Préstamos
73-
</Link>
74-
</Button>
72+
<div className="flex items-center justify-end gap-1.5">
7573
{campaign.vaultId && (
7674
<ToggleVaultButton
7775
vaultId={campaign.vaultId}
@@ -80,14 +78,42 @@ export function RoiTableRow({ campaign, balance, onAddFunds }: RoiTableRowProps)
8078
onToggled={handleToggled}
8179
/>
8280
)}
83-
<Button
84-
size="sm"
85-
className="cursor-pointer gap-1 text-xs"
86-
onClick={() => onAddFunds(campaign)}
87-
>
88-
<ArrowUpCircle className="size-3.5" />
89-
Subir Fondos
90-
</Button>
81+
82+
<DropdownMenu>
83+
<DropdownMenuTrigger asChild>
84+
<Button
85+
size="sm"
86+
variant="outline"
87+
className="cursor-pointer h-8 w-8 p-0"
88+
aria-label="Actions"
89+
>
90+
<MoreHorizontal className="size-4" />
91+
</Button>
92+
</DropdownMenuTrigger>
93+
<DropdownMenuContent align="end">
94+
<DropdownMenuItem asChild className="cursor-pointer">
95+
<Link href={`/campaigns/loans/${campaign.escrowId}`}>
96+
<Landmark className="size-3.5" />
97+
Gestionar Préstamos
98+
</Link>
99+
</DropdownMenuItem>
100+
<DropdownMenuItem
101+
className="cursor-pointer"
102+
onClick={() => onAddFunds(campaign)}
103+
>
104+
<ArrowUpCircle className="size-3.5" />
105+
Subir Fondos
106+
</DropdownMenuItem>
107+
<DropdownMenuSeparator />
108+
<DropdownMenuItem
109+
className="cursor-pointer"
110+
onClick={() => onUpdateRoi(campaign)}
111+
>
112+
<Percent className="size-3.5" />
113+
Actualizar ROI
114+
</DropdownMenuItem>
115+
</DropdownMenuContent>
116+
</DropdownMenu>
91117
</div>
92118
</TableCell>
93119
</TableRow>

apps/backoffice-tokenization/src/features/campaigns/components/roi/roi-table.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import type { RoiTableProps } from "./types";
1515

1616
const PAGE_SIZE = 4;
1717

18-
export function RoiTable({ campaigns, onAddFunds }: RoiTableProps) {
18+
export function RoiTable({ campaigns, onAddFunds, onUpdateRoi }: RoiTableProps) {
1919
const [visibleCount, setVisibleCount] = useState(PAGE_SIZE);
2020

2121
const escrowIds = campaigns.map((c) => c.escrowId).filter(Boolean);
@@ -56,6 +56,7 @@ export function RoiTable({ campaigns, onAddFunds }: RoiTableProps) {
5656
campaign={campaign}
5757
balance={balanceMap.get(campaign.escrowId) ?? 0}
5858
onAddFunds={onAddFunds}
59+
onUpdateRoi={onUpdateRoi}
5960
/>
6061
))}
6162
</TableBody>
Lines changed: 16 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,22 @@
11
"use client";
22

3-
import { StatItem } from "@/components/shared/stat-item";
43
import { RoiTable } from "@/features/campaigns/components/roi/roi-table";
54
import { FundRoiDialog } from "@/features/campaigns/components/roi/FundRoiDialog";
5+
import { UpdateRoiDialog } from "@/features/campaigns/components/roi/UpdateRoiDialog";
66
import { useRoi } from "@/features/campaigns/hooks/use-roi";
77
import { useCampaigns } from "@/features/campaigns/hooks/use-campaigns";
88

9-
const SUMMARY_STATS = [
10-
{
11-
label: "Total Activo",
12-
value: "$1,350,000",
13-
description: "+12.5% vs mes anterior",
14-
},
15-
{
16-
label: "Retorno Promedio",
17-
value: "8.4%",
18-
description: "Objetivo anual: 9.0%",
19-
},
20-
{
21-
label: "Beneficiarios",
22-
value: "1,248",
23-
description: "+154 nuevos este mes",
24-
},
25-
];
26-
279
export function RoiView() {
2810
const { data: campaigns = [] } = useCampaigns();
2911
const {
3012
fundsDialogCampaign,
3113
fundDialogOpen,
3214
openFundsDialog,
3315
closeFundsDialog,
16+
roiDialogCampaign,
17+
roiDialogOpen,
18+
openRoiDialog,
19+
closeRoiDialog,
3420
} = useRoi();
3521

3622
return (
@@ -46,30 +32,10 @@ export function RoiView() {
4632
<RoiTable
4733
campaigns={campaigns}
4834
onAddFunds={openFundsDialog}
35+
onUpdateRoi={openRoiDialog}
4936
/>
5037
</div>
5138

52-
{/* Financial summary */}
53-
{/* <div className="flex flex-col gap-4">
54-
<h3 className="text-base font-semibold text-foreground">
55-
Resumen Financiero
56-
</h3>
57-
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
58-
{SUMMARY_STATS.map((stat) => (
59-
<div
60-
key={stat.label}
61-
className="rounded-xl border border-border bg-card p-5 shadow-card"
62-
>
63-
<StatItem
64-
label={stat.label}
65-
value={stat.value}
66-
description={stat.description}
67-
/>
68-
</div>
69-
))}
70-
</div>
71-
</div> */}
72-
7339
{/* Dialogs */}
7440
{fundsDialogCampaign?.vaultId ? (
7541
<FundRoiDialog
@@ -80,6 +46,16 @@ export function RoiView() {
8046
onFunded={closeFundsDialog}
8147
/>
8248
) : null}
49+
50+
{roiDialogCampaign?.vaultId ? (
51+
<UpdateRoiDialog
52+
open={roiDialogOpen}
53+
onOpenChange={(open) => { if (!open) closeRoiDialog(); }}
54+
campaignName={roiDialogCampaign.name}
55+
vaultId={roiDialogCampaign.vaultId}
56+
onUpdated={closeRoiDialog}
57+
/>
58+
) : null}
8359
</div>
8460
);
8561
}

apps/backoffice-tokenization/src/features/campaigns/components/roi/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,14 @@ export interface RoiFormValues {
88
export interface RoiTableProps {
99
campaigns: Campaign[];
1010
onAddFunds: (campaign: Campaign) => void;
11+
onUpdateRoi: (campaign: Campaign) => void;
1112
}
1213

1314
export interface RoiTableRowProps {
1415
campaign: Campaign;
1516
balance: number;
1617
onAddFunds: (campaign: Campaign) => void;
18+
onUpdateRoi: (campaign: Campaign) => void;
1719
}
1820

1921
export interface CreateRoiDialogProps {

apps/backoffice-tokenization/src/features/campaigns/hooks/use-roi.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ export function useRoi() {
55
const [fundsDialogCampaign, setFundsDialogCampaign] = useState<Campaign | null>(null);
66
const [fundDialogOpen, setFundDialogOpen] = useState(false);
77

8+
const [roiDialogCampaign, setRoiDialogCampaign] = useState<Campaign | null>(null);
9+
const [roiDialogOpen, setRoiDialogOpen] = useState(false);
10+
811
function openFundsDialog(campaign: Campaign) {
912
setFundsDialogCampaign(campaign);
1013
setFundDialogOpen(true);
@@ -15,10 +18,24 @@ export function useRoi() {
1518
setFundsDialogCampaign(null);
1619
}
1720

21+
function openRoiDialog(campaign: Campaign) {
22+
setRoiDialogCampaign(campaign);
23+
setRoiDialogOpen(true);
24+
}
25+
26+
function closeRoiDialog() {
27+
setRoiDialogOpen(false);
28+
setRoiDialogCampaign(null);
29+
}
30+
1831
return {
1932
fundsDialogCampaign,
2033
fundDialogOpen,
2134
openFundsDialog,
2235
closeFundsDialog,
36+
roiDialogCampaign,
37+
roiDialogOpen,
38+
openRoiDialog,
39+
closeRoiDialog,
2340
};
2441
}

0 commit comments

Comments
 (0)