Skip to content

NWP-101: let ops choose export columns and scope - #179

Open
dmytromanannykov wants to merge 1 commit into
JJFromTenex:mainfrom
dmytromanannykov:NWP-101-export-options
Open

NWP-101: let ops choose export columns and scope#179
dmytromanannykov wants to merge 1 commit into
JJFromTenex:mainfrom
dmytromanannykov:NWP-101-export-options

Conversation

@dmytromanannykov

Copy link
Copy Markdown

Ticket

Closes NWP-101

What changed

Ops can now pick which columns go into a payments export and how much of the
table it covers, instead of always getting every column for the current page
of filters. The Export button opens a dialog: check the columns you want
(card last-four is unchecked by default), choose current filter or all
payments as the scope, see the row count before you download, and the file
comes down as payments-<scope-or-status>-<date>.csv. Unchecking every
column disables Download instead of letting you produce an empty file.

How I verified it

  • npm test — 42 passing (26 existing + 16 new), all in src/lib/csv.test.ts

  • npx tsc --noEmit — clean

  • npm run lint — no warnings or errors

  • Ran npm run dev and exercised GET /api/payments/export directly with curl:

    • status=disputed&scope=current → 33 rows, header matches the requested
      columns, filename payments-disputed-<date>.csv
    • scope=all → 1,658 rows, ignores merchant/status filters, filename
      payments-all-<date>.csv
    • columns= (empty) → 400 {"message":"Select at least one column to export."}
    • columns=id,evil,amount → unknown column silently dropped, header id,amount
    • no columns param at all (legacy link) → falls back to the default set, no last4
    • columns=id,last4 → last4 populated for card payments
  • Clicked through the dialog on /payments?status=disputed in the browser:
    opened it, confirmed every column checked except Card last four and
    "Current filter" selected by default with the count matching the table
    footer, unchecked every column and confirmed Download disabled with a
    message, re-checked one and confirmed it re-enabled, switched to "All
    payments" and watched the count update, and confirmed Escape closes the
    dialog and returns focus to the Export button.

  • npm test passes

  • New behavior is covered by a test

  • Checked it in the browser

Acceptance criteria

  • Ops can choose which columns are included. Card last-four is off by default.
  • Ops can choose scope: current filter or all payments. Current filter is
    the default, and the row count is visible before download.
  • The filename reflects the scope and the date — for a status filter or
    scope=all it's payments-<segment>-<date>.csv (e.g.
    payments-disputed-2026-08-13.csv). One case is narrower than the
    literal wording: a current-filter export with no status filter
    (e.g. only a merchant or search filter active) still produces
    payments-<date>.csv with no segment, matching today's filename and
    keeping it short — merchant/search aren't encoded into the name. Flagged
    below.
  • Amounts stay in minor units internally and are formatted once on the
    way out, with currency in its own column. (Already true of toCsv;
    unchanged by this PR, still covered by existing + new tests.)
  • Deselecting every column disables Download rather than producing an
    empty file.

Bugs fixed along the way

None found outside the ticket's scope. Noted but not touched: sortPayments'
amount sort in src/data/queries.ts compares String(amount) lexically
rather than numerically — pre-existing, the export inherits whatever order
the table shows by design, so I left it alone.

Notes for the reviewer

  • The export stays a plain GET + browser download (an <a href>, not a
    fetch), so the CSV is still built entirely server-side through the one
    query builder (filterPayments/sortPayments). scope=all isn't a second
    filter path — it's the same call with {status: "all"} and the other
    filters dropped.
  • Column and scope validation live as pure helpers in src/lib/csv.ts
    (parseExportColumns, DEFAULT_EXPORT_COLUMNS, exportLabel) so the
    "column serializer" the ticket's DoD asks for is directly unit-testable;
    the route calls them rather than reading searchParams itself.
  • There's no Dialog component in this codebase (despite what
    .claude/rules/components.md says) — only Drawer.tsx. I reused that
    rather than building a new modal primitive. Column checkboxes and the
    scope radios are native <input> elements in labeled <fieldset>s; there
    was no existing Checkbox/Radio component to reach for either.
  • Row counts for both scopes are computed server-side in page.tsx (via
    countPayments, a new small helper next to queryPayments) and passed to
    the dialog as props — this app has no client-side fetch anywhere, and I
    kept it that way rather than introducing one for the count.
  • Not unit-tested, by nature of this repo's node-only Vitest setup (no
    jsdom, no route-handler test harness): the Download-disabled DOM behavior,
    the live row-count display, and the emitted content-disposition header.
    All three are covered by the curl and browser checks above instead.

Adds an options dialog to the payments Export button: pick which
columns are included (card last-four off by default) and the scope
(current filter vs all payments, with the row count shown before
download).

- src/lib/csv.ts: DEFAULT_EXPORT_COLUMNS, parseExportColumns() (the
  column allowlist/serializer), exportLabel(), exportFilename() gains
  an optional scope/status segment
- src/app/api/payments/export/route.ts: validates columns and scope
  server-side, 400s on an empty column set, reuses the one query
  builder for scope=all
- src/data/queries.ts: countPayments() helper
- src/app/payments/export-dialog.tsx: new Drawer-based dialog
- src/app/payments/page.tsx: wires the dialog in, computes counts
- src/lib/csv.test.ts: 16 new cases covering the serializer, defaults,
  labels, and filenames

npm test: 42/42 passing. Verified end-to-end via curl against the
export route and a UI click-through (see PR description).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant