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.
87 lines
3.5 KiB
TypeScript
87 lines
3.5 KiB
TypeScript
"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>
|
|
);
|
|
}
|