From 31b8531335254675cb3e737c21f2ec39b36637aa Mon Sep 17 00:00:00 2001 From: Anthony Volk Date: Tue, 3 Feb 2026 20:58:52 +0300 Subject: [PATCH 1/7] Add place-level (city) filtering for US impact analysis MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add PLACE to US_REGION_TYPES in regionTypes.ts - Add PlaceOption interface and US_PLACES_OVER_100K constant (333 places with population > 100,000 from Census 2023 estimates) - Add helper functions: getPlaceStateNames, filterPlacesByState, placeToRegionString, parsePlaceRegionString, findPlaceFromRegionString - Create USPlaceSelector component for selecting cities by state - Add place option to USGeographicOptions with radio button and selector - Update PopulationScopeView validation to require region for PLACE scope 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .../geographicOptions/USGeographicOptions.tsx | 19 + .../geographicOptions/USPlaceSelector.tsx | 93 ++++ .../views/population/PopulationScopeView.tsx | 1 + app/src/types/regionTypes.ts | 1 + app/src/utils/regionStrategies.ts | 409 ++++++++++++++++++ 5 files changed, 523 insertions(+) create mode 100644 app/src/pathways/report/components/geographicOptions/USPlaceSelector.tsx diff --git a/app/src/pathways/report/components/geographicOptions/USGeographicOptions.tsx b/app/src/pathways/report/components/geographicOptions/USGeographicOptions.tsx index 8cf3fc196..f8477dba0 100644 --- a/app/src/pathways/report/components/geographicOptions/USGeographicOptions.tsx +++ b/app/src/pathways/report/components/geographicOptions/USGeographicOptions.tsx @@ -2,6 +2,7 @@ import { Box, Radio } from '@mantine/core'; import { USScopeType } from '@/types/regionTypes'; import { RegionOption, US_REGION_TYPES } from '@/utils/regionStrategies'; import USDistrictSelector from './USDistrictSelector'; +import USPlaceSelector from './USPlaceSelector'; import USStateSelector from './USStateSelector'; interface USGeographicOptionsProps { @@ -75,6 +76,24 @@ export default function USGeographicOptions({ )} + {/* Place (City) option */} + + handleScopeChange(US_REGION_TYPES.PLACE)} + /> + {scope === US_REGION_TYPES.PLACE && ( + + + + )} + + {/* Household option */} void; +} + +export default function USPlaceSelector({ + selectedPlace, + onPlaceChange, +}: USPlaceSelectorProps) { + const [selectedStateName, setSelectedStateName] = useState(''); + + // Get unique state names for the state dropdown + const stateNames = useMemo(() => getPlaceStateNames(), []); + + // Initialize selectedStateName from selectedPlace if one is already selected + useEffect(() => { + if (selectedPlace) { + const place = findPlaceFromRegionString(selectedPlace); + if (place && place.stateName !== selectedStateName) { + setSelectedStateName(place.stateName); + } + } + }, [selectedPlace, selectedStateName]); + + // Filter places based on selected state name + const filteredPlaces = useMemo( + () => filterPlacesByState(selectedStateName), + [selectedStateName] + ); + + // Format places for the dropdown + const placeOptions = useMemo( + () => + filteredPlaces.map((p) => ({ + value: placeToRegionString(p), + label: p.name, + })), + [filteredPlaces] + ); + + // Handle state change for place selection + const handleStateChange = (stateName: string | null) => { + setSelectedStateName(stateName || ''); + // Auto-select the first place when a state is chosen + if (stateName) { + const statePlaces = filterPlacesByState(stateName); + if (statePlaces.length > 0) { + onPlaceChange(placeToRegionString(statePlaces[0])); + } + } else { + onPlaceChange(''); + } + }; + + return ( + <> + + Select City + + + onPlaceChange(val || '')} + searchable + style={{ flex: 2 }} + /> + ) : ( + {selectedStateName ? ( + +
+ + Select community + {selectedStateName ? ( + { diff --git a/app/src/pathways/report/views/population/PopulationLabelView.tsx b/app/src/pathways/report/views/population/PopulationLabelView.tsx index 3ae0e2c8c..b1d41bb58 100644 --- a/app/src/pathways/report/views/population/PopulationLabelView.tsx +++ b/app/src/pathways/report/views/population/PopulationLabelView.tsx @@ -56,7 +56,7 @@ export default function PopulationLabelView({ } else if (population.geography.geographyId) { const geographyId = population.geography.geographyId; - // Check if this is a US place (community) + // Check if this is a US place (city) if (geographyId.startsWith('place/')) { const place = findPlaceFromRegionString(geographyId); if (place) { diff --git a/app/src/tests/fixtures/pathways/report/components/geographicOptions/USGeographicOptionsMocks.ts b/app/src/tests/fixtures/pathways/report/components/geographicOptions/USGeographicOptionsMocks.ts index 1e0c6bef9..76d9c00fe 100644 --- a/app/src/tests/fixtures/pathways/report/components/geographicOptions/USGeographicOptionsMocks.ts +++ b/app/src/tests/fixtures/pathways/report/components/geographicOptions/USGeographicOptionsMocks.ts @@ -63,6 +63,6 @@ export const RADIO_LABELS = { NATIONAL: 'All households nationally', STATE: 'All households in a state or federal district', CONGRESSIONAL_DISTRICT: 'All households in a congressional district', - PLACE: 'All households in a community', + PLACE: 'All households in a city', HOUSEHOLD: 'Custom household', } as const; diff --git a/app/src/tests/unit/pathways/report/components/geographicOptions/USGeographicOptions.test.tsx b/app/src/tests/unit/pathways/report/components/geographicOptions/USGeographicOptions.test.tsx index 9e0ac1ee7..9e5029ab7 100644 --- a/app/src/tests/unit/pathways/report/components/geographicOptions/USGeographicOptions.test.tsx +++ b/app/src/tests/unit/pathways/report/components/geographicOptions/USGeographicOptions.test.tsx @@ -31,7 +31,7 @@ describe('USGeographicOptions', () => { screen.getByLabelText('All households in a state or federal district') ).toBeInTheDocument(); expect(screen.getByLabelText('All households in a congressional district')).toBeInTheDocument(); - expect(screen.getByLabelText('All households in a community')).toBeInTheDocument(); + expect(screen.getByLabelText('All households in a city')).toBeInTheDocument(); expect(screen.getByLabelText('Custom household')).toBeInTheDocument(); }); @@ -214,9 +214,9 @@ describe('USGeographicOptions', () => { expect(screen.queryByText('Select State')).not.toBeInTheDocument(); }); - // Place (community) tests - describe('place (community) option', () => { - test('given component then renders community option with correct label', () => { + // Place (city) tests + describe('place (city) option', () => { + test('given component then renders city option with correct label', () => { // Given const onScopeChange = vi.fn(); const onRegionChange = vi.fn(); @@ -234,28 +234,7 @@ describe('USGeographicOptions', () => { ); // Then - expect(screen.getByLabelText('All households in a community')).toBeInTheDocument(); - }); - - test('given component then does not show city option label', () => { - // Given - const onScopeChange = vi.fn(); - const onRegionChange = vi.fn(); - - // When - render( - - ); - - // Then - expect(screen.queryByLabelText(/All households in a city/i)).not.toBeInTheDocument(); + expect(screen.getByLabelText('All households in a city')).toBeInTheDocument(); }); test('given place scope then place radio is checked', () => { @@ -276,7 +255,7 @@ describe('USGeographicOptions', () => { ); // Then - expect(screen.getByLabelText('All households in a community')).toBeChecked(); + expect(screen.getByLabelText('All households in a city')).toBeChecked(); }); test('given place scope then shows place selector', () => { @@ -297,7 +276,7 @@ describe('USGeographicOptions', () => { ); // Then - expect(screen.getByText('Select community')).toBeInTheDocument(); + expect(screen.getByText('Select city')).toBeInTheDocument(); }); test('given national scope then does not show place selector', () => { @@ -318,7 +297,7 @@ describe('USGeographicOptions', () => { ); // Then - expect(screen.queryByText('Select community')).not.toBeInTheDocument(); + expect(screen.queryByText('Select city')).not.toBeInTheDocument(); }); test('given user clicks place option then calls onScopeChange with place', async () => { @@ -338,7 +317,7 @@ describe('USGeographicOptions', () => { ); // When - await user.click(screen.getByLabelText('All households in a community')); + await user.click(screen.getByLabelText('All households in a city')); // Then expect(onRegionChange).toHaveBeenCalledWith(''); @@ -362,7 +341,7 @@ describe('USGeographicOptions', () => { ); // When - await user.click(screen.getByLabelText('All households in a community')); + await user.click(screen.getByLabelText('All households in a city')); // Then expect(onRegionChange).toHaveBeenCalledWith(''); diff --git a/app/src/tests/unit/pathways/report/components/geographicOptions/USPlaceSelector.test.tsx b/app/src/tests/unit/pathways/report/components/geographicOptions/USPlaceSelector.test.tsx index 8963ece08..1c95ab388 100644 --- a/app/src/tests/unit/pathways/report/components/geographicOptions/USPlaceSelector.test.tsx +++ b/app/src/tests/unit/pathways/report/components/geographicOptions/USPlaceSelector.test.tsx @@ -12,7 +12,7 @@ describe('USPlaceSelector', () => { }); describe('rendering', () => { - test('given no selection then renders select community label', () => { + test('given no selection then renders select city label', () => { // Given const props = createMockProps(); @@ -20,7 +20,7 @@ describe('USPlaceSelector', () => { render(); // Then - expect(screen.getByText('Select community')).toBeInTheDocument(); + expect(screen.getByText('Select city')).toBeInTheDocument(); }); test('given no selection then renders state dropdown with placeholder', () => { @@ -34,7 +34,7 @@ describe('USPlaceSelector', () => { expect(screen.getByPlaceholderText('Choose a state')).toBeInTheDocument(); }); - test('given no state selected then community dropdown is disabled', () => { + test('given no state selected then city dropdown is disabled', () => { // Given const props = createMockProps(); @@ -58,7 +58,7 @@ describe('USPlaceSelector', () => { // Then - The component should show the state selection // Since Paterson is in New Jersey, the state dropdown should show New Jersey - expect(await screen.findByText('Select community')).toBeInTheDocument(); + expect(await screen.findByText('Select city')).toBeInTheDocument(); }); }); @@ -91,12 +91,12 @@ describe('USPlaceSelector', () => { render(); // Then - component renders with existing selection - expect(screen.getByText('Select community')).toBeInTheDocument(); + expect(screen.getByText('Select city')).toBeInTheDocument(); }); }); - describe('community selection', () => { - test('given state is selected then community dropdown becomes enabled', async () => { + describe('city selection', () => { + test('given state is selected then city dropdown becomes enabled', async () => { // Given const props = createMockProps({ selectedPlace: TEST_PLACE_REGIONS.PATERSON_NJ, @@ -105,8 +105,8 @@ describe('USPlaceSelector', () => { // When render(); - // Then - community dropdown should be available (not showing "--") - expect(screen.getByText('Select community')).toBeInTheDocument(); + // Then - city dropdown should be available (not showing "--") + expect(screen.getByText('Select city')).toBeInTheDocument(); }); }); @@ -122,7 +122,7 @@ describe('USPlaceSelector', () => { expect(screen.getByPlaceholderText('Choose a state')).toBeInTheDocument(); }); - test('given component renders without state then shows disabled community dropdown', () => { + test('given component renders without state then shows disabled city dropdown', () => { // Given const props = createMockProps(); @@ -130,8 +130,8 @@ describe('USPlaceSelector', () => { render(); // Then - should show disabled dropdown with "--" placeholder - const communityPlaceholder = screen.getByPlaceholderText('--'); - expect(communityPlaceholder).toBeInTheDocument(); + const cityPlaceholder = screen.getByPlaceholderText('--'); + expect(cityPlaceholder).toBeInTheDocument(); }); }); }); diff --git a/app/src/utils/geographyUtils.ts b/app/src/utils/geographyUtils.ts index 2fd8f4b5d..2147c0a43 100644 --- a/app/src/utils/geographyUtils.ts +++ b/app/src/utils/geographyUtils.ts @@ -70,7 +70,7 @@ export function getCountryLabel(countryCode: string): string { } export function getRegionLabel(regionCode: string, metadata: MetadataState): string { - // Handle US place (community) format: "place/NY-51000" + // Handle US place (city) format: "place/NY-51000" if (regionCode.startsWith('place/')) { const place = findPlaceFromRegionString(regionCode); if (place) { @@ -124,9 +124,9 @@ export function getRegionTypeLabel( ): string { // US strategy: check metadata to determine if it's a state, congressional district, or place if (countryId === 'us') { - // Check for place (community) format first + // Check for place (city) format first if (regionCode.startsWith('place/')) { - return 'Community'; + return 'City'; } const region = metadata.economyOptions.region.find(