Skip to content

feat(expenses): admin approve/deny review flow with receipt upload - #315

Merged
nourshoreibah merged 4 commits into
mainfrom
expense-approvals-app
Aug 12, 2026
Merged

feat(expenses): admin approve/deny review flow with receipt upload#315
nourshoreibah merged 4 commits into
mainfrom
expense-approvals-app

Conversation

@nourshoreibah

Copy link
Copy Markdown
Collaborator

Implements the Figma "Expenses Page" section (node 3545:34605).

Note

Based on #314 (migration + IAM), not main. Review/merge that one first — the review modal writes admin_notes, and the receipt upload needs the S3 permissions. Retarget to main once #314 lands. Supersedes #313, which mixed both layers.

Status vocabulary

The design is internally inconsistent — the table pill reads Needs Info, the base component variant is named rejected, and the filter menu says "Need Info". Confirmed with the requester: Pending / Approved / Needs Info.

The legacy denied value stays valid in the DB and still renders (greyed, un-clipped) if an old row has it, but it is not offered in the UI.

Frontend

  • Status column + pill (StatusBadge) using the design's fills — #B5D99D / #FFD167 / #E17070.
  • ReviewExpenseModal — everyone sees the expense read-only; Admin Decision and Admin Notes render only when isAdmin, and so does Save Changes.
  • Table matches the design: Expense ID, Date, Type of Expense, Project, Amount, Receipt, Status. Description dropped. Project detail hides Project via showProject.
  • Filters consolidated into a single "Filter By" nested menu (Month / Project / Type / Status) plus "Clear Filters (n)", per the frames.
  • /expenses is no longer admin-gated. Non-admins submit expenses and read their own submissions there; only the review controls are gated. The backend already let any authenticated user list expenditures, so this aligns the frontend with the existing API rather than exposing anything new.

Sizes are content-driven, not pinned to the Figma pixel values: the pill treats 81px as a min-width so longer labels cannot clip, the modal shrinks below its 485px design width, field labels flex, and a long receipt filename truncates instead of pushing the actions off the row. Table columns are percentage-based.

Bugs fixed along the way

  • The receipt was never uploaded. FileUpload ran a fake setInterval progress bar (marked TODO) and the File was only used as a required-field gate — receipt_url was always null. It now presigns, PUTs to S3 with real XHR progress, and threads the object URL into the POST body.
  • resetForm() did not clear the selected file, so a cancelled modal reopened still holding the previous receipt.
  • validateExpenditureInput read body.receipt_url while every other field was camelCase. Now accepts receiptUrl, old key still honoured.
  • Stale project roles. The new upload route was first written against the old PI/Accountant/Admin allow-list; git merged that cleanly over feat(roles): project roles are Admin, Director, Student #311, which had renamed the roles. Silent, and it would have refused every non-global-admin. Now Director/Admin, matching the sibling routes.
  • Pre-existing next build failure (present on main, unrelated to this work): Next.js forbids non-page exports from a page module, and both accounts/page.tsx (facilitationTeam, teamMembers) and expenses/page.tsx (EXPENSE_CATEGORIES) had them. The build was already broken before this branch; it passes now.

Backend

Route Purpose
GET /expenditures/upload-url presigned PDF PUT under receipts/{projectId}/
GET /expenditures/{id}/receipt short-lived presigned GET
PATCH /expenditures/{id}/status now also persists adminNotes
GET /expenditures/{id} now returns submitter + project name

Receipts are read through a presigned GET rather than their object URL, so they do not depend on the bucket being publicly readable.

Warning

Receipts land in aws_s3_bucket.reports_bucket, which on main is still public-read (block_public_* all false plus a Principal: "*" s3:GetObject policy). Receipts are financial documents at predictable keys. #310 makes that bucket private and is not merged yet. Nothing here depends on public access, but #310 should land before this is deployed.

Verification

  • npx tsc --noEmit clean (frontend + expenditures lambda)
  • next lint — no warnings or errors
  • next build — passes
  • Frontend: 27 suites / 277 passing, 2 skipped
  • Expenditures lambda: 120 passing (unit + e2e)

New tests cover the admin-vs-non-admin gating of the review modal, the table's Status/Receipt columns, real upload success/failure, and the three new backend route behaviours.

Not implemented

  • denied is retained in the DB constraint but has no UI, per the confirmed status set.
  • Design labels the filter options "Approval" / "Need Info"; used the pill labels ("Approved" / "Needs Info") since those are the actual status names and appear far more often.

🤖 Generated with Claude Code

nourshoreibah and others added 3 commits August 11, 2026 22:27
Implements the Figma "Expenses Page" section (node 3545:34605).

Frontend
- Status column and pill: Approved / Pending / Needs Info, using the
  design's exact fills. `needs_more_info` was already permitted by the
  DB check constraint, so no enum change was needed.
- New ReviewExpenseModal. Everyone sees the expense read-only; the
  Admin Decision pills and Admin Notes are rendered only for admins,
  and Save Changes is admin-only.
- Table now matches the design: Expense ID, Date, Type of Expense,
  Project, Amount, Receipt, Status. Description was dropped; the
  project detail page hides Project via `showProject`.
- Filters consolidated into one "Filter By" nested menu (Month /
  Project / Type / Status) plus "Clear Filters (n)".
- The receipt is now actually uploaded. FileUpload previously ran a
  fake setInterval progress bar and the File was never sent anywhere,
  so receipt_url was always null. It now presigns, PUTs to S3 with
  real XHR progress, and passes the object URL through to the POST.
- resetForm did not clear the selected file, so a cancelled modal
  reopened holding the previous receipt.

Backend
- GET /expenditures/upload-url presigns a PDF PUT under receipts/.
- GET /expenditures/{id}/receipt presigns a short-lived GET, so the
  receipt does not depend on the bucket being publicly readable.
- PATCH /expenditures/{id}/status accepts and persists adminNotes.
- GET /expenditures/{id} returns the submitter and project names for
  the modal's "Submitted By".
- validateExpenditureInput read body.receipt_url while every other
  field was camelCase; it now accepts receiptUrl and keeps the old
  key working.

Infra
- The shared lambda role had no S3 permissions, so a presigned PUT
  would have failed AccessDenied. Adds PutObject/GetObject.

Also fixes a pre-existing `next build` failure: page modules may not
have non-page exports, and both accounts/page.tsx and
expenses/page.tsx did.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The pill was pinned to Figma's 81x29, which clips longer labels such as
Needs Info and the legacy denied fallback. 81px is now a min-width and
the label drives the real width.

Review modal now shrinks below its 485px Figma width, the field labels
flex instead of sitting at a fixed 120px, and a long receipt filename
truncates rather than pushing the actions off the row.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…load route

The presigned upload route was written against the old PI/Accountant/Admin
allow-list and git merged it cleanly over #311, so it silently kept roles
that no longer exist. Any non-global-admin would have been refused.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@nourshoreibah
nourshoreibah force-pushed the expense-approvals-app branch from fcfae46 to 7a97212 Compare August 12, 2026 02:27
@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-315/
API: https://5zv9k5dxo9.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.

@nourshoreibah nourshoreibah added the no-review The PR review bot won't run label Aug 12, 2026
@nourshoreibah
nourshoreibah marked this pull request as ready for review August 12, 2026 02:53
@nourshoreibah
nourshoreibah merged commit 9ff48c0 into main Aug 12, 2026
24 checks passed
@nourshoreibah
nourshoreibah deleted the expense-approvals-app branch August 12, 2026 02:53
@github-actions

Copy link
Copy Markdown
Contributor

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

nourshoreibah added a commit that referenced this pull request Aug 12, 2026
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>
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