feat(shared): replace settle buttons with payment ledger

- New split_payments table records actual payments between participants
- Balance = total split obligations - total payments (splits never marked settled)
- Record Payment modal per participant: direction toggle, amount pre-filled with balance, date, notes
- Payment history inline on each balance card with +/- display and delete
- Per-transaction Settle button removed; Action column removed from shared table
- Splits always show the true cost breakdown regardless of payment state
This commit is contained in:
2026-03-14 21:09:00 +11:00
parent 5206388958
commit 85e7801407
6 changed files with 383 additions and 94 deletions
+88
View File
@@ -0,0 +1,88 @@
import { NextRequest, NextResponse } from "next/server";
import { getCurrentUser } from "@/lib/auth";
import { queryRaw } from "@/lib/db";
import { prisma } from "@/lib/db";
export async function GET(req: NextRequest) {
const user = await getCurrentUser(req);
if (!user) return NextResponse.json({ error: "Unauthorized" }, { status: 403 });
const sp = req.nextUrl.searchParams;
const participantId = sp.get("participant_id");
// Return payment history between current user and a participant
const rows = await queryRaw<{
id: number;
from_participant_id: number;
from_name: string;
to_participant_id: number;
to_name: string;
amount: number;
payment_date: string;
notes: string | null;
linked_transaction_id: number | null;
created_at: string;
}>(
`SELECT sp.id, sp.from_participant_id, pf.name as from_name,
sp.to_participant_id, pt.name as to_name,
sp.amount, sp.payment_date, sp.notes,
sp.linked_transaction_id, sp.created_at
FROM split_payments sp
JOIN participants pf ON pf.id = sp.from_participant_id
JOIN participants pt ON pt.id = sp.to_participant_id
WHERE (sp.from_participant_id = $1 OR sp.to_participant_id = $1)
AND (sp.from_participant_id = $2 OR sp.to_participant_id = $2)
ORDER BY sp.payment_date DESC, sp.created_at DESC`,
[user.id, participantId ? Number(participantId) : user.id]
);
return NextResponse.json(rows);
}
export async function POST(req: NextRequest) {
const user = await getCurrentUser(req);
if (!user) return NextResponse.json({ error: "Unauthorized" }, { status: 403 });
const body = await req.json() as {
from_participant_id: number;
to_participant_id: number;
amount: number;
payment_date: string;
notes?: string;
linked_transaction_id?: number;
};
const { from_participant_id, to_participant_id, amount, payment_date, notes, linked_transaction_id } = body;
if (!from_participant_id || !to_participant_id || !amount || !payment_date) {
return NextResponse.json({ error: "Missing required fields" }, { status: 400 });
}
if (amount <= 0) {
return NextResponse.json({ error: "Amount must be positive" }, { status: 400 });
}
const payment = await prisma.split_payments.create({
data: {
from_participant_id,
to_participant_id,
amount,
payment_date: new Date(payment_date),
notes: notes || null,
linked_transaction_id: linked_transaction_id || null,
},
});
return NextResponse.json(payment);
}
export async function DELETE(req: NextRequest) {
const user = await getCurrentUser(req);
if (!user) return NextResponse.json({ error: "Unauthorized" }, { status: 403 });
const sp = req.nextUrl.searchParams;
const id = Number(sp.get("id"));
if (!id) return NextResponse.json({ error: "id required" }, { status: 400 });
await prisma.split_payments.delete({ where: { id } });
return NextResponse.json({ ok: true });
}