Skip to content

feat: Transactions page — browse, filter, manual categorize#15

Merged
kayodebristol merged 9 commits into
mainfrom
copilot/add-transactions-page-features
Apr 1, 2026
Merged

feat: Transactions page — browse, filter, manual categorize#15
kayodebristol merged 9 commits into
mainfrom
copilot/add-transactions-page-features

Conversation

Copilot AI commented Mar 31, 2026

Copy link
Copy Markdown
Contributor

Implements the Transactions page for the financial-advisor plugin with full browsing, filtering, sorting, confidence visualization, decision chain inspection, and manual categorization.

Data model (lib/transactions.ts)

  • FA_INFERENCES_COLLECTION — new fa-transaction-inferences PluresDB collection name
  • TransactionInference interface — per-transaction inference record with field, value, confidence, reasoning, sourceId, userConfirmed
  • confidenceLevel(score)'high' | 'medium' | 'low' tier helper
  • generateInferenceId(transactionId, field) — deterministic ID enabling safe upserts

Transactions.svelte

  • List view — date, vendor, amount (green=credit), account, category badge, confidence badge
  • Filter bar — account dropdown, date range, category select (16 categories), min/max amount
  • Sort — clickable date / amount / confidence headers; toggle asc/desc; aria-sort attributes
  • Confidence badges — green ≥90%, yellow 70–89%, red <70%
  • Expand row — calls ctx.inference.getDecisionChain() and renders the step chain alongside inference metadata (reasoning, source, confirmed flag)
  • Category override dialog — upserts to fa-transaction-inferences with confidence: 1.0 and userConfirmed: true; updates local state optimistically
// Upsert shape written on manual override
const record: TransactionInference = {
  ...existing,
  id: generateInferenceId(tx.id, 'category'),  // deterministic → safe upsert
  value: selectedCategory,
  confidence: 1.0,
  sourceId: 'user',
  userConfirmed: true,
  updatedAt: now,
};
await infCollection.put(record.id, record);

Filtering and sorting use a single $derived computed array so Svelte memoizes the result; the template references filteredRows directly rather than calling it as a function.

Copilot AI changed the title [WIP] Build transactions page with filtering and categorization feat: Transactions page — browse, filter, manual categorize Mar 31, 2026
Copilot AI requested a review from kayodebristol March 31, 2026 07:36
@kayodebristol kayodebristol requested a review from Copilot March 31, 2026 08:26

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new Transactions page to the financial-advisor plugin, plus supporting transaction-inference data model helpers, to enable browsing, filtering/sorting, confidence display, decision-chain inspection, and manual category overrides.

Changes:

  • Introduces fa-transaction-inferences collection and TransactionInference model utilities (confidence tiering + deterministic inference IDs).
  • Adds Transactions.svelte page with filter bar, sortable list view, expandable decision-chain panel, and category override modal with optimistic updates.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 5 comments.

File Description
plugins/financial-advisor/src/pages/Transactions.svelte Implements the Transactions UI (load/query, filter+sort derived rows, decision-chain expand, manual categorization modal, and styling).
plugins/financial-advisor/src/lib/transactions.ts Adds inference collection/model definitions and helpers used by the Transactions page.

Comment on lines +187 to +206
async function toggleExpand(id: string): Promise<void> {
if (expandedId === id) {
expandedId = null;
decisionChain = [];
return;
}
expandedId = id;
decisionChain = [];

const inf = inferenceMap.get(id);
if (!inf || !ctx) return;

loadingChain = true;
try {
decisionChain = (await ctx.inference.getDecisionChain(inf.id)) ?? [];
} catch {
decisionChain = [{ note: 'Decision chain unavailable.' }];
} finally {
loadingChain = false;
}

Copilot AI Mar 31, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

toggleExpand can leave loadingChain stuck true: if the newly-expanded transaction has no inference (!inf) or ctx is missing, the function returns early without resetting loadingChain. If the previous expansion was still loading, the UI will incorrectly show “Loading decision chain…” for rows with no inference. Ensure loadingChain is set to false on all early-return paths (and when collapsing).

Copilot uses AI. Check for mistakes.
Comment on lines +195 to +205

const inf = inferenceMap.get(id);
if (!inf || !ctx) return;

loadingChain = true;
try {
decisionChain = (await ctx.inference.getDecisionChain(inf.id)) ?? [];
} catch {
decisionChain = [{ note: 'Decision chain unavailable.' }];
} finally {
loadingChain = false;

Copilot AI Mar 31, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

toggleExpand is vulnerable to an async race: if a user expands row A, then quickly expands row B, the in-flight getDecisionChain for A may resolve after B and overwrite decisionChain while B is expanded. Capture the requested transaction id locally and only apply the result if expandedId still matches (or cancel/ignore stale requests).

Suggested change
const inf = inferenceMap.get(id);
if (!inf || !ctx) return;
loadingChain = true;
try {
decisionChain = (await ctx.inference.getDecisionChain(inf.id)) ?? [];
} catch {
decisionChain = [{ note: 'Decision chain unavailable.' }];
} finally {
loadingChain = false;
const requestedId = id;
const inf = inferenceMap.get(id);
if (!inf || !ctx) return;
loadingChain = true;
try {
const chain = (await ctx.inference.getDecisionChain(inf.id)) ?? [];
if (expandedId === requestedId) {
decisionChain = chain;
}
} catch {
if (expandedId === requestedId) {
decisionChain = [{ note: 'Decision chain unavailable.' }];
}
} finally {
if (expandedId === requestedId) {
loadingChain = false;
}

Copilot uses AI. Check for mistakes.
return new Intl.DateTimeFormat('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric',

Copilot AI Mar 31, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

formatDate constructs a UTC-midnight Date and then formats it in the user's local time zone. In negative-offset time zones this can render the previous calendar day (e.g., 2026-03-31 displaying as Mar 30). Consider formatting with timeZone: 'UTC', or parse the YYYY-MM-DD parts and construct a local date to avoid day-shift.

Suggested change
day: 'numeric',
day: 'numeric',
timeZone: 'UTC',

Copilot uses AI. Check for mistakes.
Comment on lines +840 to +844
.tx-table__th[onclick] {
cursor: pointer;
}

.tx-table__th[onclick]:hover {

Copilot AI Mar 31, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The CSS selector .tx-table__th[onclick] likely won’t match in Svelte because onclick={...} attaches a listener without emitting an onclick HTML attribute. As a result, the intended pointer cursor/hover styles for sortable headers may never apply. Use an explicit class for sortable headers (or target the button.tx-table__th elements directly) instead of [onclick].

Suggested change
.tx-table__th[onclick] {
cursor: pointer;
}
.tx-table__th[onclick]:hover {
button.tx-table__th {
cursor: pointer;
}
button.tx-table__th:hover {

Copilot uses AI. Check for mistakes.
Comment on lines +58 to +75
export interface TransactionInference {
/** `inf-<transactionId>-<field>` — deterministic so that upserts are safe. */
id: string;
transactionId: string;
/** Typically "category". */
field: string;
value: string;
/** 0.0 – 1.0. User-confirmed overrides are stored with confidence = 1.0. */
confidence: number;
/** Prose explanation produced by the inference rule. */
reasoning: string;
/** Which inference rule (or "user") produced this result. */
sourceId: string;
/** True when the user explicitly confirmed or overrode this inference. */
userConfirmed: boolean;
/** ISO 8601 timestamp of last write. */
updatedAt: string;
}

Copilot AI Mar 31, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TransactionInference.value is typed as string, but the plugin’s inference engine contract (InferenceResult.value) is unknown, and the file header mentions storing other inference fields (e.g., tags) that may not be strings. Consider changing value to unknown (or a narrower union) so the storage model matches the inference API and avoids unsafe casts later.

Copilot uses AI. Check for mistakes.

@kayodebristol kayodebristol left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Auto-approved: CI green + Copilot code review complete.

@kayodebristol

Copy link
Copy Markdown
Contributor

@copilot apply changes based on the comments in this thread

@kayodebristol kayodebristol marked this pull request as ready for review April 1, 2026 09:30

Copilot AI commented Apr 1, 2026

Copy link
Copy Markdown
Contributor Author

@copilot apply changes based on the comments in this thread

Applied all review comments in commit 0f9f6ec:

  • loadingChain stucktoggleExpand now sets loadingChain = false on both early-return paths: when collapsing a row and when no inference exists for the expanded row.
  • Async race — already guarded in the prior commit (requestedId capture + if (expandedId === requestedId) checks in try/catch/finally).
  • formatDate day-shift — already using timeZone: 'UTC' in the prior commit.
  • CSS selector — already using button.tx-table__th (not [onclick]) in the prior commit.
  • TransactionInference.value type — changed from string to unknown to match InferenceResult.value: unknown from the inference engine contract. All call-sites in the template already use typeof inference.value === 'string' guards.

@kayodebristol kayodebristol merged commit 7554051 into main Apr 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: Transactions page — browse, filter, manual categorize

3 participants