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:
@@ -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 });
|
||||
}
|
||||
Reference in New Issue
Block a user