Skip to content

feat(dashboard): admin-only dashboard from Figma - #318

Merged
nourshoreibah merged 7 commits into
mainfrom
worktree-dashboard-figma
Aug 12, 2026
Merged

feat(dashboard): admin-only dashboard from Figma#318
nourshoreibah merged 7 commits into
mainfrom
worktree-dashboard-figma

Conversation

@nourshoreibah

Copy link
Copy Markdown
Collaborator

Implements the Dashboard (admin view only) frame from Figma.

What

  • Rebuilds /dashboard: four summary stat cards, a three-project preview with View All, and a stacked monthly expenses chart.
  • Makes the dashboard admin-only. /dashboard joins ADMIN_PREFIXES and the nav item is gated on the admin role.
  • The Figma highlights Projects in the sidebar; per the issue description that's a bug, so Dashboard is the active item.

Landing route

Gating /dashboard breaks the landing route, which was /dashboard for every role — a non-admin would have hit the no-access panel the instant they signed in. Landing is now role-aware via landingPathFor(), with DEFAULT_LANDING_PATH (/projects) reachable by everyone. The login page redirects to / rather than naming a page, because isAdmin is not known until GET /auth/me returns.

Backend

GET /projects/dashboard already existed and was already admin-gated. Changes:

  • Spend aggregates scoped to the current calendar year, project count to projects that have not ended — so the figures match the card captions (this year, active projects).
  • Added the top category's percentage. Computed server-side because totalSpent is the divisor and may be zero.
  • The per-project budget breakdown stays lifetime-to-date on purpose; a budget bar that reset every January would show a project as under-spent when it is not.

Spend still counts pending/denied expenditures, unchanged from before — the design says nothing about status, so I did not change it silently. Easy follow-up if Total Spent should be approved-only.

Chart

Hand-rolled — no charting library is a dependency of this app.

Categories are matched with whitespace stripped: the expenses form writes "Visitor / Honorarium" while the design names the band "Visitor/Honorarium", and comparing literally split one category across two colours and listed it twice in the legend. Unrecognised categories still render with a fallback colour rather than silently dropping out of the totals.

Responsive

Not a transcription of the design's fixed 1280px frame: the stat and project grids reflow at sm/md/xl, the legend moves from a column beside the plot to a wrapping row above it, and plot height is a clamp() so bars scale with the viewport.

Drive-by fixes

  • next build was already broken on main. Pages may only export Next's own fields; accounts/page.tsx and expenses/page.tsx both exported extra values. Fixed both — otherwise this page could not be verified.
  • ProjectCard rendered NaN% for a project with no budget set.
  • Tailwind border utilities silently lose to Chakra's reset, so borders need ! on both width and colour. Applied to the new components and ProjectCard (whose border colour was rendering as Chakra grey, not the design token).

Verification

  • 27 frontend suites / 277 tests, 10 backend dashboard tests, tsc and eslint clean, next build exports all 16 routes.
  • Checked in a real browser against a stub API at 1440 / 900 / 430 px, as both an admin and a non-admin (nav hides Dashboard; direct URL shows the no-access panel).

🤖 Generated with Claude Code

Rebuilds /dashboard to the Figma design: four summary stat cards, a
three-project preview with a View All link, and a stacked monthly
expenses chart.

The dashboard is now admin-only, matching the design's
"Dashboard (admin view only)" frame. /dashboard joins ADMIN_PREFIXES and
the nav item is gated on the admin role.

That gate breaks the landing route, which was /dashboard for every role:
a non-admin would have been dropped on the no-access panel the instant
they signed in. The landing path is now role-aware via landingPathFor(),
with DEFAULT_LANDING_PATH (/projects) reachable by everyone. The login
page hands off to "/" rather than naming a page, because isAdmin is not
known until GET /auth/me returns.

Backend: GET /projects/dashboard already existed and was already
admin-gated. Scoped its spend aggregates to the current calendar year and
its project count to projects that have not ended, so the figures match
the card captions ("this year", "active projects"), and added the top
category's percentage — the client cannot derive it safely because
totalSpent is the divisor and may be zero. The per-project budget
breakdown stays lifetime-to-date on purpose.

Chart is hand-rolled; no charting library is a dependency of this app.
Categories are matched with whitespace stripped, because the expenses
form writes "Visitor / Honorarium" while the design names the band
"Visitor/Honorarium" — comparing literally split one category across two
colours. Unrecognised categories still render, with a fallback colour,
rather than being silently dropped from the totals.

Layout is fluid rather than a transcription of the design's fixed 1280px
frame: the stat and project grids reflow at sm/md/xl, the chart legend
moves from a column beside the plot to a wrapping row above it, and the
plot height is a clamp() so the bars scale with the viewport.

Also fixes two pre-existing `next build` failures. Pages may only export
Next's own fields, and both accounts/page.tsx and expenses/page.tsx
exported extra values, so the production build was broken on main.

Drive-by: ProjectCard rendered "NaN%" for a project with no budget set.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
github-actions Bot added a commit that referenced this pull request Aug 12, 2026
@nourshoreibah nourshoreibah added the no-review The PR review bot won't run label Aug 12, 2026
github-actions Bot added a commit that referenced this pull request Aug 12, 2026
@nourshoreibah nourshoreibah added the test-environment Creates a temporary (nearly free) test environment. Uses prod DB and cognito label Aug 12, 2026
@github-actions

Copy link
Copy Markdown
Contributor

🌿 ⏳ Creating preview environment… (logs)

The dashboard handler now filters summary cards to the current calendar
year and active projects, but e2e tests still used unmodified 2025 seed
dates. Shift fixture dates in the dashboard suite so CI assertions stay
stable.

Co-authored-by: Cursor <cursoragent@cursor.com>
@github-actions

Copy link
Copy Markdown
Contributor

🌿 Test environment updated in place ✅ — Click here to open. updated for ee1e9a1 · logs

Resolves conflicts between the admin-only dashboard and main's expense
approval flow (#315), project role rename (#311) and audit fixes (#310):

- routes: /dashboard is admin-gated, /expenses is not. Main opened
  /expenses to non-admins because they submit and read their own
  expenses there; only the review modal's approve/deny is admin-gated.
- accounts: both sides moved the staff roster out of page.tsx to satisfy
  the Next.js page-export rule. Kept main's mockUsers.ts and dropped the
  duplicate staff.ts.
- Navbar/routes tests follow the same split.

Co-authored-by: Cursor <cursoragent@cursor.com>
@github-actions

Copy link
Copy Markdown
Contributor

🌿 Test environment updated in place ✅ — Click here to open. updated for 49943fd · logs

@nourshoreibah nourshoreibah removed the test-environment Creates a temporary (nearly free) test environment. Uses prod DB and cognito label Aug 12, 2026
@github-actions

Copy link
Copy Markdown
Contributor

🌿 Preview environment torn down 🧹 — the stack for this PR has been destroyed.

The deploy job used cancel-in-progress, so a commit pushed moments after the
test-environment label killed the run mid `terraform apply`. The runner dies
before Terraform can persist state or release its DynamoDB lock, so the
workspace stays locked and everything already created is orphaned. On PR #318
that left a REST API with no `prod` stage, which 403s every request without
CORS headers -- surfacing in the browser as a CORS error on login.

Queue superseded runs instead of killing them; GitHub still cancels all but the
newest pending run, so rapid pushes collapse to one survivor as before.

Also check that the `prod` stage exists on the update path. Only the API's
existence was verified, so the run right after the cancelled one posted a green
"updated in place" on a stack that could not serve a single request.

Co-authored-by: Cursor <cursoragent@cursor.com>
@nourshoreibah nourshoreibah added the test-environment Creates a temporary (nearly free) test environment. Uses prod DB and cognito label Aug 12, 2026
@github-actions

Copy link
Copy Markdown
Contributor

🌿 ⏳ Creating preview environment… (logs)

@github-actions

Copy link
Copy Markdown
Contributor

🌿 Preview environment — ready ✅

Open: https://d3nmtjoh6ir9ym.cloudfront.net/pr-318/
API: https://2g19jvwjkf.execute-api.us-east-2.amazonaws.com/prod

Shared RDS + Cognito (prod data); DB migrations are not applied here — if this PR adds a migration, endpoints using the new columns will fail until it merges. New commits update this environment in place — a note is posted here on each update. Remove the test-environment label or close the PR to tear it down.

…ambda

The endpoint pulled one row per expenditure into the lambda to produce at most
12 x categories of monthly buckets, and fetched every project with selectAll --
including the unbounded description column -- next to two full-table GROUP BYs
that were then stitched together with JS Maps. Bytes transferred grew with the
expenditures table on every admin page load, though the output never did.

Postgres now does the bucketing (date_trunc + GROUP BY) and the per-project
join. The join goes through pre-aggregated subqueries rather than joining the
raw tables onto projects, which would multiply each expenditure by the
membership count and inflate `spent`. Seven queries become six.

Verified by diffing old and new responses over the same seeded database: year,
projects, expensesByMonth, totalSpent, totalProjects and topExpenseCategory come
back identical, including with an ended project in the fixture. Doing the month
bucketing in SQL also drops a latent bug -- the old code read a DATE through the
runtime's local timezone and only landed on the right month because lambda runs
in UTC.

Also fixes averageSpendPerProject, which divided all-projects spend by the
active-project count, inflating it whenever a project ended mid-year. Both sides
of the divide are now the same set of active projects.

Co-authored-by: Cursor <cursoragent@cursor.com>
@github-actions

Copy link
Copy Markdown
Contributor

🌿 Test environment updated in place ✅ — Click here to open. updated for 8ea2cab · logs

@github-actions

Copy link
Copy Markdown
Contributor

🌿 Test environment updated in place ✅ — Click here to open. updated for 50639e9 · logs

generate-readme scans every comment between the ROUTES markers with
/\/\/\s*([A-Z]+)\s+([\/\w\{\}\-]+)/, so a comment opening with a lone capital
and a space parses as a route. "A project is active..." and "A true
aggregate..." became `GET /project` and `GET /true` in the endpoint table when
the readme bot regenerated it. Reword both openings and regenerate.

Co-authored-by: Cursor <cursoragent@cursor.com>
@github-actions

Copy link
Copy Markdown
Contributor

🌿 Test environment updated in place ✅ — Click here to open. updated for d2240cb · logs

@nourshoreibah
nourshoreibah merged commit 1f7634c into main Aug 12, 2026
18 checks passed
@nourshoreibah
nourshoreibah deleted the worktree-dashboard-figma branch August 12, 2026 03:52
@github-actions

Copy link
Copy Markdown
Contributor

🌿 Preview environment torn down 🧹 — the stack for this PR has been destroyed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

no-review The PR review bot won't run test-environment Creates a temporary (nearly free) test environment. Uses prod DB and cognito

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant