feat(dashboard): admin-only dashboard from Figma - #318
Merged
Conversation
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>
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>
Contributor
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>
Contributor
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>
Contributor
|
🌿 ⏳ Creating preview environment… (logs) |
Contributor
🌿 Preview environment — ready ✅Open: https://d3nmtjoh6ir9ym.cloudfront.net/pr-318/ 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 |
…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>
Contributor
Contributor
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>
Contributor
Contributor
|
🌿 Preview environment torn down 🧹 — the stack for this PR has been destroyed. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Implements the
Dashboard (admin view only)frame from Figma.What
/dashboard: four summary stat cards, a three-project preview with View All, and a stacked monthly expenses chart./dashboardjoinsADMIN_PREFIXESand the nav item is gated on the admin role.Landing route
Gating
/dashboardbreaks the landing route, which was/dashboardfor every role — a non-admin would have hit the no-access panel the instant they signed in. Landing is now role-aware vialandingPathFor(), withDEFAULT_LANDING_PATH(/projects) reachable by everyone. The login page redirects to/rather than naming a page, becauseisAdminis not known untilGET /auth/mereturns.Backend
GET /projects/dashboardalready existed and was already admin-gated. Changes:percentage. Computed server-side becausetotalSpentis the divisor and may be zero.Spend still counts
pending/deniedexpenditures, 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 aclamp()so bars scale with the viewport.Drive-by fixes
next buildwas already broken onmain. Pages may only export Next's own fields;accounts/page.tsxandexpenses/page.tsxboth exported extra values. Fixed both — otherwise this page could not be verified.ProjectCardrenderedNaN%for a project with no budget set.!on both width and colour. Applied to the new components andProjectCard(whose border colour was rendering as Chakra grey, not the design token).Verification
tscand eslint clean,next buildexports all 16 routes.🤖 Generated with Claude Code