Skip to content

feat(dashboard): admin sales dashboard and native controls in GET forms - #24

Merged
kris1027 merged 27 commits into
mainfrom
feat/native-controls-get-forms
Oct 19, 2026
Merged

kris1027 merged 27 commits into
mainfrom
feat/native-controls-get-forms

Conversation

@kris1027

Copy link
Copy Markdown
Owner

What

/admin is now a sales dashboard. It shows net revenue, orders, average order value, net units sold, a net revenue chart and the top 10 best sellers, for a period you pick. Every GET form (filters, the period picker, search) also moves to native browser controls, so it works before the app's JavaScript loads. A lint rule keeps the Base UI Select out of the codebase from now on.

Why

  • Implements spec 0015, Admin sales dashboard (scope item 15, Slice 9). You can see what the store earned after refunds, compare it with the period before, and find the best sellers without opening Stripe.
  • Implements spec 0016, Native controls in GET forms (scope item 23). The dashboard period picker shipped with a Base UI Select. Without JavaScript you could not change it, and Apply quietly sent the same period again (fixed in 131ea65). The same silent failure was sitting in the orders, discounts and products filters, so this change fixes them all and adds a lint rule so it can't come back.

Changes

Sales dashboard (src/features/dashboard/, /admin)

  • The Welcome component is gone. /admin shows a heading, "Signed in as …", the period picker, four stat cards, the chart and the best sellers table. "Dashboard" is the first sidebar item. The page calls requireAdmin() itself.
  • Period logic (period.ts): the presets today, 7d, 30d (the default), month and 12m, plus a custom from/to range. Days are store days in STORE_TIMEZONE. A to after today is clamped to today. A bad range falls back to 30 days and shows a notice. The previous window has the same number of days and ends the day before the chosen one starts.
  • Figures (queries.ts): live $queryRaw aggregates on every request, run in one REPEATABLE READ, read only transaction. No cache, no new table, column or index. A refund counts in the period it succeeded in, so past periods never change. Orders in another currency are left out and named in a notice. Sums that are not safe integers throw instead of rounding.
  • Chart: shadcn Chart (adds recharts 3.8.0, src/components/ui/chart.tsx) with a bucket per day, week or month depending on the window length. A "Show as table" toggle shows the same buckets in a table. The SVG is hidden from screen readers.
  • Logging (log.ts): dashboard.query_failed logs the SQLSTATE as pgCode. The logger censors any code key, which is why it isn't called code (fix 9978f80). Reads slower than 1,000 ms log dashboard.slow.

Orders list

  • /admin/orders accepts dates=created|paid. With paid, from/to filter paid_at, and unpaid orders are left out. The param flows through adminOrdersHref and hasFilters, and the filter form has a new "Date" select. The Orders card links to ?status=all&dates=paid&from=…&to=….
  • src/lib/dates.ts gains storeToday() and a shared CalendarDay type.

Native controls in GET forms

  • New NativeSelect from the shadcn registry (src/components/ui/native-select.tsx), with its states on /style-guide.
  • Moved to native controls: the orders filters (Status, Refund, Date, and the "Needs attention only" checkbox), the discounts status filter, the products category filter, the period picker, the storefront sort, and the checkout country and saved address selects. The images editor select moves too, because the ban covers every form outside the style guide. Each control keeps its id, name and label.
  • eslint.config.mjs bans @/components/ui/select everywhere except src/features/style-guide/**.

Tests

  • Unit: period, change, paths, log, dates and list-params. A lint test (tests/lint/no-base-ui-select.test.ts) proves the ban fires.
  • DB: dashboard.db.test.ts checks exact sums, refund timing, currency, buckets that add up, best sellers and failure logging. dashboard-year.db.test.ts seeds about 20,000 orders and 2,000 refunds, checks every figure exactly, and asserts the median of 3 warm runs stays within 1,500 ms (144 ms measured locally). admin-orders.db.test.ts covers dates=paid.
  • E2e: admin/dashboard.spec.ts with axe, keyboard and phone width. A new blockAppScripts helper (tests/e2e/no-js.ts) gives each GET form a test with the app's scripts blocked. Specs that clicked Base UI options now use selectOption and toHaveValue.

How to test / verify

  • You might run pnpm lint && pnpm typecheck && pnpm test, then pnpm test:db (needs pnpm db:start), then pnpm test:e2e tests/e2e/admin/dashboard.spec.ts.
  • If you want to see it yourself: sign in as an admin and open /admin. Try each preset and a custom range such as ?from=2026-02-30&to=2026-03-05, which should show the notice and fall back to 30 days. Press "Show as table", then click "View these orders". The orders list should open with Date set to "Paid", and its rows should match the Orders card.
  • To check the no JavaScript path, turn JavaScript off and change a filter on /admin/orders, /admin/discounts, /admin/products and the period picker. Each should change the results.
  • The full manual checklist is in verify.md, with every step ticked.

Risk & rollout

  • No migrations, no new tables or indexes, no flags, no new env vars. The one new dependency is recharts 3.8.0.
  • The dashboard runs live aggregates on every load of /admin. The seeded year test keeps this within a 1,500 ms CI budget, and slow reads log dashboard.slow.
  • Moving to native selects changes how the controls look (the option list is drawn by the browser). URLs and accessible names stay the same.
  • The change touches e2e files across orders, discounts, catalog and the design system, so it may conflict with other open work on those files.
  • To roll back, revert the merge. Nothing stored depends on it.

Notes for reviewers

  • A refund comes off the period it succeeded in, not the period of its order. This is on purpose (AC-6), so past figures never move. It also means Net revenue for a period can be negative.
  • Lint only catches the Base UI Select. A Base UI Checkbox or RadioGroup in a new GET form is only caught by that form's no JavaScript e2e test, because POST forms still use those components (spec 0016, tradeoffs).
  • blockAppScripts blocks only /_next/static/chunks/*.js, not CSS. Without styles, fill images cover the controls.
  • After merge, spec 0016's follow up asks /sync to add the native controls rule to AGENTS.md.

🤖 Generated with Claude Code

kris1027 and others added 17 commits October 19, 2026 16:17
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…and best sellers

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…h axe

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…t JavaScript

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…vaScript e2e test

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…act it

The logger censors any `code` key (TOTP codes, spec 0004), so dashboard.query_failed
always logged "[redacted]". A unit test now runs the line through the real logger.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Spec 0015 done: scope status done, spec Accepted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kris1027 kris1027 added accessibility Keyboard access, screen reader support, contrast, and accessibility feature New user-facing functionality refactor Code restructuring without changing user-facing behavior test Automated tests and test infrastructure labels Oct 19, 2026
@github-project-automation github-project-automation Bot moved this to Backlog in BeStore Oct 19, 2026
@kris1027 kris1027 added accessibility Keyboard access, screen reader support, contrast, and accessibility feature New user-facing functionality refactor Code restructuring without changing user-facing behavior test Automated tests and test infrastructure labels Oct 19, 2026
@kris1027 kris1027 self-assigned this Oct 19, 2026
@kris1027 kris1027 moved this from Backlog to In review in BeStore Oct 19, 2026
…range

The date inputs stay filled when scripts never load, and the dates won on the
server, so the preset was ignored. The picker now marks a custom range page
with shown=custom, and a valid period wins over the dates then.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
kris1027 and others added 9 commits October 19, 2026 20:41
…e copy

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e queries

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…in full

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…T forms

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…of the context

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
0015: the range line under the picker, the relabelled orders date inputs, and
the fourth query for the other currency count. 0016: the checkout select is
the saved addresses one, and the category page search has its no JavaScript
test too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kris1027
kris1027 merged commit 9503d6d into main Oct 19, 2026
7 checks passed
@github-project-automation github-project-automation Bot moved this from In review to Done in BeStore Oct 19, 2026
@kris1027
kris1027 deleted the feat/native-controls-get-forms branch October 19, 2026 18:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

accessibility Keyboard access, screen reader support, contrast, and accessibility feature New user-facing functionality refactor Code restructuring without changing user-facing behavior test Automated tests and test infrastructure

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

1 participant