feat(rules): show what an apply run changed, and which rule ran
ci / lint-test (push) Successful in 38s
ci / lint-test (push) Successful in 38s
Apply History listed only counts - '13 matches · 13 transactions' - which reads identically whether the run renamed a merchant or split every transaction with another participant. Revert is destructive, so that is not enough to decide on. Two additions. Migration 0017 records rule_id, rule_name and source on each run: rule_name is denormalised so history stays readable after a rule is edited or deleted, and there is no FK so deleting a rule cannot cascade away the audit trail. Both write paths now populate it - the condition-matched run and the selection-based quick action. And rows expand to show the run's snapshot set against current values: which transactions were touched and what changed on each. Rows changed by something else since the run are called out, because reverting restores the pre-run value and would discard that later edit. Runs recorded before this show 'Unknown rule' - the rule they came from is not recoverable.
This commit is contained in:
@@ -0,0 +1,86 @@
|
||||
"use client";
|
||||
|
||||
import { useRuleRunDetail } from "@/lib/hooks";
|
||||
import { formatCategory } from "@/lib/categories";
|
||||
|
||||
/**
|
||||
* The expanded body of an Apply History row: which transactions the run touched
|
||||
* and what it changed on each, so Revert is an informed decision rather than a
|
||||
* guess from a count.
|
||||
*/
|
||||
|
||||
function fmt(n: number) {
|
||||
return new Intl.NumberFormat("en-AU", { style: "currency", currency: "AUD" }).format(n);
|
||||
}
|
||||
|
||||
function describe(c: { field: string; from: string | null; to: string | null }) {
|
||||
if (c.field === "category") return `${formatCategory(c.from)} → ${formatCategory(c.to)}`;
|
||||
if (c.field === "merchant") return `${c.from ?? "—"} → ${c.to ?? "—"}`;
|
||||
if (c.field === "tags") return `+ tag ${c.to}`;
|
||||
return `split ${c.from ?? "none"} → ${c.to ?? "none"}`;
|
||||
}
|
||||
|
||||
export function RuleRunDetail({ runId }: { runId: number }) {
|
||||
const { data, isLoading, error } = useRuleRunDetail(runId);
|
||||
|
||||
if (isLoading) return <p className="px-4 py-3 text-xs text-zinc-500">Loading…</p>;
|
||||
if (error) return <p className="px-4 py-3 text-xs text-red-400">Failed to load detail.</p>;
|
||||
if (!data) return null;
|
||||
|
||||
const { transactions, still_changed, run } = data;
|
||||
|
||||
return (
|
||||
<div className="border-t border-zinc-800 bg-zinc-950/60">
|
||||
<div className="px-4 py-2 text-xs text-zinc-500 flex gap-4 flex-wrap">
|
||||
<span>{transactions.length} transactions</span>
|
||||
{run.reverted_at ? (
|
||||
<span className="text-zinc-500">already reverted — values are back to their originals</span>
|
||||
) : (
|
||||
<span className="text-amber-400">{still_changed} still showing this run's changes</span>
|
||||
)}
|
||||
{still_changed < transactions.length && !run.reverted_at && (
|
||||
<span className="text-zinc-600">
|
||||
{transactions.length - still_changed} since changed by something else — reverting restores the pre-run value
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="max-h-72 overflow-auto">
|
||||
<table className="w-full text-xs">
|
||||
<thead className="sticky top-0 bg-zinc-950">
|
||||
<tr className="text-zinc-600 border-b border-zinc-800">
|
||||
<th className="text-left px-4 py-1.5">Date</th>
|
||||
<th className="text-left px-3 py-1.5">Transaction</th>
|
||||
<th className="text-right px-3 py-1.5">Amount</th>
|
||||
<th className="text-left px-3 py-1.5">Changed</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{transactions.map((t) => (
|
||||
<tr key={t.id} className="border-b border-zinc-800/40">
|
||||
<td className="px-4 py-1.5 text-zinc-500 whitespace-nowrap">
|
||||
{String(t.transaction_date).slice(0, 10)}
|
||||
</td>
|
||||
<td className="px-3 py-1.5 text-zinc-300 max-w-[300px] truncate" title={t.description}>
|
||||
{t.merchant || t.description}
|
||||
<span className="text-zinc-600 ml-2">{t.bank_name}</span>
|
||||
</td>
|
||||
<td className="px-3 py-1.5 text-right tabular-nums text-zinc-400">
|
||||
{fmt(Number(t.amount_aud ?? t.amount))}
|
||||
</td>
|
||||
<td className="px-3 py-1.5">
|
||||
{t.changes.length === 0 ? (
|
||||
<span className="text-zinc-600">no change</span>
|
||||
) : (
|
||||
t.changes.map((c, i) => (
|
||||
<div key={i} className="text-amber-400">{describe(c)}</div>
|
||||
))
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user