Adds trips table usage across API and UI: trip CRUD, per-trip analytics (category/daily/merchant/tag/participant breakdowns), tag-to-trip conversion, trip assignment via transaction overrides, and trip filter in the transactions view. Recovered from working tree after local git corruption; feature was already live via host-context Docker builds.
89 lines
2.9 KiB
TypeScript
89 lines
2.9 KiB
TypeScript
import { NextRequest, NextResponse } from "next/server";
|
|
import { prisma, queryRaw } from "@/lib/db";
|
|
import { assignTransactionsToTrip } from "@/lib/queries";
|
|
|
|
export async function POST(req: NextRequest) {
|
|
const body = await req.json();
|
|
const { action, ids, category, merchant_normalized, splits, tag_id } = body as {
|
|
action: string;
|
|
ids: number[];
|
|
category?: string;
|
|
merchant_normalized?: string;
|
|
splits?: { participant_id: number; share_percent: number }[];
|
|
tag_id?: number;
|
|
};
|
|
|
|
if (!ids || !Array.isArray(ids) || ids.length === 0) {
|
|
return NextResponse.json({ error: "ids required" }, { status: 400 });
|
|
}
|
|
|
|
if (action === "categorize" && category) {
|
|
const ops = ids.map((id) =>
|
|
prisma.transaction_overrides.upsert({
|
|
where: { transaction_id: id },
|
|
update: { category_override: category, updated_at: new Date() },
|
|
create: { transaction_id: id, category_override: category },
|
|
})
|
|
);
|
|
await prisma.$transaction(ops);
|
|
return NextResponse.json({ updated: ids.length });
|
|
}
|
|
|
|
if (action === "normalize" && merchant_normalized) {
|
|
const ops = ids.map((id) =>
|
|
prisma.transaction_overrides.upsert({
|
|
where: { transaction_id: id },
|
|
update: { merchant_normalized, updated_at: new Date() },
|
|
create: { transaction_id: id, merchant_normalized },
|
|
})
|
|
);
|
|
await prisma.$transaction(ops);
|
|
return NextResponse.json({ updated: ids.length });
|
|
}
|
|
|
|
if (action === "split" && Array.isArray(splits) && splits.length > 0) {
|
|
const total = splits.reduce((s, x) => s + x.share_percent, 0);
|
|
if (Math.abs(total - 100) > 0.01) {
|
|
return NextResponse.json({ error: "Shares must sum to 100%" }, { status: 400 });
|
|
}
|
|
await prisma.$transaction(
|
|
ids.flatMap((id) => [
|
|
prisma.transaction_splits.deleteMany({ where: { transaction_id: id } }),
|
|
prisma.transaction_splits.createMany({
|
|
data: splits.map((s) => ({
|
|
transaction_id: id,
|
|
participant_id: s.participant_id,
|
|
share_percent: s.share_percent,
|
|
})),
|
|
}),
|
|
])
|
|
);
|
|
return NextResponse.json({ updated: ids.length });
|
|
}
|
|
|
|
if ((action === "tag" || action === "untag") && tag_id) {
|
|
if (action === "tag") {
|
|
for (const id of ids) {
|
|
await queryRaw(
|
|
`INSERT INTO transaction_tags (transaction_id, tag_id) VALUES ($1, $2) ON CONFLICT DO NOTHING`,
|
|
[id, tag_id]
|
|
);
|
|
}
|
|
} else {
|
|
await queryRaw(
|
|
`DELETE FROM transaction_tags WHERE transaction_id = ANY($1::int[]) AND tag_id = $2`,
|
|
[ids, tag_id]
|
|
);
|
|
}
|
|
return NextResponse.json({ updated: ids.length });
|
|
}
|
|
|
|
if (action === "assign_trip") {
|
|
const { trip_id } = body as { ids: number[]; trip_id: number | null };
|
|
await assignTransactionsToTrip(trip_id, ids);
|
|
return NextResponse.json({ updated: ids.length });
|
|
}
|
|
|
|
return NextResponse.json({ error: "Invalid action" }, { status: 400 });
|
|
}
|