feat(trips): trip tracking with analytics, tag conversion, and transaction assignment

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.
This commit is contained in:
2026-07-19 20:00:51 +10:00
parent b706973fb5
commit 48ec151c15
21 changed files with 1506 additions and 55 deletions
+6 -3
View File
@@ -23,7 +23,7 @@ export async function PATCH(
const transactionId = Number(id);
const body = await req.json();
const { category, merchant_normalized, notes, transaction_type, my_share_percent, description, amount, transaction_date } = body as {
const { category, merchant_normalized, notes, transaction_type, my_share_percent, description, amount, transaction_date, trip_id } = body as {
category?: string;
merchant_normalized?: string;
notes?: string;
@@ -32,6 +32,7 @@ export async function PATCH(
description?: string;
amount?: number;
transaction_date?: string;
trip_id?: number | null;
};
if (my_share_percent !== undefined && my_share_percent !== null) {
@@ -72,8 +73,8 @@ export async function PATCH(
);
}
// category/merchant/notes/my_share_percent go through the overrides table
const hasOverride = category !== undefined || merchant_normalized !== undefined || notes !== undefined || my_share_percent !== undefined;
// category/merchant/notes/my_share_percent/trip_id go through the overrides table
const hasOverride = category !== undefined || merchant_normalized !== undefined || notes !== undefined || my_share_percent !== undefined || trip_id !== undefined;
if (!hasOverride) {
return NextResponse.json({ ok: true });
}
@@ -83,6 +84,7 @@ export async function PATCH(
if (merchant_normalized !== undefined) data.merchant_normalized = merchant_normalized;
if (notes !== undefined) data.notes = notes;
if (my_share_percent !== undefined) data.my_share_percent = my_share_percent;
if (trip_id !== undefined) data.trip_id = trip_id;
const override = await prisma.transaction_overrides.upsert({
where: { transaction_id: transactionId },
@@ -93,6 +95,7 @@ export async function PATCH(
merchant_normalized: merchant_normalized || null,
notes: notes || null,
my_share_percent: my_share_percent != null ? String(my_share_percent) : null,
trip_id: trip_id ?? null,
},
});
+7
View File
@@ -1,5 +1,6 @@
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();
@@ -77,5 +78,11 @@ export async function POST(req: NextRequest) {
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 });
}
+1
View File
@@ -25,6 +25,7 @@ export async function GET(req: NextRequest) {
amount_min: sp.get("amount_min") ? Number(sp.get("amount_min")) : undefined,
amount_max: sp.get("amount_max") ? Number(sp.get("amount_max")) : undefined,
has_split: sp.get("has_split") || undefined,
trip_id: sp.get("trip_id") || undefined,
});
return NextResponse.json(result);