feat(ui): mobile-responsive sidebar + rules improvements

- Sidebar: hidden on mobile, opens as slide-out drawer with hamburger
  toggle; auto-closes on navigation; desktop layout unchanged
- Layout: responsive padding accounting for mobile header bar
- Rules: add tag as a condition field (has/not-has tag)
- Rules: apply a single rule via per-rule Apply button
- Rules: splits-from defaults to 2026-01-09
This commit is contained in:
2026-03-21 08:33:08 +11:00
parent ef73a9cea0
commit 0a1f6b48a2
5 changed files with 120 additions and 21 deletions
+16 -6
View File
@@ -4,7 +4,7 @@ import { queryRaw } from "@/lib/db";
import { getTransactions } from "@/lib/queries";
interface Condition {
field: "merchant_normalized" | "description" | "category" | "bank_name" | "amount" | "transaction_type";
field: "merchant_normalized" | "description" | "category" | "bank_name" | "amount" | "transaction_type" | "tag";
operator: "contains" | "equals" | "starts_with" | "gt" | "lt" | "not_equals";
value: string;
}
@@ -37,6 +37,7 @@ interface TxFields {
bank_name: string;
amount: number;
transaction_type: string;
tags: { id: number }[];
}
function evaluateCondition(cond: Condition, tx: TxFields): boolean {
@@ -52,6 +53,12 @@ function evaluateCondition(cond: Condition, tx: TxFields): boolean {
}
}
if (cond.field === "tag") {
const tagId = Number(cond.value);
const hasTag = tx.tags.some((t) => t.id === tagId);
return cond.operator === "not_equals" ? !hasTag : hasTag;
}
let fieldVal: string;
switch (cond.field) {
case "merchant_normalized": fieldVal = tx.effective_merchant || ""; break;
@@ -97,16 +104,19 @@ 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().catch(() => ({})) as { splitFrom?: string | null; ruleId?: number | null };
const splitFrom = body.splitFrom || null;
const ruleId = body.ruleId || null;
const rules = await queryRaw<{ id: number; conditions: unknown; actions: unknown }>(
`SELECT id, conditions, actions FROM rules WHERE owner_id = $1 AND enabled = true ORDER BY priority DESC`,
[user.id]
ruleId
? `SELECT id, conditions, actions FROM rules WHERE owner_id = $1 AND id = $2`
: `SELECT id, conditions, actions FROM rules WHERE owner_id = $1 AND enabled = true ORDER BY priority DESC`,
ruleId ? [user.id, ruleId] : [user.id]
);
if (!rules.length) return NextResponse.json({ matched: 0, transactions_affected: 0 });
const body = await req.json().catch(() => ({})) as { splitFrom?: string | null };
const splitFrom = body.splitFrom || null;
const { data: transactions } = await getTransactions(user.id, { limit: 100000, offset: 0 });
// --- Pre-pass: find all transactions that will match any rule ---