diff --git a/app/api/households/[id]/fair-split/route.ts b/app/api/households/[id]/fair-split/route.ts new file mode 100644 index 0000000..6dcd7b0 --- /dev/null +++ b/app/api/households/[id]/fair-split/route.ts @@ -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`); +} diff --git a/app/settings/households/page.tsx b/app/settings/households/page.tsx index 3387f25..08a8cbe 100644 --- a/app/settings/households/page.tsx +++ b/app/settings/households/page.tsx @@ -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>({}); + const [fairSplitDraft, setFairSplitDraft] = useState({ + expenseAmount: "", + mineMonthlyIncome: "", + yoursMonthlyIncome: "", + splitMode: "income_weighted" as "equal" | "income_weighted" | "custom", + customMinePercent: "50", + }); + const [fairSplitResult, setFairSplitResult] = useState(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(`/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(`/api/households/${selectedId}/members/${member.id}`, { @@ -500,6 +564,92 @@ export default function HouseholdSettingsPage() { +
+
+
+

Fair split calculator

+

Split shared expenses by income or custom share

+

+ Calculate a household split before applying the same percentages to a shared transaction. +

+ {fairSplitResult ? ( +
+
+

Mine

+

{formatMoney(fairSplitResult.split.mineAmount)}

+

{fairSplitResult.split.minePercent}% share

+
+
+

Yours

+

{formatMoney(fairSplitResult.split.yoursAmount)}

+

{fairSplitResult.split.yoursPercent}% share

+
+

+ {fairSplitResult.rationale} Transaction default: {fairSplitResult.transactionDefaults.splitMode} split with ours attribution. +

+
+ ) : null} +
+
+ setFairSplitDraft((prev) => ({ ...prev, expenseAmount: event.target.value }))} + placeholder="Expense amount" + className={inputClass} + /> + + setFairSplitDraft((prev) => ({ ...prev, mineMonthlyIncome: event.target.value }))} + placeholder="Mine monthly income" + className={inputClass} + /> + setFairSplitDraft((prev) => ({ ...prev, yoursMonthlyIncome: event.target.value }))} + placeholder="Yours monthly income" + className={inputClass} + /> + {fairSplitDraft.splitMode === "custom" ? ( + setFairSplitDraft((prev) => ({ ...prev, customMinePercent: event.target.value }))} + placeholder="Mine percent" + className={inputClass} + /> + ) : null} + + {fairSplitStatus ?

{fairSplitStatus}

: null} +
+
+
+