fixed rebalance ui, helper feature redistruvbtion
All checks were successful
Deploy / deploy (push) Successful in 57s
Security Tests / security-non-db (push) Successful in 19s
Security Tests / security-db (push) Successful in 23s

This commit is contained in:
2026-03-11 22:11:15 -05:00
parent d39928a3f7
commit e6dac3f344

View File

@@ -71,22 +71,33 @@ export default function RebalancePage() {
push("err", "Amount exceeds available budget"); push("err", "Amount exceeds available budget");
return; return;
} }
const baseSum = sum(others.map((o) => o.targetCents)) || others.length;
const next = rows.map((r) => ({ ...r })); const weightSum = others.reduce((s, o) => s + (o.percent || 1), 0) || others.length;
let distributed = 0; const provisional = others.map((o) => {
others.forEach((o) => { const exact = (remaining * (o.percent || 1)) / weightSum;
const share = Math.floor((remaining * (o.targetCents || 1)) / baseSum); const base = Math.floor(exact);
const idx = next.findIndex((n) => n.id === o.id); return { id: o.id, base, frac: exact - base };
next[idx].targetCents = share;
distributed += share;
}); });
const leftover = remaining - distributed; let distributed = provisional.reduce((s, p) => s + p.base, 0);
let leftover = remaining - distributed;
provisional
.slice()
.sort((a, b) => b.frac - a.frac)
.forEach((p) => {
if (leftover > 0) { if (leftover > 0) {
const firstIdx = next.findIndex((n) => n.id !== adjustId); p.base += 1;
if (firstIdx >= 0) next[firstIdx].targetCents += leftover; leftover -= 1;
} }
});
const next = rows.map((r) => ({ ...r }));
provisional.forEach((p) => {
const idx = next.findIndex((n) => n.id === p.id);
if (idx >= 0) next[idx].targetCents = p.base;
});
const targetIdx = next.findIndex((n) => n.id === adjustId); const targetIdx = next.findIndex((n) => n.id === adjustId);
next[targetIdx].targetCents = desired; if (targetIdx >= 0) next[targetIdx].targetCents = desired;
setRows(next); setRows(next);
}; };
@@ -115,28 +126,28 @@ export default function RebalancePage() {
if (isLoading || !data) return <div className="muted">Loading</div>; if (isLoading || !data) return <div className="muted">Loading</div>;
return ( return (
<div className="space-y-4"> <div className="space-y-6">
<header className="space-y-1"> <header className="space-y-1">
<h2 className="text-lg font-semibold">Rebalance variable pool</h2> <h2 className="text-2xl font-semibold">Rebalance variable pool</h2>
<p className="text-sm muted"> <p className="text-sm muted">
Redistribute your current variable pool without changing future income percentages. Redistribute your current variable pool without changing future income percentages.
</p> </p>
</header> </header>
<div className="card p-4 flex flex-wrap gap-4 items-center justify-between"> <div className="card glass p-5 flex flex-wrap gap-6 items-center justify-between">
<div className="text-sm"> <div className="space-y-1">
<div className="font-semibold">Available</div> <div className="text-xs uppercase tracking-wide muted">Available</div>
<div className="text-xl font-mono">${(available / 100).toFixed(2)}</div> <div className="text-2xl font-mono font-semibold">${(available / 100).toFixed(2)}</div>
</div> </div>
<div className="text-sm"> <div className="space-y-1">
<div>Totals</div> <div className="text-xs uppercase tracking-wide muted">Totals</div>
<div className="font-mono"> <div className="text-sm font-mono">
${(total / 100).toFixed(2)} / ${(available / 100).toFixed(2)} ${(total / 100).toFixed(2)} / ${(available / 100).toFixed(2)}
</div> </div>
</div> </div>
<div className="text-sm"> <div className="space-y-1">
<div>Savings total</div> <div className="text-xs uppercase tracking-wide muted">Savings total</div>
<div className="font-mono">${(savingsTotal / 100).toFixed(2)}</div> <div className="text-sm font-mono">${(savingsTotal / 100).toFixed(2)}</div>
</div> </div>
<label className="flex items-center gap-2 text-sm"> <label className="flex items-center gap-2 text-sm">
<input <input
@@ -148,12 +159,12 @@ export default function RebalancePage() {
</label> </label>
</div> </div>
<div className="card p-4 space-y-3"> <div className="card p-5 space-y-4">
<div className="flex flex-wrap gap-2 items-end"> <div className="flex flex-wrap gap-3 items-end">
<div className="flex flex-col"> <div className="flex flex-col gap-1">
<label className="text-xs muted">Adjust one category</label> <label className="text-xs uppercase tracking-wide muted">Adjust one category</label>
<select <select
className="input" className="input w-56"
value={adjustId} value={adjustId}
onChange={(e) => setAdjustId(e.target.value)} onChange={(e) => setAdjustId(e.target.value)}
> >
@@ -164,10 +175,10 @@ export default function RebalancePage() {
))} ))}
</select> </select>
</div> </div>
<div className="flex flex-col"> <div className="flex flex-col gap-1">
<label className="text-xs muted">Amount</label> <label className="text-xs uppercase tracking-wide muted">Amount</label>
<input <input
className="input" className="input w-40"
type="number" type="number"
min={0} min={0}
step="0.01" step="0.01"
@@ -176,32 +187,33 @@ export default function RebalancePage() {
placeholder="0.00" placeholder="0.00"
/> />
</div> </div>
<button className="btn" type="button" onClick={applyAdjustOne}> <button className="btn primary" type="button" onClick={applyAdjustOne}>
Apply helper Apply helper
</button> </button>
</div> </div>
<div className="overflow-auto"> <div className="overflow-auto rounded-2xl border border-[--color-border]/50 bg-[--color-panel]">
<table className="w-full text-sm"> <table className="w-full text-sm">
<thead> <thead className="bg-[--color-surface]">
<tr className="text-left"> <tr className="text-left">
<th className="py-2">Category</th> <th className="py-3 px-4 text-xs uppercase tracking-wide muted">Category</th>
<th className="py-2">Current</th> <th className="py-3 px-4 text-xs uppercase tracking-wide muted">Current</th>
<th className="py-2">Percent</th> <th className="py-3 px-4 text-xs uppercase tracking-wide muted">Percent</th>
<th className="py-2">Target</th> <th className="py-3 px-4 text-xs uppercase tracking-wide muted">Target</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{rows.map((row) => ( {rows.map((row) => (
<tr key={row.id} className="border-t border-[--color-panel]"> <tr key={row.id} className="border-t border-[--color-border]/30">
<td className="py-2 font-medium"> <td className="py-3 px-4 font-medium flex items-center gap-2">
{row.name} {row.isSavings ? <span className="text-xs text-emerald-500">Savings</span> : null} {row.name}
{row.isSavings ? <span className="badge badge-ghost">Savings</span> : null}
</td> </td>
<td className="py-2 font-mono">${(row.balanceCents / 100).toFixed(2)}</td> <td className="py-3 px-4 font-mono">${(row.balanceCents / 100).toFixed(2)}</td>
<td className="py-2">{row.percent}%</td> <td className="py-3 px-4">{row.percent}%</td>
<td className="py-2"> <td className="py-3 px-4">
<CurrencyInput <CurrencyInput
className="w-32" className="input w-32"
valueCents={row.targetCents} valueCents={row.targetCents}
onChange={(cents) => onChange={(cents) =>
setRows((prev) => setRows((prev) =>
@@ -238,7 +250,7 @@ export default function RebalancePage() {
Apply rebalance Apply rebalance
</button> </button>
<button <button
className="btn" className="btn ghost"
type="button" type="button"
onClick={async () => { onClick={async () => {
setForceSavings(false); setForceSavings(false);