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