Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions app/src/components/flagship/report/ReportView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -407,6 +407,7 @@ export default function ReportView({
baselinePolicyId={baselinePolicyId}
reformPolicyId={reformPolicyId}
reformPaths={provisions.map((provision) => provision.path)}
reformChangesNothing={!!output && isSocietyWideReportNoOp(output)}
/>
) : (
<>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@ import { IconPencil, IconPlus, IconTrash } from '@tabler/icons-react';
import { useSelector } from 'react-redux';
import { STATE_NAMES } from '@/api/billFeed';
import HouseholdImpactResult from '@/components/flagship/report/household/HouseholdImpactResult';
import {
COMMON_HOUSEHOLD_INPUTS,
householdInputDescription,
householdInputName,
householdTopicIcon,
} from '@/components/flagship/report/household/householdPickerInputs';
import RunProgress from '@/components/flagship/report/RunProgress';
import { ErrorState } from '@/components/flagship/states';
import HouseholdBuilderForm from '@/components/household/HouseholdBuilderForm';
Expand All @@ -11,8 +17,11 @@ import { colors, spacing, typography } from '@/designTokens';
import { useFlagshipHouseholdPayload } from '@/hooks/useFlagshipHousehold';
import { suggestedHouseholdInputs } from '@/libs/flagship/householdSuggestions';
import {
describesAHousehold,
HOUSEHOLD_INCOME_SUBGROUPS,
HOUSEHOLD_VARIABLE_GROUPS,
householdVariableGroup,
householdVariableSubgroup,
listsForState,
} from '@/libs/flagship/householdVariableGroups';
import { loadParameterDependencies } from '@/libs/flagship/parameterDependencies';
Expand Down Expand Up @@ -67,12 +76,16 @@ interface HouseholdImpactSectionProps {
reformPolicyId: string;
/** The parameters the reform changes: the household inputs they read lead the variable search. */
reformPaths?: string[];
/** The report found the reform changes nothing in its year: no input is suggested. */
reformChangesNothing?: boolean;
}

/**
* The household inputs the reform's parameters read, nearest first; none
* until the dependency map loads, or when it traces none of them.
*/
const PREFERRED_INPUTS = new Set(COMMON_HOUSEHOLD_INPUTS.map((input) => input.name));

function useSuggestedInputs(paths: string[]): string[] {
const metadata = useSelector((state: RootState) => state.metadata);
const inputs = useMemo(
Expand All @@ -90,7 +103,9 @@ function useSuggestedInputs(paths: string[]): string[] {
loadParameterDependencies()
.then((map) => {
if (!cancelled) {
setSuggested(suggestedHouseholdInputs(pathsKey.split('\n'), map, inputs));
setSuggested(
suggestedHouseholdInputs(pathsKey.split('\n'), map, inputs, undefined, PREFERRED_INPUTS)
);
}
})
.catch(() => {
Expand All @@ -117,8 +132,10 @@ export default function HouseholdImpactSection({
baselinePolicyId,
reformPolicyId,
reformPaths = [],
reformChangesNothing = false,
}: HouseholdImpactSectionProps) {
const suggestedVariables = useSuggestedInputs(reformPaths);
// A reform that changes nothing in the year makes no input worth suggesting.
const suggestedVariables = useSuggestedInputs(reformChangesNothing ? [] : reformPaths);
// Each household by a key of its own, so removing one keeps the others' inputs.
const [keys, setKeys] = useState<number[]>([]);
const [nextKey, setNextKey] = useState(1);
Expand Down Expand Up @@ -224,10 +241,11 @@ function HouseholdCard({
.flatMap(([, fields]) => fields),
[basicInputFields]
);
// The variable search lists only the household's own state's programs.
// The variable search lists only the household's own state's programs,
// and only inputs that describe a household (no survey-reported totals).
const stateCode = householdState(household, year);
const variableFilter = useCallback(
(variable: VariableInfo) => listsForState(variable, stateCode),
(variable: VariableInfo) => listsForState(variable, stateCode) && describesAHousehold(variable),
[stateCode]
);
// Whether the household can run, as the household builder checks it.
Expand Down Expand Up @@ -336,6 +354,13 @@ function HouseholdCard({
variableGroupOf={householdVariableGroup}
variableGroupOrder={HOUSEHOLD_VARIABLE_GROUPS}
variableFilter={variableFilter}
variableSubgroupOf={householdVariableSubgroup}
variableSubgroupOrder={HOUSEHOLD_INCOME_SUBGROUPS}
searchByEntity
variablePickerCommon={COMMON_HOUSEHOLD_INPUTS}
variableGroupIcon={householdTopicIcon}
variableNameOf={householdInputName}
variableDescribe={householdInputDescription}
/>
<div
style={{
Expand Down
178 changes: 178 additions & 0 deletions app/src/components/flagship/report/household/householdPickerInputs.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
/**
* What the report's household picker opens on: the inputs most
* households need, with short names, and an icon per topic. An input
* shows only where it is offered, so one list serves the person picker
* and the household picker alike.
*/
import {
IconBuildingCommunity,
IconCash,
IconDots,
IconHome,
IconPigMoney,
IconReceiptTax,
IconStethoscope,
IconUsers,
IconWallet,
} from '@tabler/icons-react';
import type { CommonInput } from '@/components/household/VariablePickerDialog';
import type { VariableInfo } from '@/utils/VariableResolver';

/**
* In order of how often a household needs them; the picker shows the first
* it offers. The model has no description for most of them
* (PolicyEngine/policyengine-us), so each carries its own.
*/
export const COMMON_HOUSEHOLD_INPUTS: CommonInput[] = [
// A person's
{
name: 'pre_subsidy_rent',
label: 'Rent',
description: 'Rent this person pays for the home in a year, before any housing assistance.',
},
{
name: 'self_employment_income_before_lsr',
label: 'Self-employment income',
description:
'Net earnings from self-employment in a year: business income minus business expenses.',
},
{
name: 'tip_income',
label: 'Tips',
description: 'Tips received in a year, in cash or through an employer.',
},
{
name: 'unemployment_compensation',
label: 'Unemployment benefits',
description: 'Unemployment insurance benefits received in a year.',
},
{
name: 'social_security_retirement',
label: 'Social Security retirement',
description: 'Social Security retirement benefits received in a year.',
},
{
name: 'social_security_disability',
label: 'Social Security disability',
description: 'Social Security Disability Insurance (SSDI) benefits received in a year.',
},
{
name: 'taxable_private_pension_income',
label: 'Pension income',
description: 'Taxable pension income from a private (non-government) employer in a year.',
},
{
name: 'taxable_interest_income',
label: 'Interest income',
description: 'Taxable interest from bank accounts, bonds and similar holdings in a year.',
},
{
name: 'other_medical_expenses',
label: 'Medical expenses',
description: 'Out-of-pocket medical costs in a year, other than insurance premiums.',
},
{
name: 'qualified_dividend_income',
label: 'Qualified dividends',
description: 'Dividends taxed at the lower capital gains rates, received in a year.',
},
{
name: 'child_support_received',
label: 'Child support received',
description: 'Child support payments received in a year.',
},
{
name: 'health_insurance_premiums',
label: 'Health insurance premiums',
description: 'Health insurance premiums this person pays in a year.',
},
{
name: 'student_loan_interest',
label: 'Student loan interest',
description: 'Interest paid on student loans in a year.',
},
// The household's
{
name: 'spm_unit_pre_subsidy_childcare_expenses',
label: 'Child care expenses',
description: 'What the household pays for child care in a year, before any child care subsidy.',
},
{
name: 'pre_subsidy_electricity_expense',
label: 'Electricity',
description:
'What the household pays for electricity in a year, before any utility assistance.',
},
{
name: 'gas_expense',
label: 'Gas',
description: 'What the household pays for natural gas in a year.',
},
{
name: 'water_expense',
label: 'Water',
description: 'What the household pays for water in a year.',
},
{
name: 'mortgage_payments',
label: 'Mortgage payments',
description: 'Mortgage payments the household makes in a year.',
},
{
name: 'homeowners_insurance',
label: 'Homeowners insurance',
description: 'Homeowners insurance premiums the household pays in a year.',
},
{
name: 'spm_unit_assets',
label: 'Savings and assets',
description:
"The value of the household's savings and other assets, which benefit asset tests count.",
},
{
name: 'household_vehicles_owned',
label: 'Vehicles owned',
description: 'The number of vehicles the household owns.',
},
];

const TOPIC_ICONS: Record<string, typeof IconHome> = {
Income: IconCash,
People: IconUsers,
'Housing and utilities': IconHome,
Health: IconStethoscope,
'Other expenses': IconWallet,
Assets: IconPigMoney,
'Benefits and taxes': IconReceiptTax,
'State and local': IconBuildingCommunity,
Other: IconDots,
};

export function householdTopicIcon(group: string) {
const Icon = TOPIC_ICONS[group] ?? IconDots;
return <Icon size={16} stroke={1.75} />;
}

const SHORT_NAMES = new Map(COMMON_HOUSEHOLD_INPUTS.map((input) => [input.name, input.label]));

/**
* An input's name as the picker lists it: a tile's short name, else the
* model's label in sentence case without its modeling notes.
*/
export function householdInputName(variable: VariableInfo): string {
const short = SHORT_NAMES.get(variable.name);
if (short) {
return short;
}
const label = variable.label.replace(/\s*\(before labor supply responses\)/i, '').trim();
return label.charAt(0).toUpperCase() + label.slice(1);
}

const DESCRIPTIONS = new Map(
COMMON_HOUSEHOLD_INPUTS.map((input) => [input.name, input.description])
);

/** What to enter for an input: the model's documentation, else ours for a featured one. */
export function householdInputDescription(variable: VariableInfo): string | undefined {
return variable.documentation || DESCRIPTIONS.get(variable.name);
}
Loading
Loading