feat(rules): manual-only rules as one-click quick actions on selected transactions
ci / lint-test (push) Successful in 52s
ci / lint-test (push) Successful in 52s
A rule flagged manual_only never runs in the apply-all pass; instead it shows as a button in the transactions bulk bar and applies its actions to the current selection (conditions ignored — the selection is the condition). Recorded as a rule_apply_run, so it reverts from Rules -> Apply History like any other run. Motivation: tagging Home + splitting 50/50 with Sonu was two bulk actions every time. Now it is one click, and any other combo can be defined the same way. Extracts the action-application and snapshot logic from the apply route into src/lib/rule-actions.ts so both callers share one implementation — splits upsert rather than delete+reinsert, so settled flags survive.
This commit is contained in:
@@ -2,6 +2,8 @@ import { NextRequest, NextResponse } from "next/server";
|
||||
import { prisma, queryRaw } from "@/lib/db";
|
||||
import { assignTransactionsToTrip, canAccessTransactions } from "@/lib/queries";
|
||||
import { getCurrentUser } from "@/lib/auth";
|
||||
import { applyRuleActions, captureSnapshot } from "@/lib/rule-actions";
|
||||
import type { Actions } from "@/lib/rules";
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
const user = await getCurrentUser(req);
|
||||
@@ -85,6 +87,37 @@ export async function POST(req: NextRequest) {
|
||||
return NextResponse.json({ updated: ids.length });
|
||||
}
|
||||
|
||||
// Quick action: fire a saved rule's actions at the current selection. The
|
||||
// selection replaces the rule's conditions, which are not evaluated at all.
|
||||
// Recorded as a rule_apply_run so it can be reverted like any other run.
|
||||
if (action === "apply_rule") {
|
||||
const { rule_id } = body as { rule_id?: number };
|
||||
if (!rule_id) return NextResponse.json({ error: "rule_id required" }, { status: 400 });
|
||||
|
||||
const rules = await queryRaw<{ id: number; name: string; actions: unknown }>(
|
||||
`SELECT id, name, actions FROM rules WHERE id = $1 AND owner_id = $2`,
|
||||
[Number(rule_id), user.id]
|
||||
);
|
||||
if (!rules.length) return NextResponse.json({ error: "Rule not found" }, { status: 404 });
|
||||
|
||||
const actions = (typeof rules[0].actions === "string"
|
||||
? JSON.parse(rules[0].actions)
|
||||
: rules[0].actions) as Actions;
|
||||
|
||||
const snapshot = await captureSnapshot(ids.map(Number));
|
||||
for (const id of ids) {
|
||||
await applyRuleActions(Number(id), actions);
|
||||
}
|
||||
|
||||
const run = await queryRaw<{ id: number }>(
|
||||
`INSERT INTO rule_apply_runs (owner_id, split_from, matched, transactions_affected, snapshot)
|
||||
VALUES ($1, NULL, $2, $3, $4) RETURNING id`,
|
||||
[user.id, ids.length, ids.length, JSON.stringify(snapshot)]
|
||||
);
|
||||
|
||||
return NextResponse.json({ updated: ids.length, run_id: run[0].id, rule: rules[0].name });
|
||||
}
|
||||
|
||||
if (action === "assign_trip") {
|
||||
const { trip_id } = body as { ids: number[]; trip_id: number | null };
|
||||
await assignTransactionsToTrip(trip_id, ids);
|
||||
|
||||
Reference in New Issue
Block a user