@retiregolden/planner-ui 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +661 -0
- package/README.md +181 -0
- package/package.json +77 -0
- package/src/App.tsx +246 -0
- package/src/RouteErrorBoundary.tsx +45 -0
- package/src/assets/hero.png +0 -0
- package/src/assets/react.svg +1 -0
- package/src/assets/vite.svg +1 -0
- package/src/data/fedInvestClient.ts +113 -0
- package/src/data/localStore.ts +42 -0
- package/src/data/planOrigin.ts +24 -0
- package/src/data/planStore.ts +165 -0
- package/src/data/v2Backup.ts +101 -0
- package/src/import/ImportPage.tsx +347 -0
- package/src/import/ReviewChecklistView.tsx +38 -0
- package/src/import/brokerCsv.ts +395 -0
- package/src/import/csv.ts +133 -0
- package/src/import/genericCsv.ts +224 -0
- package/src/import/projectionLab.ts +350 -0
- package/src/import/reviewChecklist.ts +33 -0
- package/src/import/tenForty.ts +275 -0
- package/src/index.css +630 -0
- package/src/index.ts +16 -0
- package/src/learn/ArticleBody.tsx +78 -0
- package/src/learn/ArticlePage.tsx +57 -0
- package/src/learn/GlossaryPage.tsx +33 -0
- package/src/learn/LearnAboutScreen.tsx +41 -0
- package/src/learn/LearnCards.tsx +41 -0
- package/src/learn/LearnLink.tsx +91 -0
- package/src/learn/LearningCenterPage.tsx +114 -0
- package/src/learn/SourcesPage.tsx +98 -0
- package/src/learn/components/ArticleFigure.tsx +34 -0
- package/src/learn/components/ArticleShell.tsx +86 -0
- package/src/learn/components/ComparisonTable.tsx +42 -0
- package/src/learn/components/FormulaBlock.tsx +34 -0
- package/src/learn/components/PurchasingPowerChart.tsx +41 -0
- package/src/learn/components/RelatedArticles.tsx +27 -0
- package/src/learn/components/ScenarioCard.tsx +24 -0
- package/src/learn/components/SourceList.tsx +23 -0
- package/src/learn/components/charts.tsx +21 -0
- package/src/learn/content/about-retiregolden.ts +100 -0
- package/src/learn/content/aca-premium-tax-credits-and-magi.ts +103 -0
- package/src/learn/content/account-types-overview.ts +106 -0
- package/src/learn/content/after-tax-estate.ts +111 -0
- package/src/learn/content/agi-magi-and-taxable-income.ts +112 -0
- package/src/learn/content/appealing-irmaa-ssa-44.ts +95 -0
- package/src/learn/content/assumption-general-inflation.ts +82 -0
- package/src/learn/content/assumption-healthcare-inflation.ts +85 -0
- package/src/learn/content/assumption-heir-tax-rate.ts +79 -0
- package/src/learn/content/assumption-investment-returns.ts +90 -0
- package/src/learn/content/assumption-longevity-planning-age.ts +78 -0
- package/src/learn/content/assumption-recent-magi.ts +83 -0
- package/src/learn/content/assumption-social-security-cola.ts +89 -0
- package/src/learn/content/assumption-social-security-trust-fund.ts +83 -0
- package/src/learn/content/assumption-state-tax-override.ts +79 -0
- package/src/learn/content/beneficiaries-and-account-titling.ts +99 -0
- package/src/learn/content/break-even-useful-lens.ts +94 -0
- package/src/learn/content/building-a-retirement-spending-budget.ts +100 -0
- package/src/learn/content/cola-and-inflation-protection.ts +102 -0
- package/src/learn/content/divorced-spousal-and-survivor-records.ts +104 -0
- package/src/learn/content/dynamic-spending-guardrails.ts +90 -0
- package/src/learn/content/earnings-test-before-fra.ts +100 -0
- package/src/learn/content/employer-match-and-contribution-order.ts +104 -0
- package/src/learn/content/examplePlanArticles.ts +525 -0
- package/src/learn/content/fees-expense-ratios-and-compounding-drag.ts +98 -0
- package/src/learn/content/fi-number-and-four-percent-rule.ts +64 -0
- package/src/learn/content/filling-a-tax-bracket-with-roth-conversions.ts +98 -0
- package/src/learn/content/funded-ratio.ts +70 -0
- package/src/learn/content/healthcare-after-65.ts +103 -0
- package/src/learn/content/healthcare-before-65.ts +104 -0
- package/src/learn/content/historical-vs-random-return-models.ts +101 -0
- package/src/learn/content/how-assumptions-change-the-answer.ts +105 -0
- package/src/learn/content/how-much-can-i-spend.ts +105 -0
- package/src/learn/content/how-social-security-is-taxed.ts +95 -0
- package/src/learn/content/how-the-optimizer-thinks.ts +102 -0
- package/src/learn/content/how-the-optimizer-values-after-tax-estate.ts +97 -0
- package/src/learn/content/how-to-model-accumulation.ts +67 -0
- package/src/learn/content/how-to-read-a-retirement-projection.ts +115 -0
- package/src/learn/content/hsas-and-qualified-medical-expenses.ts +108 -0
- package/src/learn/content/hsas-as-retirement-accounts.ts +101 -0
- package/src/learn/content/inflation-risk.ts +98 -0
- package/src/learn/content/inherited-ira-10-year-rule.ts +105 -0
- package/src/learn/content/insurance-in-your-retirement-plan.ts +103 -0
- package/src/learn/content/irmaa-two-year-lookback.ts +99 -0
- package/src/learn/content/long-term-care-costs-and-insurance.ts +103 -0
- package/src/learn/content/long-term-care-insurance-as-risk-transfer.ts +98 -0
- package/src/learn/content/longevity-risk.ts +99 -0
- package/src/learn/content/marginal-vs-effective-tax-rate.ts +98 -0
- package/src/learn/content/medicare-part-b-vs-part-d-irmaa.ts +102 -0
- package/src/learn/content/mortality-weighted-social-security.ts +113 -0
- package/src/learn/content/moving-to-retiregolden.ts +86 -0
- package/src/learn/content/niit-high-income-investment-tax.ts +98 -0
- package/src/learn/content/ordinary-income-vs-capital-gains.ts +103 -0
- package/src/learn/content/paying-conversion-taxes-taxable-vs-ira.ts +102 -0
- package/src/learn/content/pensions-and-annuities.ts +101 -0
- package/src/learn/content/permanent-life-insurance-in-a-plan.ts +106 -0
- package/src/learn/content/pia-aime-and-bend-points.ts +103 -0
- package/src/learn/content/planner-overview.ts +106 -0
- package/src/learn/content/planning-for-couples-and-survivor-years.ts +108 -0
- package/src/learn/content/privacy-what-stays-in-your-browser.ts +99 -0
- package/src/learn/content/qcds-qualified-charitable-distributions.ts +101 -0
- package/src/learn/content/reading-the-results-page.ts +96 -0
- package/src/learn/content/reading-the-social-security-analysis-page.ts +106 -0
- package/src/learn/content/real-estate-home-equity-and-debt.ts +100 -0
- package/src/learn/content/reports-csv-exports-and-sharing.ts +101 -0
- package/src/learn/content/risk-based-guardrails.ts +100 -0
- package/src/learn/content/rmds-required-minimum-distributions.ts +100 -0
- package/src/learn/content/roth-conversion-basics.ts +104 -0
- package/src/learn/content/rsus-and-espp.ts +101 -0
- package/src/learn/content/rule-of-55-and-72t.ts +107 -0
- package/src/learn/content/savings-rate-biggest-lever.ts +66 -0
- package/src/learn/content/seed-your-plan-from-your-tax-return.ts +93 -0
- package/src/learn/content/sensitivity-testing-what-changes-the-answer.ts +104 -0
- package/src/learn/content/sequence-of-returns-risk.ts +98 -0
- package/src/learn/content/social-security-bridge.ts +67 -0
- package/src/learn/content/social-security-claiming-age-basics.ts +113 -0
- package/src/learn/content/social-security-taxes-vs-benefits.ts +76 -0
- package/src/learn/content/spending-profiles-and-the-retirement-smile.ts +92 -0
- package/src/learn/content/spousal-and-survivor-benefits.ts +120 -0
- package/src/learn/content/ssdi-and-retirement-planning.ts +72 -0
- package/src/learn/content/standard-deduction-senior-deduction-and-itemizing.ts +97 -0
- package/src/learn/content/state-income-taxes-in-retirement.ts +97 -0
- package/src/learn/content/step-up-in-basis.ts +102 -0
- package/src/learn/content/survivor-planning-for-couples.ts +110 -0
- package/src/learn/content/survivor-spending-in-couple-plans.ts +98 -0
- package/src/learn/content/tax-cliffs-and-bracket-edges.ts +105 -0
- package/src/learn/content/tax-loss-and-gain-harvesting.ts +99 -0
- package/src/learn/content/taxable-brokerage-basis-and-capital-gains.ts +99 -0
- package/src/learn/content/three-big-questions-spending-time-risk.ts +103 -0
- package/src/learn/content/tips-ladders.ts +92 -0
- package/src/learn/content/todays-dollars-vs-future-dollars.ts +107 -0
- package/src/learn/content/traditional-vs-roth-contributions.ts +113 -0
- package/src/learn/content/troubleshooting-surprising-results.ts +105 -0
- package/src/learn/content/trust-fund-haircut-scenarios.ts +101 -0
- package/src/learn/content/understanding-monte-carlo-success-rate.ts +118 -0
- package/src/learn/content/understanding-your-plan-assumptions.ts +134 -0
- package/src/learn/content/using-assumptions-and-provenance.ts +98 -0
- package/src/learn/content/using-scenarios-to-compare-choices.ts +99 -0
- package/src/learn/content/what-changes-when-you-move-states.ts +141 -0
- package/src/learn/content/what-is-fire.ts +65 -0
- package/src/learn/content/what-monte-carlo-proves.ts +98 -0
- package/src/learn/content/what-retiregolden-models.ts +103 -0
- package/src/learn/content/what-retirement-healthcare-really-costs.ts +117 -0
- package/src/learn/content/why-95-percent-is-not-a-guarantee.ts +98 -0
- package/src/learn/content/why-roth-conversions-raise-other-costs.ts +106 -0
- package/src/learn/content/why-small-tax-cliffs-can-matter.ts +109 -0
- package/src/learn/content/widows-penalty-and-survivor-brackets.ts +106 -0
- package/src/learn/content/withdrawal-order-basics.ts +105 -0
- package/src/learn/glossary.ts +191 -0
- package/src/learn/inlineMarkdown.tsx +54 -0
- package/src/learn/learn.css +537 -0
- package/src/learn/learningRegistry.ts +502 -0
- package/src/longevity/LongevityResults.tsx +85 -0
- package/src/longevity/LongevityWizard.tsx +305 -0
- package/src/longevity/constants.ts +15 -0
- package/src/longevity/factors.ts +125 -0
- package/src/longevity/model.ts +31 -0
- package/src/longevity/persistedGuard.ts +129 -0
- package/src/longevity/storage.ts +40 -0
- package/src/mc/messages.ts +118 -0
- package/src/mc/monteCarlo.worker.ts +44 -0
- package/src/mc/pool.ts +267 -0
- package/src/mc/runRequest.ts +125 -0
- package/src/optimize/messages.ts +84 -0
- package/src/optimize/optimize.worker.ts +29 -0
- package/src/optimize/runOptimize.ts +92 -0
- package/src/optimize/runSpendingSolve.ts +47 -0
- package/src/optimize/runner.ts +21 -0
- package/src/optimize/spendingMessages.ts +44 -0
- package/src/optimize/spendingRunner.ts +21 -0
- package/src/optimize/spendingSolve.worker.ts +18 -0
- package/src/planner/AssumptionsCardPage.tsx +136 -0
- package/src/planner/BucketLensCard.tsx +114 -0
- package/src/planner/ComparePlansPage.tsx +219 -0
- package/src/planner/DisclaimerPage.tsx +88 -0
- package/src/planner/HowTestedPage.tsx +159 -0
- package/src/planner/LiveStatus.tsx +15 -0
- package/src/planner/LongevityModal.tsx +55 -0
- package/src/planner/Modal.tsx +97 -0
- package/src/planner/MonteCarloPage.tsx +907 -0
- package/src/planner/OptimizePage.tsx +611 -0
- package/src/planner/PlanContext.tsx +198 -0
- package/src/planner/PlanPickerPage.tsx +124 -0
- package/src/planner/PlanWorkspace.tsx +290 -0
- package/src/planner/ProvenancePanel.tsx +45 -0
- package/src/planner/RelocationComparePage.tsx +485 -0
- package/src/planner/ReportPage.tsx +375 -0
- package/src/planner/ResultsPage.tsx +817 -0
- package/src/planner/ScenariosPage.tsx +285 -0
- package/src/planner/SocialSecuritySection.tsx +556 -0
- package/src/planner/SpendingSolverPage.tsx +512 -0
- package/src/planner/SsAnalysisPage.tsx +1134 -0
- package/src/planner/SurvivalPercentileModal.tsx +161 -0
- package/src/planner/SurvivorTransitionPage.tsx +286 -0
- package/src/planner/assumptionsExport.ts +371 -0
- package/src/planner/bucketLens.ts +89 -0
- package/src/planner/chartFrame.ts +8 -0
- package/src/planner/chartStyle.ts +11 -0
- package/src/planner/dialogViews.tsx +184 -0
- package/src/planner/dialogs.tsx +133 -0
- package/src/planner/examples/ExampleLibrary.tsx +189 -0
- package/src/planner/examples/ExamplePreviewBanner.tsx +55 -0
- package/src/planner/examples/ExamplesPage.tsx +25 -0
- package/src/planner/examples/OpenExampleButton.tsx +61 -0
- package/src/planner/examples/buildAggressiveSaver.ts +102 -0
- package/src/planner/examples/buildAnnuityEstate.ts +137 -0
- package/src/planner/examples/buildBaristaFire.ts +115 -0
- package/src/planner/examples/buildBracketFillRoth.ts +65 -0
- package/src/planner/examples/buildBridgeEarlyRetirement.ts +94 -0
- package/src/planner/examples/buildBrokerageNoHsa.ts +109 -0
- package/src/planner/examples/buildCoastFire.ts +88 -0
- package/src/planner/examples/buildContext.ts +20 -0
- package/src/planner/examples/buildEarlyCareerMatch.ts +93 -0
- package/src/planner/examples/buildEarlyRetireeAca.ts +61 -0
- package/src/planner/examples/buildExampleCouple.ts +103 -0
- package/src/planner/examples/buildFixedTargetSpending.ts +74 -0
- package/src/planner/examples/buildGlidepathAllocation.ts +131 -0
- package/src/planner/examples/buildGuardrailsFlex.ts +120 -0
- package/src/planner/examples/buildHsaPropertyDepth.ts +109 -0
- package/src/planner/examples/buildHsaStealthRetirement.ts +97 -0
- package/src/planner/examples/buildLeanFatFire.ts +109 -0
- package/src/planner/examples/buildLtcShock.ts +62 -0
- package/src/planner/examples/buildMovingStateTax.ts +53 -0
- package/src/planner/examples/buildNoAnnuityBrokerage.ts +92 -0
- package/src/planner/examples/buildRmdIrmaa.ts +55 -0
- package/src/planner/examples/buildSalaryGrowthEscalation.ts +96 -0
- package/src/planner/examples/buildStaticAllocationControl.ts +96 -0
- package/src/planner/examples/buildSurvivorYears.ts +62 -0
- package/src/planner/examples/buildUnderSavedSingle.ts +51 -0
- package/src/planner/examples/exampleCopy.ts +23 -0
- package/src/planner/examples/loadExample.ts +90 -0
- package/src/planner/examples/registry.ts +313 -0
- package/src/planner/explainPanels.tsx +233 -0
- package/src/planner/fields.tsx +381 -0
- package/src/planner/format.ts +33 -0
- package/src/planner/home/DataAndPrivacyCard.tsx +56 -0
- package/src/planner/home/GettingStartedPaths.tsx +46 -0
- package/src/planner/home/GettingStartedReopener.tsx +32 -0
- package/src/planner/home/StartHereLinks.tsx +22 -0
- package/src/planner/home/WelcomeHero.tsx +39 -0
- package/src/planner/home/YourPlans.tsx +72 -0
- package/src/planner/home/importErrorMessage.ts +22 -0
- package/src/planner/home/startHereSlugs.ts +7 -0
- package/src/planner/home/useHomeData.ts +190 -0
- package/src/planner/home/useHomeMode.ts +47 -0
- package/src/planner/householdActions.ts +22 -0
- package/src/planner/insights/InsightCardView.tsx +340 -0
- package/src/planner/insights/InsightsPage.tsx +204 -0
- package/src/planner/insights/categoryLabels.ts +11 -0
- package/src/planner/learnLinks.ts +85 -0
- package/src/planner/marketModelPicker.ts +172 -0
- package/src/planner/optimizePageChart.ts +40 -0
- package/src/planner/optimizePageClaim.ts +64 -0
- package/src/planner/planCompleteness.ts +27 -0
- package/src/planner/planContextCore.ts +26 -0
- package/src/planner/planner.css +2304 -0
- package/src/planner/provenanceLinks.ts +25 -0
- package/src/planner/sections/AccountFields.tsx +872 -0
- package/src/planner/sections/AccountsSection.tsx +89 -0
- package/src/planner/sections/AllocationPanel.tsx +261 -0
- package/src/planner/sections/AssumptionsSection.tsx +256 -0
- package/src/planner/sections/HouseholdSection.tsx +243 -0
- package/src/planner/sections/IncomeFloorSection.tsx +418 -0
- package/src/planner/sections/IncomeSection.tsx +170 -0
- package/src/planner/sections/InsuranceSection.tsx +362 -0
- package/src/planner/sections/SpendingSection.tsx +904 -0
- package/src/planner/sections/StrategySection.tsx +349 -0
- package/src/planner/sections/UpdateBalancesPanel.tsx +182 -0
- package/src/planner/sections/sectionHelpers.ts +48 -0
- package/src/planner/sections/shared.tsx +15 -0
- package/src/planner/sections.tsx +15 -0
- package/src/planner/ssAnalysis.ts +325 -0
- package/src/planner/successBand.ts +20 -0
- package/src/planner/survivorAnalysis.ts +277 -0
- package/src/planner/usStates.ts +19 -0
- package/src/planner/useMcSuccessRate.ts +77 -0
- package/src/planner/useProjection.ts +63 -0
- package/src/relocation/messages.ts +21 -0
- package/src/relocation/relocation.worker.ts +18 -0
- package/src/relocation/runRelocation.ts +17 -0
- package/src/relocation/runner.ts +22 -0
- package/src/report/brandingContext.ts +15 -0
- package/src/report/downloadReport.ts +34 -0
- package/src/report/reportHtml.ts +547 -0
- package/src/routes/LearnRoutes.tsx +46 -0
- package/src/routes/PlanRoutes.tsx +55 -0
- package/src/routes/RouteFallback.tsx +9 -0
- package/src/socialSecurity/breakEven.ts +107 -0
- package/src/socialSecurity/expectedPv.ts +164 -0
- package/src/socialSecurity/explain.ts +92 -0
- package/src/socialSecurity/ficaReturn.ts +81 -0
- package/src/socialSecurity/persistedSsGuard.ts +138 -0
- package/src/socialSecurity/ssFormUtils.ts +48 -0
- package/src/socialSecurity/ssaStatementXml.ts +156 -0
- package/src/socialSecurity/storage.ts +69 -0
- package/src/socialSecurity/survivorSwitching.ts +153 -0
- package/src/testSupport/samplePlan.ts +2 -0
- package/src/workers/run.ts +45 -0
|
@@ -0,0 +1,556 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Social Security entry. Reuses the household profile (date of birth, planning
|
|
3
|
+
* age) so the user only adds what's new per person: the benefit (a quick PIA
|
|
4
|
+
* or an imported earnings record) and a claiming age. The claim-age trade-off
|
|
5
|
+
* analysis lives on Explore → Social Security.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { useState, type ChangeEvent } from 'react'
|
|
9
|
+
import { Link } from 'react-router-dom'
|
|
10
|
+
|
|
11
|
+
import type { FormerSpouse, IncomeStream, Person } from '@retiregolden/engine/model/plan'
|
|
12
|
+
import {
|
|
13
|
+
bendTierForAime,
|
|
14
|
+
CREDITS_FOR_ELIGIBILITY,
|
|
15
|
+
estimateCredits,
|
|
16
|
+
replaceZeroYearGain,
|
|
17
|
+
summarizeComputation,
|
|
18
|
+
} from '../socialSecurity/explain'
|
|
19
|
+
import { effectiveBirthYear, fraForBirthYear } from '@retiregolden/engine/socialSecurity/nra'
|
|
20
|
+
import type { PiaFromEarningsResult } from '@retiregolden/engine/socialSecurity/piaFromEarnings'
|
|
21
|
+
import { parseSsaStatementXml } from '../socialSecurity/ssaStatementXml'
|
|
22
|
+
import { usePlan } from './planContextCore'
|
|
23
|
+
import { CheckboxField, DateField, NumberField, MoneyField, SelectField } from './fields'
|
|
24
|
+
import { LearnAboutScreen } from '../learn/LearnAboutScreen'
|
|
25
|
+
import { fmtMoney } from './format'
|
|
26
|
+
import { dobParts, resolvePia } from './ssAnalysis'
|
|
27
|
+
|
|
28
|
+
const newId = () => crypto.randomUUID()
|
|
29
|
+
|
|
30
|
+
type SsStream = Extract<IncomeStream, { type: 'socialSecurity' }>
|
|
31
|
+
|
|
32
|
+
/** Teaching panel: how the earnings history becomes a PIA (AIME, zero years, bend tier). */
|
|
33
|
+
function AimeExplainer({ detail, sampleEarnings }: { detail: PiaFromEarningsResult; sampleEarnings: number | null }) {
|
|
34
|
+
const s = summarizeComputation(detail)
|
|
35
|
+
const tier = bendTierForAime(detail.aime, detail.eligibilityYear)
|
|
36
|
+
const gain = sampleEarnings && sampleEarnings > 0 ? replaceZeroYearGain(detail, sampleEarnings) : null
|
|
37
|
+
const tierAside =
|
|
38
|
+
tier.label === '90%' ? ' — the most valuable tier' : tier.label === '15%' ? ' — the least valuable tier, so extra earnings add little' : ''
|
|
39
|
+
return (
|
|
40
|
+
<details className="ss-explainer">
|
|
41
|
+
<summary>How this benefit is built — AIME & bend points</summary>
|
|
42
|
+
<ul>
|
|
43
|
+
<li>
|
|
44
|
+
Averages your top <strong>{s.computationYearCount}</strong> earning years (wage-indexed) into an AIME of{' '}
|
|
45
|
+
<strong>{fmtMoney(detail.aime)}/mo</strong>.
|
|
46
|
+
</li>
|
|
47
|
+
{s.zeroYearsInAime > 0 ? (
|
|
48
|
+
<li>
|
|
49
|
+
<strong>{s.zeroYearsInAime}</strong> of those {s.computationYearCount} years are $0 — each one pulls the
|
|
50
|
+
average down.
|
|
51
|
+
</li>
|
|
52
|
+
) : (
|
|
53
|
+
<li>Every averaged year has earnings — no zero years dragging the average down.</li>
|
|
54
|
+
)}
|
|
55
|
+
<li>
|
|
56
|
+
Your next dollar of AIME is credited at the <strong>{tier.label}</strong> bend-point rate{tierAside}.
|
|
57
|
+
</li>
|
|
58
|
+
{gain !== null && s.zeroYearsInAime > 0 ? (
|
|
59
|
+
<li>
|
|
60
|
+
Replacing one $0 year with about {fmtMoney(sampleEarnings!)} of earnings would add roughly{' '}
|
|
61
|
+
<strong>{fmtMoney(gain)}/mo</strong> — a rough estimate at the current bend rate.
|
|
62
|
+
</li>
|
|
63
|
+
) : null}
|
|
64
|
+
</ul>
|
|
65
|
+
</details>
|
|
66
|
+
)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** 40-credit (10-year) covered-work eligibility gate with an optional manual override. */
|
|
70
|
+
function EligibilityNote({ stream, onCommitCredits }: { stream: SsStream; onCommitCredits: (v: number | null) => void }) {
|
|
71
|
+
const est = estimateCredits(stream.earnings ?? [], stream.coveredQuarters)
|
|
72
|
+
return (
|
|
73
|
+
<div style={{ marginTop: '0.6rem' }}>
|
|
74
|
+
<div className="form-grid">
|
|
75
|
+
<NumberField
|
|
76
|
+
label="Covered-work credits"
|
|
77
|
+
help="SSA 'credits' (formerly quarters of coverage). You generally need 40 (about 10 years of covered work) to qualify for your own retirement benefit. Leave blank to estimate from the earnings above; set it if you know the exact number."
|
|
78
|
+
hint={est.estimated ? `Blank = estimated ${est.credits} from your earnings (40 needed).` : '40 needed to qualify.'}
|
|
79
|
+
value={stream.coveredQuarters ?? null}
|
|
80
|
+
allowNull
|
|
81
|
+
min={0}
|
|
82
|
+
max={40}
|
|
83
|
+
onCommit={(v) => onCommitCredits(v === null ? null : Math.round(v))}
|
|
84
|
+
/>
|
|
85
|
+
</div>
|
|
86
|
+
{!est.eligible ? (
|
|
87
|
+
<div className="callout callout--warn">
|
|
88
|
+
{est.estimated ? 'Estimated' : 'Entered'} <strong>{est.credits}</strong> of the {CREDITS_FOR_ELIGIBILITY}{' '}
|
|
89
|
+
credits needed — not yet eligible for a personal retirement benefit. Add covered-work years above or set the
|
|
90
|
+
credit count if the estimate is off (it assumes 4 credits per substantial year).
|
|
91
|
+
</div>
|
|
92
|
+
) : null}
|
|
93
|
+
</div>
|
|
94
|
+
)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Editor for divorced-spousal / survivor benefit records on a former spouse's PIA. */
|
|
98
|
+
function FormerSpousesEditor({
|
|
99
|
+
stream,
|
|
100
|
+
setStream,
|
|
101
|
+
householdIsSingle,
|
|
102
|
+
}: {
|
|
103
|
+
stream: SsStream
|
|
104
|
+
setStream: (mut: (s: SsStream) => void) => void
|
|
105
|
+
householdIsSingle: boolean
|
|
106
|
+
}) {
|
|
107
|
+
const records = stream.formerSpouses ?? []
|
|
108
|
+
const add = (relationship: 'divorced' | 'deceased') =>
|
|
109
|
+
setStream((s) => {
|
|
110
|
+
const list = s.formerSpouses ?? (s.formerSpouses = [])
|
|
111
|
+
list.push({
|
|
112
|
+
id: newId(),
|
|
113
|
+
relationship,
|
|
114
|
+
dob: '1960-01-01',
|
|
115
|
+
piaMonthly: 0,
|
|
116
|
+
marriageYears: relationship === 'divorced' ? 10 : 1,
|
|
117
|
+
remarriedAtAge: null,
|
|
118
|
+
})
|
|
119
|
+
})
|
|
120
|
+
const updateRecord = (id: string, mut: (r: FormerSpouse) => void) =>
|
|
121
|
+
setStream((s) => {
|
|
122
|
+
const r = s.formerSpouses?.find((x) => x.id === id)
|
|
123
|
+
if (r) mut(r)
|
|
124
|
+
})
|
|
125
|
+
const remove = (id: string) =>
|
|
126
|
+
setStream((s) => {
|
|
127
|
+
if (s.formerSpouses) s.formerSpouses = s.formerSpouses.filter((x) => x.id !== id)
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
return (
|
|
131
|
+
<div style={{ marginTop: '0.8rem' }}>
|
|
132
|
+
<h4 style={{ margin: '0 0 0.3rem' }}>Former spouses</h4>
|
|
133
|
+
<p className="card-hint">
|
|
134
|
+
A 10+ year marriage to a living ex (while you're currently unmarried) can pay a divorced-spousal benefit of up to
|
|
135
|
+
half their benefit; a deceased former spouse can pay a survivor benefit. You receive whichever is largest — your
|
|
136
|
+
own, spousal, or survivor — so add any that might apply.
|
|
137
|
+
</p>
|
|
138
|
+
{records.map((r) => (
|
|
139
|
+
<div key={r.id} className="item-row">
|
|
140
|
+
<div className="item-row-head">
|
|
141
|
+
<span className="item-row-title">
|
|
142
|
+
<span className="type-chip">{r.relationship === 'divorced' ? 'Divorced ex' : 'Survivor'}</span>
|
|
143
|
+
</span>
|
|
144
|
+
<button type="button" className="btn-ghost btn-ghost-danger" onClick={() => remove(r.id)}>
|
|
145
|
+
Remove
|
|
146
|
+
</button>
|
|
147
|
+
</div>
|
|
148
|
+
<div className="form-grid">
|
|
149
|
+
<SelectField
|
|
150
|
+
label="Type"
|
|
151
|
+
value={r.relationship}
|
|
152
|
+
options={[
|
|
153
|
+
{ value: 'divorced', label: 'Living ex (divorced-spousal)' },
|
|
154
|
+
{ value: 'deceased', label: 'Deceased (survivor)' },
|
|
155
|
+
]}
|
|
156
|
+
onCommit={(v) => updateRecord(r.id, (x) => (x.relationship = v as FormerSpouse['relationship']))}
|
|
157
|
+
/>
|
|
158
|
+
<DateField label="Their date of birth" value={r.dob} onCommit={(v) => updateRecord(r.id, (x) => (x.dob = v))} />
|
|
159
|
+
<MoneyField
|
|
160
|
+
label="Their PIA (monthly at FRA)"
|
|
161
|
+
help="Your estimate of the ex/deceased spouse's monthly benefit at their full retirement age, today's dollars."
|
|
162
|
+
value={r.piaMonthly}
|
|
163
|
+
onCommit={(v) => updateRecord(r.id, (x) => (x.piaMonthly = v ?? 0))}
|
|
164
|
+
/>
|
|
165
|
+
<NumberField
|
|
166
|
+
label="Years married"
|
|
167
|
+
hint={r.relationship === 'divorced' ? '10+ for divorced-spousal.' : '9 months (0.75) minimum.'}
|
|
168
|
+
value={r.marriageYears}
|
|
169
|
+
min={0}
|
|
170
|
+
max={75}
|
|
171
|
+
step={r.relationship === 'divorced' ? 1 : 0.25}
|
|
172
|
+
onCommit={(v) => updateRecord(r.id, (x) => (x.marriageYears = v ?? 0))}
|
|
173
|
+
/>
|
|
174
|
+
{r.relationship === 'deceased' ? (
|
|
175
|
+
<NumberField
|
|
176
|
+
label="Age you remarried"
|
|
177
|
+
help="Remarrying before 60 forfeits this survivor benefit; at or after 60 preserves it. Leave blank if you didn't remarry after this spouse died."
|
|
178
|
+
value={r.remarriedAtAge}
|
|
179
|
+
allowNull
|
|
180
|
+
min={0}
|
|
181
|
+
max={120}
|
|
182
|
+
onCommit={(v) => updateRecord(r.id, (x) => (x.remarriedAtAge = v === null ? null : Math.round(v)))}
|
|
183
|
+
/>
|
|
184
|
+
) : null}
|
|
185
|
+
{r.relationship === 'deceased' ? (
|
|
186
|
+
<NumberField
|
|
187
|
+
label="When they claimed (age)"
|
|
188
|
+
hint="Leave blank if they claimed at/after FRA."
|
|
189
|
+
help="The age the deceased claimed their own benefit. If they claimed early (before FRA), the widow's-limit (RIB-LIM) caps your survivor benefit at the larger of their reduced benefit or 82.5% of their PIA — usually higher than their reduced amount. Leave blank if they claimed at or after FRA (the safe default)."
|
|
190
|
+
value={r.deceasedClaimAge?.years ?? null}
|
|
191
|
+
allowNull
|
|
192
|
+
min={62}
|
|
193
|
+
max={70}
|
|
194
|
+
onCommit={(v) =>
|
|
195
|
+
updateRecord(r.id, (x) => {
|
|
196
|
+
if (v === null) x.deceasedClaimAge = null
|
|
197
|
+
else x.deceasedClaimAge = { years: Math.round(v), months: x.deceasedClaimAge?.months ?? 0 }
|
|
198
|
+
})
|
|
199
|
+
}
|
|
200
|
+
/>
|
|
201
|
+
) : null}
|
|
202
|
+
{r.relationship === 'deceased' && r.deceasedClaimAge ? (
|
|
203
|
+
<NumberField
|
|
204
|
+
label="When they claimed (+ months)"
|
|
205
|
+
value={r.deceasedClaimAge.months}
|
|
206
|
+
min={0}
|
|
207
|
+
max={11}
|
|
208
|
+
onCommit={(v) =>
|
|
209
|
+
updateRecord(r.id, (x) => {
|
|
210
|
+
if (x.deceasedClaimAge) x.deceasedClaimAge = { ...x.deceasedClaimAge, months: Math.round(v ?? 0) }
|
|
211
|
+
})
|
|
212
|
+
}
|
|
213
|
+
/>
|
|
214
|
+
) : null}
|
|
215
|
+
</div>
|
|
216
|
+
{r.relationship === 'divorced' && !householdIsSingle ? (
|
|
217
|
+
<p className="field-hint" style={{ color: 'var(--warn)' }}>
|
|
218
|
+
Divorced-spousal needs you to be currently unmarried — with a partner on this plan it won't apply (you'd get
|
|
219
|
+
the current-spouse top-up instead).
|
|
220
|
+
</p>
|
|
221
|
+
) : null}
|
|
222
|
+
</div>
|
|
223
|
+
))}
|
|
224
|
+
<div className="add-row">
|
|
225
|
+
<button type="button" className="btn btn-secondary btn-small" onClick={() => add('divorced')}>
|
|
226
|
+
+ Divorced ex-spouse
|
|
227
|
+
</button>
|
|
228
|
+
<button type="button" className="btn btn-secondary btn-small" onClick={() => add('deceased')}>
|
|
229
|
+
+ Deceased former spouse
|
|
230
|
+
</button>
|
|
231
|
+
</div>
|
|
232
|
+
</div>
|
|
233
|
+
)
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function PersonSsCard({ person, personIndex }: { person: Person; personIndex: number }) {
|
|
237
|
+
const { plan, update } = usePlan()
|
|
238
|
+
const stream = plan.incomes.find((s): s is SsStream => s.type === 'socialSecurity' && s.personId === person.id)
|
|
239
|
+
const { y, m, d } = dobParts(person)
|
|
240
|
+
const fra = fraForBirthYear(effectiveBirthYear(y, m, d))
|
|
241
|
+
const [xmlNote, setXmlNote] = useState<{ kind: 'ok' | 'error'; text: string } | null>(null)
|
|
242
|
+
|
|
243
|
+
if (!stream) {
|
|
244
|
+
return (
|
|
245
|
+
<div className="item-row">
|
|
246
|
+
<div className="item-row-head">
|
|
247
|
+
<span className="item-row-title">
|
|
248
|
+
<span className="type-chip">{personIndex === 0 ? 'Primary' : 'Partner'}</span>
|
|
249
|
+
{person.name}
|
|
250
|
+
</span>
|
|
251
|
+
</div>
|
|
252
|
+
<p className="card-hint" style={{ marginBottom: '0.6rem' }}>
|
|
253
|
+
No Social Security benefit is modeled for {person.name} yet.
|
|
254
|
+
</p>
|
|
255
|
+
<button
|
|
256
|
+
type="button"
|
|
257
|
+
className="btn btn-secondary btn-small"
|
|
258
|
+
onClick={() =>
|
|
259
|
+
update((d2) => {
|
|
260
|
+
d2.incomes.push({ type: 'socialSecurity', id: newId(), personId: person.id, piaMonthly: 0, earnings: null, claimAge: { years: 67, months: 0 } })
|
|
261
|
+
})
|
|
262
|
+
}
|
|
263
|
+
>
|
|
264
|
+
+ Add Social Security
|
|
265
|
+
</button>
|
|
266
|
+
</div>
|
|
267
|
+
)
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const streamIndex = plan.incomes.findIndex((s) => s.id === stream.id)
|
|
271
|
+
const mode: 'quick' | 'earnings' = stream.piaMonthly === null ? 'earnings' : 'quick'
|
|
272
|
+
const resolved = resolvePia(person, stream)
|
|
273
|
+
|
|
274
|
+
const earnings = stream.earnings ?? []
|
|
275
|
+
const mostRecentEarnings =
|
|
276
|
+
earnings.length > 0 ? earnings.reduce((a, b) => (b.year >= a.year ? b : a)).amount : null
|
|
277
|
+
const projYears = (resolved.detail?.indexedYears ?? []).filter((y2) => y2.projected)
|
|
278
|
+
const projectedYears = projYears.length
|
|
279
|
+
const projectedRange = projectedYears > 0 ? `${projYears[0]!.year}–${projYears[projectedYears - 1]!.year}` : ''
|
|
280
|
+
const projectedAmount = projectedYears > 0 ? projYears[0]!.rawEarnings : null
|
|
281
|
+
|
|
282
|
+
const setStream = (mut: (s: SsStream) => void) =>
|
|
283
|
+
update((d2) => {
|
|
284
|
+
const s = d2.incomes[streamIndex]
|
|
285
|
+
if (s?.type === 'socialSecurity') mut(s)
|
|
286
|
+
})
|
|
287
|
+
|
|
288
|
+
const handleXml = (e: ChangeEvent<HTMLInputElement>) => {
|
|
289
|
+
const file = e.target.files?.[0]
|
|
290
|
+
e.target.value = ''
|
|
291
|
+
if (!file) return
|
|
292
|
+
void file.text().then((text) => {
|
|
293
|
+
const result = parseSsaStatementXml(text)
|
|
294
|
+
if (!result.ok) {
|
|
295
|
+
setXmlNote({ kind: 'error', text: result.message })
|
|
296
|
+
return
|
|
297
|
+
}
|
|
298
|
+
setStream((s) => {
|
|
299
|
+
s.piaMonthly = null
|
|
300
|
+
s.earnings = result.rows.map((r) => ({ year: r.year, amount: r.amount }))
|
|
301
|
+
})
|
|
302
|
+
setXmlNote({ kind: 'ok', text: `Imported ${result.rows.length} years of earnings from your statement.` })
|
|
303
|
+
})
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
return (
|
|
307
|
+
<div className="item-row">
|
|
308
|
+
<div className="item-row-head">
|
|
309
|
+
<span className="item-row-title">
|
|
310
|
+
<span className="type-chip">{personIndex === 0 ? 'Primary' : 'Partner'}</span>
|
|
311
|
+
{person.name} — full retirement age {fra.years}
|
|
312
|
+
{fra.extraMonths > 0 ? `y ${fra.extraMonths}m` : ''}
|
|
313
|
+
</span>
|
|
314
|
+
<button type="button" className="btn-ghost btn-ghost-danger" onClick={() => update((d2) => void d2.incomes.splice(streamIndex, 1))}>
|
|
315
|
+
Remove
|
|
316
|
+
</button>
|
|
317
|
+
</div>
|
|
318
|
+
|
|
319
|
+
<div className="form-grid">
|
|
320
|
+
<SelectField
|
|
321
|
+
label="Benefit source"
|
|
322
|
+
help="Quick: type the PIA from your SSA statement. Earnings record: paste or import your full earnings history and we compute the PIA (AIME → bend points), which is more accurate if you'll retire before the statement's assumed work-through age."
|
|
323
|
+
value={mode}
|
|
324
|
+
options={[
|
|
325
|
+
{ value: 'quick', label: 'Quick — enter PIA' },
|
|
326
|
+
{ value: 'earnings', label: 'Earnings record (advanced)' },
|
|
327
|
+
]}
|
|
328
|
+
onCommit={(v) =>
|
|
329
|
+
setStream((s) => {
|
|
330
|
+
if (v === 'quick') {
|
|
331
|
+
s.piaMonthly = s.piaMonthly ?? 0
|
|
332
|
+
} else {
|
|
333
|
+
s.piaMonthly = null
|
|
334
|
+
s.earnings = s.earnings ?? []
|
|
335
|
+
}
|
|
336
|
+
})
|
|
337
|
+
}
|
|
338
|
+
/>
|
|
339
|
+
<NumberField
|
|
340
|
+
label="Claim age (years)"
|
|
341
|
+
help="The age this person starts benefits. Earlier than full retirement age permanently reduces it; waiting past it adds ~8%/year until 70. Compare the options on Explore → Social Security."
|
|
342
|
+
value={stream.claimAge.years}
|
|
343
|
+
min={62}
|
|
344
|
+
max={70}
|
|
345
|
+
onCommit={(v) => setStream((s) => (s.claimAge = { years: Math.round(v ?? 67), months: s.claimAge.months }))}
|
|
346
|
+
/>
|
|
347
|
+
<NumberField
|
|
348
|
+
label="Claim age (+ months)"
|
|
349
|
+
value={stream.claimAge.months}
|
|
350
|
+
min={0}
|
|
351
|
+
max={11}
|
|
352
|
+
onCommit={(v) => setStream((s) => (s.claimAge = { years: s.claimAge.years, months: Math.round(v ?? 0) }))}
|
|
353
|
+
/>
|
|
354
|
+
</div>
|
|
355
|
+
|
|
356
|
+
<details className="ss-explainer" style={{ marginTop: '0.6rem' }}>
|
|
357
|
+
<summary>Disability (SSDI)</summary>
|
|
358
|
+
<p className="card-hint">
|
|
359
|
+
If you're receiving Social Security disability, your benefit is your <strong>full PIA</strong> (no
|
|
360
|
+
early-retirement reduction) from the onset age, converting to the retirement benefit at FRA at the same
|
|
361
|
+
dollar amount. Earnings above Substantial Gainful Activity (SGA) suspend it before FRA. Leave this off
|
|
362
|
+
for a normal retirement claim.
|
|
363
|
+
</p>
|
|
364
|
+
<div className="form-grid">
|
|
365
|
+
<CheckboxField
|
|
366
|
+
label="Receiving Social Security disability (SSDI)"
|
|
367
|
+
help="SSDI pays your full PIA (no early-retirement reduction) from the disability onset age, is gated by Substantial Gainful Activity, and converts to the retirement benefit at FRA at the same dollar amount (no delayed-retirement credits). It's taxed like retirement benefits."
|
|
368
|
+
value={stream.disability != null}
|
|
369
|
+
onCommit={(on) =>
|
|
370
|
+
setStream((s) => {
|
|
371
|
+
s.disability = on ? { onsetAge: 62 } : undefined
|
|
372
|
+
})
|
|
373
|
+
}
|
|
374
|
+
/>
|
|
375
|
+
{stream.disability ? (
|
|
376
|
+
<NumberField
|
|
377
|
+
label="Disability onset age"
|
|
378
|
+
help="The age your disability began. SSDI starts here (not at your retirement claim age) and pays the full PIA. Must be before your full retirement age — an onset at/after FRA is ignored (SSDI converts to retirement at FRA, so it can't start later)."
|
|
379
|
+
value={stream.disability.onsetAge}
|
|
380
|
+
min={40}
|
|
381
|
+
max={75}
|
|
382
|
+
onCommit={(v) =>
|
|
383
|
+
setStream((s) => {
|
|
384
|
+
if (s.disability) s.disability.onsetAge = Math.round(v ?? 62)
|
|
385
|
+
})
|
|
386
|
+
}
|
|
387
|
+
/>
|
|
388
|
+
) : null}
|
|
389
|
+
</div>
|
|
390
|
+
</details>
|
|
391
|
+
|
|
392
|
+
{mode === 'quick' ? (
|
|
393
|
+
<div className="form-grid">
|
|
394
|
+
<MoneyField
|
|
395
|
+
label="PIA (monthly benefit at FRA)"
|
|
396
|
+
help="Your Primary Insurance Amount — the monthly benefit at full retirement age in today's dollars, from ssa.gov/myaccount."
|
|
397
|
+
value={stream.piaMonthly}
|
|
398
|
+
allowNull
|
|
399
|
+
onCommit={(v) => setStream((s) => (s.piaMonthly = v ?? 0))}
|
|
400
|
+
/>
|
|
401
|
+
</div>
|
|
402
|
+
) : (
|
|
403
|
+
<div className="earnings-entry">
|
|
404
|
+
<label className="field-label" htmlFor={`earn-${stream.id}`}>
|
|
405
|
+
Annual covered earnings — one <code>year amount</code> per line
|
|
406
|
+
</label>
|
|
407
|
+
<textarea
|
|
408
|
+
id={`earn-${stream.id}`}
|
|
409
|
+
className="earnings-textarea"
|
|
410
|
+
value={(stream.earnings ?? []).map((r) => `${r.year} ${r.amount}`).join('\n')}
|
|
411
|
+
onChange={(e) =>
|
|
412
|
+
setStream((s) => {
|
|
413
|
+
s.piaMonthly = null
|
|
414
|
+
s.earnings = e.target.value
|
|
415
|
+
.split('\n')
|
|
416
|
+
.map((line) => line.trim().split(/[\s,]+/))
|
|
417
|
+
.filter((parts) => parts.length >= 2)
|
|
418
|
+
.map((parts) => ({ year: Number(parts[0]), amount: Number(parts[1]) }))
|
|
419
|
+
.filter((r) => Number.isFinite(r.year) && Number.isFinite(r.amount))
|
|
420
|
+
})
|
|
421
|
+
}
|
|
422
|
+
placeholder={'1995 28500\n1996 31200\n…'}
|
|
423
|
+
/>
|
|
424
|
+
<div className="add-row">
|
|
425
|
+
<label className="btn btn-secondary btn-small" style={{ cursor: 'pointer' }}>
|
|
426
|
+
Import mySSA statement (XML)
|
|
427
|
+
<input type="file" accept=".xml,application/xml,text/xml" className="sr-only" onChange={handleXml} />
|
|
428
|
+
</label>
|
|
429
|
+
</div>
|
|
430
|
+
{xmlNote ? (
|
|
431
|
+
<p className={xmlNote.kind === 'error' ? 'error-text' : 'field-hint'} style={{ marginTop: '0.4rem' }}>
|
|
432
|
+
{xmlNote.text}
|
|
433
|
+
</p>
|
|
434
|
+
) : null}
|
|
435
|
+
|
|
436
|
+
<div className="earnings-projection" style={{ marginTop: '0.6rem' }}>
|
|
437
|
+
<CheckboxField
|
|
438
|
+
label="Project future earnings until retirement"
|
|
439
|
+
help="An SSA statement assumes you keep earning until full retirement age. If you'll stop sooner, that overstates your benefit; leaving the gap years at $0 understates it. This fills the years between your last reported year and your retirement age at an assumed wage."
|
|
440
|
+
value={stream.earningsProjection != null}
|
|
441
|
+
onCommit={(on) =>
|
|
442
|
+
setStream((s) => {
|
|
443
|
+
s.earningsProjection = on ? { assumedAnnualEarnings: null, throughAge: null } : null
|
|
444
|
+
})
|
|
445
|
+
}
|
|
446
|
+
/>
|
|
447
|
+
{stream.earningsProjection != null ? (
|
|
448
|
+
<div className="form-grid">
|
|
449
|
+
<MoneyField
|
|
450
|
+
label="Assumed annual earnings"
|
|
451
|
+
help="Covered wages to assume for each projected year, in today's dollars."
|
|
452
|
+
hint={
|
|
453
|
+
mostRecentEarnings !== null
|
|
454
|
+
? `Blank = reuse your most recent year (${fmtMoney(mostRecentEarnings)}).`
|
|
455
|
+
: 'Blank = reuse your most recent year.'
|
|
456
|
+
}
|
|
457
|
+
value={stream.earningsProjection.assumedAnnualEarnings}
|
|
458
|
+
allowNull
|
|
459
|
+
onCommit={(v) =>
|
|
460
|
+
setStream((s) => {
|
|
461
|
+
if (s.earningsProjection) s.earningsProjection.assumedAnnualEarnings = v
|
|
462
|
+
})
|
|
463
|
+
}
|
|
464
|
+
/>
|
|
465
|
+
<NumberField
|
|
466
|
+
label="Work through age"
|
|
467
|
+
help="Project earnings up to (but not including) this age — your last full working year. Defaults to this person's retirement age."
|
|
468
|
+
hint={person.retirementAge !== null ? `Blank = retirement age (${person.retirementAge}).` : 'Blank = retirement age.'}
|
|
469
|
+
value={stream.earningsProjection.throughAge}
|
|
470
|
+
allowNull
|
|
471
|
+
min={50}
|
|
472
|
+
max={75}
|
|
473
|
+
onCommit={(v) =>
|
|
474
|
+
setStream((s) => {
|
|
475
|
+
if (s.earningsProjection) s.earningsProjection.throughAge = v === null ? null : Math.round(v)
|
|
476
|
+
})
|
|
477
|
+
}
|
|
478
|
+
/>
|
|
479
|
+
</div>
|
|
480
|
+
) : null}
|
|
481
|
+
</div>
|
|
482
|
+
|
|
483
|
+
{resolved.piaMonthly !== null ? (
|
|
484
|
+
<p className="field-hint" style={{ marginTop: '0.4rem' }}>
|
|
485
|
+
Computed PIA: <strong>{fmtMoney(resolved.piaMonthly)}/mo</strong> at full retirement age.
|
|
486
|
+
{projectedYears > 0 ? (
|
|
487
|
+
<>
|
|
488
|
+
{' '}
|
|
489
|
+
Includes <strong>{projectedYears}</strong> projected {projectedYears === 1 ? 'year' : 'years'} (
|
|
490
|
+
{projectedRange}) at {fmtMoney(projectedAmount ?? 0)}/yr — fewer zero years than the SSA statement
|
|
491
|
+
would assume if you retire early.
|
|
492
|
+
</>
|
|
493
|
+
) : null}
|
|
494
|
+
</p>
|
|
495
|
+
) : null}
|
|
496
|
+
{stream.earningsProjection != null && projectedYears === 0 && resolved.piaMonthly !== null ? (
|
|
497
|
+
<p className="field-hint" style={{ marginTop: '0.4rem', color: 'var(--warn)' }}>
|
|
498
|
+
No years were projected — your reported earnings already reach your retirement age, or no retirement age is
|
|
499
|
+
set on the Household step.
|
|
500
|
+
</p>
|
|
501
|
+
) : null}
|
|
502
|
+
|
|
503
|
+
{resolved.detail ? (
|
|
504
|
+
<AimeExplainer detail={resolved.detail} sampleEarnings={projectedAmount ?? mostRecentEarnings} />
|
|
505
|
+
) : null}
|
|
506
|
+
|
|
507
|
+
<EligibilityNote
|
|
508
|
+
stream={stream}
|
|
509
|
+
onCommitCredits={(v) => setStream((s) => (s.coveredQuarters = v))}
|
|
510
|
+
/>
|
|
511
|
+
</div>
|
|
512
|
+
)}
|
|
513
|
+
|
|
514
|
+
<FormerSpousesEditor stream={stream} setStream={setStream} householdIsSingle={plan.household.people.length === 1} />
|
|
515
|
+
|
|
516
|
+
{resolved.warning ? <p className="field-hint" style={{ color: 'var(--warn)' }}>{resolved.warning}</p> : null}
|
|
517
|
+
</div>
|
|
518
|
+
)
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
export function SocialSecuritySection() {
|
|
522
|
+
const { plan } = usePlan()
|
|
523
|
+
const couple = plan.household.people.length === 2
|
|
524
|
+
return (
|
|
525
|
+
<section>
|
|
526
|
+
<div className="card">
|
|
527
|
+
<h2>Social Security</h2>
|
|
528
|
+
<p className="card-hint">
|
|
529
|
+
Birth dates and planning ages come from the Household form — here you add each person's benefit and a claiming
|
|
530
|
+
age. Monthly precision, the earnings test, spousal and survivor benefits, and an optional trust-fund cut (see
|
|
531
|
+
Assumptions) are all modeled.
|
|
532
|
+
</p>
|
|
533
|
+
{couple ? (
|
|
534
|
+
<div className="callout callout--info">
|
|
535
|
+
For couples, the survivor keeps the larger of the two benefits, so delaying the <em>higher</em> earner's
|
|
536
|
+
claim protects both lifetimes. The analysis page works out the best combination for your whole plan.
|
|
537
|
+
</div>
|
|
538
|
+
) : null}
|
|
539
|
+
{plan.household.people.map((p, i) => (
|
|
540
|
+
<PersonSsCard key={p.id} person={p} personIndex={i} />
|
|
541
|
+
))}
|
|
542
|
+
<div className="callout callout--info" style={{ marginTop: '1rem' }}>
|
|
543
|
+
<strong>Claiming early while still working?</strong> The earnings test withholds part of the benefit above an
|
|
544
|
+
annual wage limit before full retirement age — RetireGolden models that withholding, and credits the withheld
|
|
545
|
+
months back at full retirement age (recomputing the benefit as if you'd claimed that many months later), as an
|
|
546
|
+
annual approximation.
|
|
547
|
+
</div>
|
|
548
|
+
<p className="muted small" style={{ marginTop: '0.75rem' }}>
|
|
549
|
+
Not sure when to claim?{' '}
|
|
550
|
+
<Link to="../social-security-analysis">Compare claiming ages on the Social Security analysis page →</Link>
|
|
551
|
+
</p>
|
|
552
|
+
<LearnAboutScreen route="/plan/:planId/social-security" />
|
|
553
|
+
</div>
|
|
554
|
+
</section>
|
|
555
|
+
)
|
|
556
|
+
}
|