Filter transactions by classification status
The Transactions toolbar gains a status filter over classification provenance — manual, AI, merchant rule, transfer match, unclassified — alongside the existing Needs review toggle. The Source column now renders the same human labels the filter options use instead of raw provenance keys, so "openrouter" reads as AI and both fallback shapes read as Unclassified.
This commit is contained in:
@@ -41,6 +41,26 @@ const EVENTS: Record<string, string> = {
|
||||
corporate_action: "Corporate action",
|
||||
position_transfer: "Position transfer",
|
||||
};
|
||||
// Human labels for classification provenance sources; the filter options and
|
||||
// the Source column speak the same language. Both fallback shapes — the
|
||||
// import-time "unclassified" error record and the plain sign-based
|
||||
// "fallback" — read as Unclassified.
|
||||
const CLASSIFICATIONS: Record<string, string> = {
|
||||
manual: "Manual",
|
||||
openrouter: "AI",
|
||||
rule: "Merchant rule",
|
||||
transfer_match: "Transfer match",
|
||||
fallback: "Unclassified",
|
||||
unclassified: "Unclassified",
|
||||
};
|
||||
const CLASSIFICATION_FILTERS: [string, string][] = [
|
||||
["manual", "Manual"],
|
||||
["openrouter", "AI"],
|
||||
["rule", "Merchant rule"],
|
||||
["transfer_match", "Transfer match"],
|
||||
["unclassified", "Unclassified"],
|
||||
];
|
||||
|
||||
// A corporate action or a position transfer moves shares between holdings and
|
||||
// settles no money at all, so its zero amount is a fact and not a gap.
|
||||
function positionOnly(investment?: Investment): boolean {
|
||||
@@ -92,6 +112,7 @@ export function Transactions({
|
||||
}) {
|
||||
const [query, setQuery] = useState("");
|
||||
const [needsReview, setNeedsReview] = useState(false);
|
||||
const [status, setStatus] = useState("");
|
||||
const [editing, setEditing] = useState<Transaction | null>(null);
|
||||
const [page, setPage] = useState(0);
|
||||
const filtered = useMemo(() => {
|
||||
@@ -122,6 +143,12 @@ export function Transactions({
|
||||
(e.kind === "income"
|
||||
? "cat_income_unclassified"
|
||||
: "cat_expenses_unclassified")) &&
|
||||
(!status ||
|
||||
(status === "unclassified"
|
||||
? ["fallback", "unclassified", ""].includes(
|
||||
e.classification.source || "",
|
||||
)
|
||||
: e.classification.source === status)) &&
|
||||
(!filter.tag_id || e.tag_ids.includes(filter.tag_id)) &&
|
||||
(!filter.merchant_id || e.merchant_id === filter.merchant_id) &&
|
||||
(!query ||
|
||||
@@ -134,7 +161,7 @@ export function Transactions({
|
||||
b.facts.booking_date.localeCompare(a.facts.booking_date) ||
|
||||
a.facts.id.localeCompare(b.facts.id),
|
||||
);
|
||||
}, [data, filter, query, needsReview]);
|
||||
}, [data, filter, query, needsReview, status]);
|
||||
const currentPage = Math.min(
|
||||
page,
|
||||
Math.max(0, Math.ceil(filtered.length / 40) - 1),
|
||||
@@ -181,6 +208,22 @@ export function Transactions({
|
||||
/>
|
||||
Needs review
|
||||
</label>
|
||||
<select
|
||||
className="toolbar-select"
|
||||
aria-label="Classification status"
|
||||
value={status}
|
||||
onChange={(e) => {
|
||||
setStatus(e.target.value);
|
||||
setPage(0);
|
||||
}}
|
||||
>
|
||||
<option value="">All classifications</option>
|
||||
{CLASSIFICATION_FILTERS.map(([value, label]) => (
|
||||
<option key={value} value={value}>
|
||||
{label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<span className="muted small">
|
||||
<SlidersHorizontal size={15} /> Click a transaction to edit
|
||||
</span>
|
||||
@@ -284,7 +327,8 @@ export function Transactions({
|
||||
</td>
|
||||
<td>
|
||||
<span className="badge neutral">
|
||||
{e.classification.source}
|
||||
{CLASSIFICATIONS[e.classification.source] ??
|
||||
e.classification.source}
|
||||
</span>
|
||||
{e.classification.error && (
|
||||
<small className="text-danger">
|
||||
|
||||
@@ -744,6 +744,16 @@ main {
|
||||
.search input::placeholder {
|
||||
color: #9aa6b3;
|
||||
}
|
||||
.toolbar-select {
|
||||
height: 35px;
|
||||
font-size: 11px;
|
||||
padding: 0 9px;
|
||||
border: 1px solid #dbe2ea;
|
||||
border-radius: 5px;
|
||||
background: #fff;
|
||||
color: #46596a;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.table-scroll {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user