@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,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Survival-percentile planning-age picker (spending-paths & SWR-lenses plan,
|
|
3
|
+
* Goal 4). Expresses the planning age as "the age I/we have a 25% (10%) chance
|
|
4
|
+
* of reaching" — the standard longevity-risk framing the Actuaries Longevity
|
|
5
|
+
* Illustrator popularized — computed from the same SSA 2022 table as the
|
|
6
|
+
* stochastic-longevity engine (engine/montecarlo/survival.ts). The age is
|
|
7
|
+
* computed once at pick time and written as an ordinary planning age with
|
|
8
|
+
* provenance; it is never silently recomputed later (the anti-drift rule the
|
|
9
|
+
* spending presets follow). A manual edit of the field remains the override.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { useMemo, useState } from 'react'
|
|
13
|
+
|
|
14
|
+
import type { Person } from '@retiregolden/engine/model/plan'
|
|
15
|
+
import {
|
|
16
|
+
hazardForExpectancyMultiplier,
|
|
17
|
+
jointSurvivalPercentileAge,
|
|
18
|
+
survivalPercentileAge,
|
|
19
|
+
} from '@retiregolden/engine/montecarlo/survival'
|
|
20
|
+
import { loadLongevity, loadLongevityPartner } from '../longevity/storage'
|
|
21
|
+
import { CheckboxField, SelectField } from './fields'
|
|
22
|
+
import { Modal } from './Modal'
|
|
23
|
+
import { currentStartYear } from './useProjection'
|
|
24
|
+
|
|
25
|
+
export interface SurvivalPercentileModalProps {
|
|
26
|
+
person: Person
|
|
27
|
+
/** 0 = primary slot, 1 = partner slot (matches the questionnaire storage). */
|
|
28
|
+
personIndex: number
|
|
29
|
+
/** The other household member, when this is a couple plan. */
|
|
30
|
+
partner: Person | null
|
|
31
|
+
onApply: (longevity: Person['longevity']) => void
|
|
32
|
+
onClose: () => void
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Plan-year age: current year − birth year, the same convention the ledger
|
|
37
|
+
* (and ABW's survival25/survival10 horizon) uses — so a percentile picked here
|
|
38
|
+
* and the engine's survival horizon quote the same SSA age, never off by one
|
|
39
|
+
* from birth-month rounding.
|
|
40
|
+
*/
|
|
41
|
+
function planYearAge(dob: string): number {
|
|
42
|
+
const age = currentStartYear() - Number(dob.slice(0, 4))
|
|
43
|
+
return Math.min(110, Math.max(18, age))
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const PCT_OPTIONS = [
|
|
47
|
+
{ value: '50', label: '50% — the median (half of people live longer)' },
|
|
48
|
+
{ value: '25', label: '25% — the standard prudent choice' },
|
|
49
|
+
{ value: '10', label: '10% — conservative' },
|
|
50
|
+
]
|
|
51
|
+
|
|
52
|
+
export function SurvivalPercentileModal({ person, personIndex, partner, onApply, onClose }: SurvivalPercentileModalProps) {
|
|
53
|
+
const [pct, setPct] = useState<'50' | '25' | '10'>('25')
|
|
54
|
+
const [joint, setJoint] = useState(partner !== null)
|
|
55
|
+
const [useHealth, setUseHealth] = useState(false)
|
|
56
|
+
|
|
57
|
+
// Saved questionnaire multipliers by household slot (this modal's person may
|
|
58
|
+
// be either slot). Absent questionnaire = no adjustment offered for that slot.
|
|
59
|
+
const savedSelf = useMemo(() => (personIndex === 0 ? loadLongevity() : loadLongevityPartner()), [personIndex])
|
|
60
|
+
const savedPartner = useMemo(
|
|
61
|
+
() => (partner ? (personIndex === 0 ? loadLongevityPartner() : loadLongevity()) : null),
|
|
62
|
+
[partner, personIndex],
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
const computed = useMemo(() => {
|
|
66
|
+
const pctNum = Number(pct)
|
|
67
|
+
const selfAge = planYearAge(person.dob)
|
|
68
|
+
const selfMultiplier = useHealth && savedSelf ? savedSelf.result.appliedMultiplier : null
|
|
69
|
+
const selfHazard = selfMultiplier !== null ? hazardForExpectancyMultiplier(selfAge, person.sex, selfMultiplier) : 1
|
|
70
|
+
if (joint && partner) {
|
|
71
|
+
const partnerAge = planYearAge(partner.dob)
|
|
72
|
+
const partnerMultiplier = useHealth && savedPartner ? savedPartner.result.appliedMultiplier : null
|
|
73
|
+
const partnerHazard =
|
|
74
|
+
partnerMultiplier !== null ? hazardForExpectancyMultiplier(partnerAge, partner.sex, partnerMultiplier) : 1
|
|
75
|
+
return jointSurvivalPercentileAge(
|
|
76
|
+
{ age: selfAge, sex: person.sex, hazard: selfHazard },
|
|
77
|
+
{ age: partnerAge, sex: partner.sex, hazard: partnerHazard },
|
|
78
|
+
pctNum,
|
|
79
|
+
)
|
|
80
|
+
}
|
|
81
|
+
return survivalPercentileAge(selfAge, person.sex, pctNum, selfHazard)
|
|
82
|
+
}, [pct, joint, useHealth, person, partner, savedSelf, savedPartner])
|
|
83
|
+
|
|
84
|
+
const clampedAge = Math.min(120, Math.max(60, computed))
|
|
85
|
+
const healthAvailable = savedSelf !== null || (joint && savedPartner !== null)
|
|
86
|
+
|
|
87
|
+
const apply = () => {
|
|
88
|
+
const isJoint = joint && partner !== null
|
|
89
|
+
onApply({
|
|
90
|
+
planningAge: clampedAge,
|
|
91
|
+
source: 'percentile',
|
|
92
|
+
percentile: {
|
|
93
|
+
pct: Number(pct),
|
|
94
|
+
joint: isJoint,
|
|
95
|
+
// Record every multiplier the computation actually used, so the
|
|
96
|
+
// provenance can restate the pick (partner-only adjustments included).
|
|
97
|
+
...(useHealth && savedSelf ? { healthMultiplier: savedSelf.result.appliedMultiplier } : {}),
|
|
98
|
+
...(useHealth && isJoint && savedPartner
|
|
99
|
+
? { partnerHealthMultiplier: savedPartner.result.appliedMultiplier }
|
|
100
|
+
: {}),
|
|
101
|
+
},
|
|
102
|
+
})
|
|
103
|
+
onClose()
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return (
|
|
107
|
+
<Modal title={`Planning age from a survival percentile — ${person.name}`} onClose={onClose} width="40rem">
|
|
108
|
+
<p className="card-hint">
|
|
109
|
+
Instead of guessing an age, plan to the age you have only a chosen chance of reaching. Probabilities come
|
|
110
|
+
from the Social Security Administration 2022 period life table — the same table behind the questionnaire
|
|
111
|
+
and the Monte Carlo longevity model. The result is written as an ordinary planning age you can still edit.
|
|
112
|
+
</p>
|
|
113
|
+
<div className="form-grid">
|
|
114
|
+
<SelectField
|
|
115
|
+
label="Chance of reaching that age"
|
|
116
|
+
help="The survival probability the planning age is anchored to. Planning to the 25th percentile means only a 1-in-4 chance of outliving the plan horizon — the usual prudent recommendation. The median (50%) is a coin flip; 10% is conservative."
|
|
117
|
+
value={pct}
|
|
118
|
+
options={PCT_OPTIONS}
|
|
119
|
+
onCommit={(v) => setPct(v as '50' | '25' | '10')}
|
|
120
|
+
/>
|
|
121
|
+
{partner !== null ? (
|
|
122
|
+
<CheckboxField
|
|
123
|
+
label="Either of us (joint)"
|
|
124
|
+
help="For couples: the age at which there is still the chosen chance that at least one of you is alive (independent lifetimes). Joint horizons are several years longer than either individual's — the usual reason couple plans should run longer. Applying writes the joint age as THIS person's planning age so the household horizon reaches it; your partner's own planning age is left unchanged (the plan always runs to the later of the two)."
|
|
125
|
+
value={joint}
|
|
126
|
+
onCommit={setJoint}
|
|
127
|
+
/>
|
|
128
|
+
) : null}
|
|
129
|
+
{healthAvailable ? (
|
|
130
|
+
<CheckboxField
|
|
131
|
+
label="Adjust for questionnaire answers"
|
|
132
|
+
help="Applies the saved life-expectancy questionnaire's smoking/health/lifestyle multiplier as a mortality adjustment (a proportional-hazards transform of the SSA table), the way the Actuaries Longevity Illustrator adjusts for smoker status and general health. Only available after running Calculate at least once."
|
|
133
|
+
value={useHealth}
|
|
134
|
+
onCommit={setUseHealth}
|
|
135
|
+
/>
|
|
136
|
+
) : null}
|
|
137
|
+
</div>
|
|
138
|
+
<div className="mc-hero" style={{ marginTop: '0.75rem' }}>
|
|
139
|
+
<div>
|
|
140
|
+
<h2 style={{ margin: 0 }}>
|
|
141
|
+
Age {clampedAge}
|
|
142
|
+
</h2>
|
|
143
|
+
<p className="muted" style={{ margin: '0.35rem 0 0' }}>
|
|
144
|
+
{joint && partner
|
|
145
|
+
? `There is a ${pct}% chance at least one of you is still alive at ${person.name}'s age ${clampedAge}.`
|
|
146
|
+
: `${person.name} has a ${pct}% chance of reaching age ${clampedAge}.`}
|
|
147
|
+
{computed !== clampedAge ? ' (Clamped to the supported 60–120 planning-age range.)' : ''}
|
|
148
|
+
</p>
|
|
149
|
+
</div>
|
|
150
|
+
</div>
|
|
151
|
+
<div style={{ display: 'flex', gap: '0.75rem', marginTop: '0.9rem' }}>
|
|
152
|
+
<button type="button" className="btn btn-primary btn-small" onClick={apply}>
|
|
153
|
+
Use age {clampedAge} as the planning age
|
|
154
|
+
</button>
|
|
155
|
+
<button type="button" className="btn btn-secondary btn-small" onClick={onClose}>
|
|
156
|
+
Cancel
|
|
157
|
+
</button>
|
|
158
|
+
</div>
|
|
159
|
+
</Modal>
|
|
160
|
+
)
|
|
161
|
+
}
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Survivor transition view (survivor-widowhood-and-irmaa-relief, step 2).
|
|
3
|
+
*
|
|
4
|
+
* For two-adult plans: each death timing is the user's own plan re-run with a
|
|
5
|
+
* death-age override on the same deterministic ledger as Results, so every
|
|
6
|
+
* number agrees exactly with running the equivalent scenario by hand. The
|
|
7
|
+
* framing is deliberately educational — death timings are scenarios the reader
|
|
8
|
+
* chooses to look at, never predictions.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { useEffect, useMemo, useState } from 'react'
|
|
12
|
+
|
|
13
|
+
import { usePlan } from './planContextCore'
|
|
14
|
+
import { LearnAboutScreen } from '../learn/LearnAboutScreen'
|
|
15
|
+
import { LearnLink } from '../learn/LearnLink'
|
|
16
|
+
import { LEARN } from './learnLinks'
|
|
17
|
+
import { fmtMoney, fmtMoneyCompact } from './format'
|
|
18
|
+
import { currentStartYear, taxCalculatorFor } from './useProjection'
|
|
19
|
+
import {
|
|
20
|
+
buildSurvivorAnalysis,
|
|
21
|
+
type SurvivorAnalysis,
|
|
22
|
+
type SurvivorScenarioRow,
|
|
23
|
+
} from './survivorAnalysis'
|
|
24
|
+
|
|
25
|
+
function filingLabel(status: SurvivorScenarioRow['firstSurvivorYear']['filingStatus']): string {
|
|
26
|
+
return status === 'marriedFilingJointly'
|
|
27
|
+
? 'married filing jointly'
|
|
28
|
+
: status === 'qualifyingSurvivingSpouse'
|
|
29
|
+
? 'qualifying surviving spouse'
|
|
30
|
+
: 'single'
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function TimelineCell({ row }: { row: SurvivorScenarioRow }) {
|
|
34
|
+
return (
|
|
35
|
+
<span>
|
|
36
|
+
{row.filingTimeline.map((seg, i) => (
|
|
37
|
+
<span key={seg.fromYear}>
|
|
38
|
+
{i > 0 ? ' → ' : ''}
|
|
39
|
+
{filingLabel(seg.status)}
|
|
40
|
+
<span className="small"> {seg.fromYear === seg.toYear ? `(${seg.fromYear})` : `(${seg.fromYear}–${seg.toYear})`}</span>
|
|
41
|
+
</span>
|
|
42
|
+
))}
|
|
43
|
+
</span>
|
|
44
|
+
)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function ScenarioTable({ rows, personName }: { rows: SurvivorScenarioRow[]; personName: string }) {
|
|
48
|
+
if (rows.length === 0) {
|
|
49
|
+
return (
|
|
50
|
+
<p className="small">
|
|
51
|
+
No earlier-death timings to sweep for {personName} — the standard ages (70–90) all fall outside their current
|
|
52
|
+
age and planning age.
|
|
53
|
+
</p>
|
|
54
|
+
)
|
|
55
|
+
}
|
|
56
|
+
return (
|
|
57
|
+
<div className="year-table-wrap" style={{ border: 'none' }}>
|
|
58
|
+
<table className="compare-table">
|
|
59
|
+
<thead>
|
|
60
|
+
<tr>
|
|
61
|
+
<th>Dies at</th>
|
|
62
|
+
<th>Filing status</th>
|
|
63
|
+
<th>Household Social Security</th>
|
|
64
|
+
<th>Tax around the transition</th>
|
|
65
|
+
<th>IRMAA relief (SSA-44)</th>
|
|
66
|
+
<th>Survivor spending</th>
|
|
67
|
+
<th>Convert-early lever</th>
|
|
68
|
+
</tr>
|
|
69
|
+
</thead>
|
|
70
|
+
<tbody>
|
|
71
|
+
{rows.map((row) => (
|
|
72
|
+
<tr key={`${row.deceasedPersonId}-${row.deathAge}`}>
|
|
73
|
+
<td>
|
|
74
|
+
<strong>{row.deathAge}</strong>
|
|
75
|
+
<div className="small">{row.deathYear}</div>
|
|
76
|
+
</td>
|
|
77
|
+
<td style={{ maxWidth: '14rem', textAlign: 'left' }}>
|
|
78
|
+
<TimelineCell row={row} />
|
|
79
|
+
</td>
|
|
80
|
+
<td>
|
|
81
|
+
{fmtMoneyCompact(row.ssBeforeDeath)} → {fmtMoneyCompact(row.ssAfterDeath)}
|
|
82
|
+
<div className="small">
|
|
83
|
+
{row.lastJointYear.year} vs {row.firstSurvivorYear.year}
|
|
84
|
+
</div>
|
|
85
|
+
</td>
|
|
86
|
+
<td>
|
|
87
|
+
{fmtMoneyCompact(row.lastJointYear.tax)} → {fmtMoneyCompact(row.firstSurvivorYear.tax)}
|
|
88
|
+
<div className="small">
|
|
89
|
+
on MAGI {fmtMoneyCompact(row.lastJointYear.magi)} → {fmtMoneyCompact(row.firstSurvivorYear.magi)},{' '}
|
|
90
|
+
{filingLabel(row.firstSurvivorYear.filingStatus)}
|
|
91
|
+
</div>
|
|
92
|
+
</td>
|
|
93
|
+
<td>
|
|
94
|
+
{row.ssa44PremiumSavings > 0.5 ? (
|
|
95
|
+
<>
|
|
96
|
+
<span className="delta-pos">{fmtMoney(Math.round(row.ssa44PremiumSavings))}</span>
|
|
97
|
+
<div className="small">
|
|
98
|
+
{row.irmaaYears
|
|
99
|
+
.filter((y) => y.tierWithoutSsa44 !== y.tierWithSsa44)
|
|
100
|
+
.map((y) => `${y.year}: tier ${y.tierWithoutSsa44} → ${y.tierWithSsa44}`)
|
|
101
|
+
.join('; ') || 'premium difference across survivor years'}
|
|
102
|
+
</div>
|
|
103
|
+
</>
|
|
104
|
+
) : (
|
|
105
|
+
<>
|
|
106
|
+
—<div className="small">no surcharge to relieve at this timing</div>
|
|
107
|
+
</>
|
|
108
|
+
)}
|
|
109
|
+
</td>
|
|
110
|
+
<td>
|
|
111
|
+
{row.survivorShortfallYears === 0 ? (
|
|
112
|
+
<span className="delta-pos">covered</span>
|
|
113
|
+
) : (
|
|
114
|
+
<span className="delta-neg">{row.survivorShortfallYears} shortfall yrs</span>
|
|
115
|
+
)}
|
|
116
|
+
<div className="small">low point {fmtMoneyCompact(row.minSurvivorInvestable)}</div>
|
|
117
|
+
</td>
|
|
118
|
+
<td>
|
|
119
|
+
{row.conversionLever ? (
|
|
120
|
+
<>
|
|
121
|
+
<span
|
|
122
|
+
className={
|
|
123
|
+
row.conversionLever.estateDelta > 500
|
|
124
|
+
? 'delta-pos'
|
|
125
|
+
: row.conversionLever.estateDelta < -500
|
|
126
|
+
? 'delta-neg'
|
|
127
|
+
: undefined
|
|
128
|
+
}
|
|
129
|
+
>
|
|
130
|
+
{row.conversionLever.estateDelta > 0 ? '+' : ''}
|
|
131
|
+
{fmtMoneyCompact(row.conversionLever.estateDelta)}
|
|
132
|
+
</span>
|
|
133
|
+
<div className="small">
|
|
134
|
+
after-tax estate ({row.conversionLever.lifetimeTaxDelta > 0 ? '+' : ''}
|
|
135
|
+
{fmtMoneyCompact(row.conversionLever.lifetimeTaxDelta)} lifetime tax), filling the 12% bracket
|
|
136
|
+
through {row.deathYear}
|
|
137
|
+
</div>
|
|
138
|
+
</>
|
|
139
|
+
) : (
|
|
140
|
+
'—'
|
|
141
|
+
)}
|
|
142
|
+
</td>
|
|
143
|
+
</tr>
|
|
144
|
+
))}
|
|
145
|
+
</tbody>
|
|
146
|
+
</table>
|
|
147
|
+
</div>
|
|
148
|
+
)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function SurvivorTransitionPage() {
|
|
152
|
+
const { plan, update } = usePlan()
|
|
153
|
+
// The analysis is stored WITH the plan it was computed for and derived to
|
|
154
|
+
// null whenever the current plan differs, so a stale sweep can never render
|
|
155
|
+
// against an edited plan through the debounce window.
|
|
156
|
+
const [snapshot, setSnapshot] = useState<{ plan: typeof plan; analysis: SurvivorAnalysis } | null>(null)
|
|
157
|
+
const eligible = plan.household.filingStatus === 'marriedFilingJointly' && plan.household.people.length === 2
|
|
158
|
+
|
|
159
|
+
// Each timing runs a handful of full ledger simulations; debounce off the
|
|
160
|
+
// keystroke path like the Scenarios page does.
|
|
161
|
+
useEffect(() => {
|
|
162
|
+
if (!eligible) return undefined
|
|
163
|
+
const t = window.setTimeout(() => {
|
|
164
|
+
try {
|
|
165
|
+
setSnapshot({
|
|
166
|
+
plan,
|
|
167
|
+
analysis: buildSurvivorAnalysis(plan, { startYear: currentStartYear(), taxCalculator: taxCalculatorFor(plan) }),
|
|
168
|
+
})
|
|
169
|
+
} catch {
|
|
170
|
+
// Per-timing failures are already absorbed inside the sweep; this is
|
|
171
|
+
// the whole-sweep backstop so the page shows an error card, not a
|
|
172
|
+
// stuck skeleton.
|
|
173
|
+
setSnapshot({ plan, analysis: { eligible: true, planUsesSsa44: false, rows: [], failedTimings: 0, error: true } })
|
|
174
|
+
}
|
|
175
|
+
}, 200)
|
|
176
|
+
return () => window.clearTimeout(t)
|
|
177
|
+
}, [plan, eligible])
|
|
178
|
+
const analysis = snapshot !== null && snapshot.plan === plan ? snapshot.analysis : null
|
|
179
|
+
|
|
180
|
+
const anySsa44Savings = useMemo(
|
|
181
|
+
() => (analysis?.rows ?? []).some((r) => r.ssa44PremiumSavings > 0.5),
|
|
182
|
+
[analysis],
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
if (!eligible) {
|
|
186
|
+
return (
|
|
187
|
+
<section>
|
|
188
|
+
<div className="card">
|
|
189
|
+
<h2 style={{ marginTop: 0 }}>Survivor transition</h2>
|
|
190
|
+
<div className="empty-state">
|
|
191
|
+
<p>
|
|
192
|
+
This view is for plans with two adults filing jointly: it shows how taxes, Medicare premiums, and income
|
|
193
|
+
change for the surviving spouse under different death timings. A one-adult plan has no survivor
|
|
194
|
+
transition to model.
|
|
195
|
+
</p>
|
|
196
|
+
</div>
|
|
197
|
+
</div>
|
|
198
|
+
<LearnAboutScreen route="/plan/:planId/survivor" />
|
|
199
|
+
</section>
|
|
200
|
+
)
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return (
|
|
204
|
+
<section>
|
|
205
|
+
<div className="card">
|
|
206
|
+
<h2 style={{ marginTop: 0 }}>Survivor transition</h2>
|
|
207
|
+
<p>
|
|
208
|
+
Couples' plans quietly assume both of you reach your planning ages. This view re-runs <em>your own plan</em>{' '}
|
|
209
|
+
with earlier first-death timings so you can see the transition while there is still time to prepare for it:
|
|
210
|
+
the filing-status change, the surviving spouse's Social Security, the tax on similar income, Medicare's
|
|
211
|
+
income surcharge (IRMAA), and the levers available in the joint years. Each timing is a scenario you choose
|
|
212
|
+
to look at — nothing here is a prediction.{' '}
|
|
213
|
+
<LearnLink slug="widows-penalty-and-survivor-brackets" label="Learn about the widow's penalty" />
|
|
214
|
+
</p>
|
|
215
|
+
<ul className="small">
|
|
216
|
+
<li>
|
|
217
|
+
<strong>IRMAA relief (SSA-44):</strong> the death of a spouse is a qualifying life-changing event, so the
|
|
218
|
+
survivor can ask Social Security to price IRMAA on current income instead of the two-year lookback. The
|
|
219
|
+
column shows the premium difference between modeling that relief and not. The model is deliberately
|
|
220
|
+
conservative in the first survivor year — its income estimate still references the death year's joint
|
|
221
|
+
income, where a real filing could use the survivor's own — so year-one relief can be understated.{' '}
|
|
222
|
+
<LearnLink slug={LEARN.ssa44.slug} label={LEARN.ssa44.label} />
|
|
223
|
+
</li>
|
|
224
|
+
<li>
|
|
225
|
+
<strong>Convert-early lever:</strong> the change in ending after-tax estate from filling the 12% bracket
|
|
226
|
+
with Roth conversions while joint brackets last, priced on the same ledger as everything else.
|
|
227
|
+
</li>
|
|
228
|
+
</ul>
|
|
229
|
+
{anySsa44Savings && !plan.expenses.healthcare.ssa44?.survivorYears ? (
|
|
230
|
+
<div className="callout callout--info">
|
|
231
|
+
<p style={{ margin: 0 }}>
|
|
232
|
+
Some timings show IRMAA relief your plan is not currently modeling. You can turn SSA-44 survivor relief
|
|
233
|
+
on under <strong>Spending → Healthcare</strong>, or{' '}
|
|
234
|
+
<button
|
|
235
|
+
type="button"
|
|
236
|
+
className="btn btn-secondary btn-small"
|
|
237
|
+
onClick={() =>
|
|
238
|
+
update((d) => {
|
|
239
|
+
d.expenses.healthcare.ssa44 = {
|
|
240
|
+
survivorYears: true,
|
|
241
|
+
retirementYears: d.expenses.healthcare.ssa44?.retirementYears ?? false,
|
|
242
|
+
}
|
|
243
|
+
})
|
|
244
|
+
}
|
|
245
|
+
>
|
|
246
|
+
model it in this plan
|
|
247
|
+
</button>{' '}
|
|
248
|
+
(the form itself is filed with Social Security when the time comes).
|
|
249
|
+
</p>
|
|
250
|
+
</div>
|
|
251
|
+
) : null}
|
|
252
|
+
</div>
|
|
253
|
+
|
|
254
|
+
{analysis === null ? (
|
|
255
|
+
<div className="card">
|
|
256
|
+
<div className="skeleton" style={{ height: '10rem' }} aria-label="Running death-timing scenarios" />
|
|
257
|
+
</div>
|
|
258
|
+
) : analysis.error ? (
|
|
259
|
+
<div className="card">
|
|
260
|
+
<div className="callout callout--warn" role="alert">
|
|
261
|
+
The death-timing sweep could not run on this plan. The rest of the planner is unaffected — if this
|
|
262
|
+
persists, check the plan for validation issues on the Enter screens.
|
|
263
|
+
</div>
|
|
264
|
+
</div>
|
|
265
|
+
) : (
|
|
266
|
+
<>
|
|
267
|
+
{analysis.failedTimings > 0 ? (
|
|
268
|
+
<div className="callout callout--warn" role="alert">
|
|
269
|
+
{analysis.failedTimings} death timing{analysis.failedTimings === 1 ? '' : 's'} could not be simulated and{' '}
|
|
270
|
+
{analysis.failedTimings === 1 ? 'is' : 'are'} not shown — the rows below are still exact for their own
|
|
271
|
+
timings.
|
|
272
|
+
</div>
|
|
273
|
+
) : null}
|
|
274
|
+
{plan.household.people.map((person) => (
|
|
275
|
+
<div className="card" key={person.id}>
|
|
276
|
+
<h3 style={{ marginTop: 0 }}>If {person.name} dies first</h3>
|
|
277
|
+
<ScenarioTable rows={analysis.rows.filter((r) => r.deceasedPersonId === person.id)} personName={person.name} />
|
|
278
|
+
</div>
|
|
279
|
+
))}
|
|
280
|
+
</>
|
|
281
|
+
)}
|
|
282
|
+
|
|
283
|
+
<LearnAboutScreen route="/plan/:planId/survivor" />
|
|
284
|
+
</section>
|
|
285
|
+
)
|
|
286
|
+
}
|