feat(merchants): scatter plot, merchant profiles, and per-merchant transaction history

- /merchants page with spend-vs-frequency scatter chart (click to open profile)
- Merchant profile drawer: stats, monthly trend line, full transaction history
- /api/analytics/merchants: split-adjusted merchant aggregates + monthly trends
- /api/analytics/merchants/[merchant]: per-merchant transaction list
- Add Merchants nav item to sidebar
This commit is contained in:
2026-03-10 00:05:48 +11:00
parent 2a10450c3e
commit 714c5a9b25
5 changed files with 647 additions and 0 deletions
@@ -0,0 +1,53 @@
import { NextRequest, NextResponse } from "next/server";
import { getCurrentUser } from "@/lib/auth";
import { queryRaw } from "@/lib/db";
export async function GET(
req: NextRequest,
{ params }: { params: Promise<{ merchant: string }> }
) {
const user = await getCurrentUser(req);
if (!user) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
const { merchant } = await params;
const decoded = decodeURIComponent(merchant);
const transactions = await queryRaw<{
id: number;
transaction_date: string;
description: string;
amount: number;
amount_aud: number | null;
my_amount: number;
transaction_type: string;
category: string;
bank_name: string;
statement_id: number;
}>(`
SELECT
t.id,
t.transaction_date::text,
t.description,
t.amount,
t.amount_aud,
CASE
WHEN ts.share_percent IS NOT NULL THEN COALESCE(t.amount_aud, t.amount) * ts.share_percent / 100
ELSE COALESCE(t.amount_aud, t.amount)
END::numeric(10,2) as my_amount,
t.transaction_type,
COALESCE(o.category_override, t.category) as category,
s.bank_name,
t.statement_id
FROM transactions t
JOIN statements s ON s.id = t.statement_id
LEFT JOIN transaction_overrides o ON o.transaction_id = t.id
LEFT JOIN transaction_splits ts ON ts.transaction_id = t.id AND ts.participant_id = $1
WHERE s.owner_id = $1
AND t.transaction_type IN ('debit', 'fee')
AND COALESCE(o.merchant_normalized, t.merchant_normalized, t.merchant_name, t.description) = $2
ORDER BY t.transaction_date DESC
LIMIT 500
`, [user.id, decoded]);
return NextResponse.json({ transactions });
}