Skip to content

[Issue #8499] Update Opportunities List page to single-agency view#8671

Open
desaiamit wants to merge 5 commits intomainfrom
adesai/8499-opportunities-list-single-agency
Open

[Issue #8499] Update Opportunities List page to single-agency view#8671
desaiamit wants to merge 5 commits intomainfrom
adesai/8499-opportunities-list-single-agency

Conversation

@desaiamit
Copy link
Collaborator

Summary

Work for #8499

Changes proposed

  • Add userAgenciesFetcher.ts — server-only fetcher that calls POST /v1/users/<user_id>/agencies to retrieve the logged-in user's associated agencies
  • Add AgencySelector.tsx — client component that renders a dropdown for multi-agency users and updates the URL on change
  • Update OpportunitiesListPage to:
    • Redirect to ?agency=<first_agency_id> if no agency param is in the URL
    • Show an error if the agency in the URL is not in the user's agencies
    • Show an error if the user has no agency associations
    • Filter opportunities by the selected agency's agency_code
    • Show the agency dropdown only for users with 2+ agencies
  • Add i18n strings: agencySelector, agencyNotAuthorized, noAgencies

Context for reviewers

The page now uses URL state (?agency=<agency_id>) to track the selected agency. The frontend enforces the authorization check by comparing the URL agency against the list returned by the user agencies endpoint — no separate API call is needed for this check.

The AgencySelector is a client component to support useRouter for navigation on dropdown change. The page itself remains a server component.

Validation steps

Prerequisites

cd api && make db-seed-local-with-agencies

Ensure API and frontend are running locally.

Scenario 1 — Single agency: no dropdown

  1. Click Sign in, enter one_agency_opp_edit on the Mock OAuth2 Sign-in page
  2. Navigate to http://localhost:3000/opportunities
  3. Expected: Redirects to ?agency=<uuid> automatically
  4. Expected: No "Select agency" dropdown visible
  5. Expected: Table shows opportunities (or empty state) for USAID-ETH only

Scenario 2 — Multi-agency: dropdown shown

  1. Sign out, click Sign in, enter two_agency_opp_pub
  2. Navigate to http://localhost:3000/opportunities
  3. Expected: Redirects to ?agency=<uuid> for the first agency
  4. Expected: "Select agency" dropdown visible with USAID-ETH and USAID-SAF
  5. Select the other agency from the dropdown
  6. Expected: URL updates to ?agency=<other-uuid> and table refreshes

Scenario 3 — Unauthorized agency in URL

  1. While logged in as one_agency_opp_edit, navigate to:
    http://localhost:3000/opportunities?agency=00000000-0000-0000-0000-000000000000
  2. Expected: Red alert — "You do not have access to this agency's opportunities."

Scenario 4 — Copy/paste URL preserves agency view

  1. Log in as two_agency_opp_pub, navigate to /opportunities
  2. Use the dropdown to switch to the second agency — URL updates to ?agency=<uuid-B>
  3. Copy the full URL, open a new tab, paste and hit Enter
  4. Expected: Loads directly into that agency's view without redirecting

- Add userAgenciesFetcher to fetch the logged-in user's agencies
- Add AgencySelector client component with dropdown for multi-agency users
- Update OpportunitiesListPage to filter opportunities by selected agency,
  redirect to first agency if no agency param, and show error states for
  no agencies or unauthorized agency access
- Add i18n strings for agency selector label and error messages
myduong-navapbc
myduong-navapbc previously approved these changes Mar 3, 2026
@myduong-navapbc
Copy link
Collaborator

[low priority] UserAgency is now used across the fetcher, page, component, and tests. It might make sense to move it into a shared types module so UI code does not depend on the fetcher layer.

export interface UserAgency {
  agency_id: string
  agency_name: string
  agency_code: string
}

consider adding agencyTypes to frontend/src/types

@myduong-navapbc
Copy link
Collaborator

[question] Since the default agency depends on userAgencies[0], do we know whether the agencies endpoint returns a stable ordering? Just want to confirm the default behavior is deterministic.

@desaiamit
Copy link
Collaborator Author

[low priority] UserAgency is now used across the fetcher, page, component, and tests. It might make sense to move it into a shared types module so UI code does not depend on the fetcher layer.

export interface UserAgency {
  agency_id: string
  agency_name: string
  agency_code: string
}

consider adding agencyTypes to frontend/src/types

Thanks for the suggestion. Agreed this type may belong in a shared types module as usage grows. For this PR, I kept it scoped to the fetcher/feature implementation to minimize surface-area changes. I’ll open a follow-up to move UserAgency into frontend/src/types once we confirm all intended consumers for the opportunity publishing flow.

@desaiamit
Copy link
Collaborator Author

[question] Since the default agency depends on userAgencies[0], do we know whether the agencies endpoint returns a stable ordering? Just want to confirm the default behavior is deterministic.

Agreed, relying on API order without an explicit contract can be brittle. I’ll make the updates so first-load behavior is stable.

Copy link
Collaborator

@myduong-navapbc myduong-navapbc left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I ran through all three scenarios with no issues.

Nice work!

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.

2 participants