Add fair split calculator UI

This commit is contained in:
MOHAN 2026-07-16 23:15:32 +05:30
parent 1649fb8e77
commit 69a88163dc
2 changed files with 159 additions and 0 deletions

View File

@ -0,0 +1,9 @@
import { NextRequest } from "next/server";
import { proxyRequest } from "@/lib/backend";
export async function POST(
req: NextRequest,
{ params }: { params: { id: string } }
) {
return proxyRequest(req, `households/${params.id}/fair-split`);
}

View File

@ -125,6 +125,31 @@ type DashboardData = {
recentTransactions: RecentTransaction[];
};
type FairSplitResult = {
expenseAmount: number;
splitMode: "equal" | "income_weighted" | "custom";
mineMonthlyIncome: number;
yoursMonthlyIncome: number;
totalIncome: number;
incomeShares: {
mine: number;
yours: number;
};
split: {
minePercent: number;
yoursPercent: number;
mineAmount: number;
yoursAmount: number;
};
transactionDefaults: {
attribution: "ours";
splitMode: "equal" | "custom";
splitMinePercent: number;
splitYoursPercent: number;
};
rationale: string;
};
const money = new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
@ -173,6 +198,15 @@ export default function HouseholdSettingsPage() {
priority: "medium" as "low" | "medium" | "high",
});
const [goalContributions, setGoalContributions] = useState<Record<string, string>>({});
const [fairSplitDraft, setFairSplitDraft] = useState({
expenseAmount: "",
mineMonthlyIncome: "",
yoursMonthlyIncome: "",
splitMode: "income_weighted" as "equal" | "income_weighted" | "custom",
customMinePercent: "50",
});
const [fairSplitResult, setFairSplitResult] = useState<FairSplitResult | null>(null);
const [fairSplitStatus, setFairSplitStatus] = useState("");
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
const [status, setStatus] = useState("");
@ -349,6 +383,36 @@ export default function HouseholdSettingsPage() {
await loadInvites(selectedId);
};
const calculateFairSplit = async (event: FormEvent) => {
event.preventDefault();
if (!selectedId) return;
const expenseAmount = Number.parseFloat(fairSplitDraft.expenseAmount);
if (!Number.isFinite(expenseAmount) || expenseAmount <= 0) {
setFairSplitStatus("Enter an expense amount greater than zero.");
return;
}
setFairSplitStatus("Calculating fair split...");
const res = await apiFetch<FairSplitResult>(`/api/households/${selectedId}/fair-split`, {
method: "POST",
body: JSON.stringify({
expenseAmount,
mineMonthlyIncome: Number.parseFloat(fairSplitDraft.mineMonthlyIncome || "0"),
yoursMonthlyIncome: Number.parseFloat(fairSplitDraft.yoursMonthlyIncome || "0"),
splitMode: fairSplitDraft.splitMode,
customMinePercent: fairSplitDraft.splitMode === "custom"
? Number.parseFloat(fairSplitDraft.customMinePercent)
: undefined,
}),
});
if (res.error) {
setFairSplitResult(null);
setFairSplitStatus(res.error.message ?? "Unable to calculate split.");
return;
}
setFairSplitResult(res.data);
setFairSplitStatus("");
};
const updateMemberAccess = async (member: HouseholdMember, payload: { role?: string; status?: string }) => {
if (!selectedId) return;
const res = await apiFetch<HouseholdMember>(`/api/households/${selectedId}/members/${member.id}`, {
@ -500,6 +564,92 @@ export default function HouseholdSettingsPage() {
</div>
</div>
<div className="glass-panel rounded-2xl p-6 shadow-sm">
<div className="grid gap-6 lg:grid-cols-[0.9fr_1.1fr]">
<div>
<p className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">Fair split calculator</p>
<h2 className="mt-2 text-xl font-bold text-foreground">Split shared expenses by income or custom share</h2>
<p className="mt-2 text-sm text-muted-foreground">
Calculate a household split before applying the same percentages to a shared transaction.
</p>
{fairSplitResult ? (
<div className="mt-5 grid gap-3 sm:grid-cols-2">
<div className="rounded-xl border border-border bg-background/40 p-4">
<p className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">Mine</p>
<p className="mt-2 text-2xl font-bold text-foreground">{formatMoney(fairSplitResult.split.mineAmount)}</p>
<p className="text-sm text-muted-foreground">{fairSplitResult.split.minePercent}% share</p>
</div>
<div className="rounded-xl border border-border bg-background/40 p-4">
<p className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">Yours</p>
<p className="mt-2 text-2xl font-bold text-foreground">{formatMoney(fairSplitResult.split.yoursAmount)}</p>
<p className="text-sm text-muted-foreground">{fairSplitResult.split.yoursPercent}% share</p>
</div>
<p className="text-sm text-muted-foreground sm:col-span-2">
{fairSplitResult.rationale} Transaction default: {fairSplitResult.transactionDefaults.splitMode} split with ours attribution.
</p>
</div>
) : null}
</div>
<form onSubmit={calculateFairSplit} className="grid gap-3 md:grid-cols-2">
<input
type="number"
min="0"
step="0.01"
value={fairSplitDraft.expenseAmount}
onChange={(event) => setFairSplitDraft((prev) => ({ ...prev, expenseAmount: event.target.value }))}
placeholder="Expense amount"
className={inputClass}
/>
<select
value={fairSplitDraft.splitMode}
onChange={(event) => setFairSplitDraft((prev) => ({ ...prev, splitMode: event.target.value as "equal" | "income_weighted" | "custom" }))}
className={inputClass}
>
<option value="income_weighted">Income-weighted</option>
<option value="equal">Equal 50/50</option>
<option value="custom">Custom percent</option>
</select>
<input
type="number"
min="0"
step="0.01"
value={fairSplitDraft.mineMonthlyIncome}
onChange={(event) => setFairSplitDraft((prev) => ({ ...prev, mineMonthlyIncome: event.target.value }))}
placeholder="Mine monthly income"
className={inputClass}
/>
<input
type="number"
min="0"
step="0.01"
value={fairSplitDraft.yoursMonthlyIncome}
onChange={(event) => setFairSplitDraft((prev) => ({ ...prev, yoursMonthlyIncome: event.target.value }))}
placeholder="Yours monthly income"
className={inputClass}
/>
{fairSplitDraft.splitMode === "custom" ? (
<input
type="number"
min="0"
max="100"
step="0.01"
value={fairSplitDraft.customMinePercent}
onChange={(event) => setFairSplitDraft((prev) => ({ ...prev, customMinePercent: event.target.value }))}
placeholder="Mine percent"
className={inputClass}
/>
) : null}
<button
type="submit"
className="rounded-lg bg-primary px-4 py-2 text-sm font-bold text-primary-foreground hover:bg-primary/90 md:col-span-2"
>
Calculate split
</button>
{fairSplitStatus ? <p className="text-sm text-muted-foreground md:col-span-2">{fairSplitStatus}</p> : null}
</form>
</div>
</div>
<div className="grid gap-6 xl:grid-cols-[0.9fr_1.1fr]">
<div className="glass-panel rounded-2xl p-6 shadow-sm">
<div className="flex items-center justify-between gap-3">