@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.
Files changed (298) hide show
  1. package/LICENSE +661 -0
  2. package/README.md +181 -0
  3. package/package.json +77 -0
  4. package/src/App.tsx +246 -0
  5. package/src/RouteErrorBoundary.tsx +45 -0
  6. package/src/assets/hero.png +0 -0
  7. package/src/assets/react.svg +1 -0
  8. package/src/assets/vite.svg +1 -0
  9. package/src/data/fedInvestClient.ts +113 -0
  10. package/src/data/localStore.ts +42 -0
  11. package/src/data/planOrigin.ts +24 -0
  12. package/src/data/planStore.ts +165 -0
  13. package/src/data/v2Backup.ts +101 -0
  14. package/src/import/ImportPage.tsx +347 -0
  15. package/src/import/ReviewChecklistView.tsx +38 -0
  16. package/src/import/brokerCsv.ts +395 -0
  17. package/src/import/csv.ts +133 -0
  18. package/src/import/genericCsv.ts +224 -0
  19. package/src/import/projectionLab.ts +350 -0
  20. package/src/import/reviewChecklist.ts +33 -0
  21. package/src/import/tenForty.ts +275 -0
  22. package/src/index.css +630 -0
  23. package/src/index.ts +16 -0
  24. package/src/learn/ArticleBody.tsx +78 -0
  25. package/src/learn/ArticlePage.tsx +57 -0
  26. package/src/learn/GlossaryPage.tsx +33 -0
  27. package/src/learn/LearnAboutScreen.tsx +41 -0
  28. package/src/learn/LearnCards.tsx +41 -0
  29. package/src/learn/LearnLink.tsx +91 -0
  30. package/src/learn/LearningCenterPage.tsx +114 -0
  31. package/src/learn/SourcesPage.tsx +98 -0
  32. package/src/learn/components/ArticleFigure.tsx +34 -0
  33. package/src/learn/components/ArticleShell.tsx +86 -0
  34. package/src/learn/components/ComparisonTable.tsx +42 -0
  35. package/src/learn/components/FormulaBlock.tsx +34 -0
  36. package/src/learn/components/PurchasingPowerChart.tsx +41 -0
  37. package/src/learn/components/RelatedArticles.tsx +27 -0
  38. package/src/learn/components/ScenarioCard.tsx +24 -0
  39. package/src/learn/components/SourceList.tsx +23 -0
  40. package/src/learn/components/charts.tsx +21 -0
  41. package/src/learn/content/about-retiregolden.ts +100 -0
  42. package/src/learn/content/aca-premium-tax-credits-and-magi.ts +103 -0
  43. package/src/learn/content/account-types-overview.ts +106 -0
  44. package/src/learn/content/after-tax-estate.ts +111 -0
  45. package/src/learn/content/agi-magi-and-taxable-income.ts +112 -0
  46. package/src/learn/content/appealing-irmaa-ssa-44.ts +95 -0
  47. package/src/learn/content/assumption-general-inflation.ts +82 -0
  48. package/src/learn/content/assumption-healthcare-inflation.ts +85 -0
  49. package/src/learn/content/assumption-heir-tax-rate.ts +79 -0
  50. package/src/learn/content/assumption-investment-returns.ts +90 -0
  51. package/src/learn/content/assumption-longevity-planning-age.ts +78 -0
  52. package/src/learn/content/assumption-recent-magi.ts +83 -0
  53. package/src/learn/content/assumption-social-security-cola.ts +89 -0
  54. package/src/learn/content/assumption-social-security-trust-fund.ts +83 -0
  55. package/src/learn/content/assumption-state-tax-override.ts +79 -0
  56. package/src/learn/content/beneficiaries-and-account-titling.ts +99 -0
  57. package/src/learn/content/break-even-useful-lens.ts +94 -0
  58. package/src/learn/content/building-a-retirement-spending-budget.ts +100 -0
  59. package/src/learn/content/cola-and-inflation-protection.ts +102 -0
  60. package/src/learn/content/divorced-spousal-and-survivor-records.ts +104 -0
  61. package/src/learn/content/dynamic-spending-guardrails.ts +90 -0
  62. package/src/learn/content/earnings-test-before-fra.ts +100 -0
  63. package/src/learn/content/employer-match-and-contribution-order.ts +104 -0
  64. package/src/learn/content/examplePlanArticles.ts +525 -0
  65. package/src/learn/content/fees-expense-ratios-and-compounding-drag.ts +98 -0
  66. package/src/learn/content/fi-number-and-four-percent-rule.ts +64 -0
  67. package/src/learn/content/filling-a-tax-bracket-with-roth-conversions.ts +98 -0
  68. package/src/learn/content/funded-ratio.ts +70 -0
  69. package/src/learn/content/healthcare-after-65.ts +103 -0
  70. package/src/learn/content/healthcare-before-65.ts +104 -0
  71. package/src/learn/content/historical-vs-random-return-models.ts +101 -0
  72. package/src/learn/content/how-assumptions-change-the-answer.ts +105 -0
  73. package/src/learn/content/how-much-can-i-spend.ts +105 -0
  74. package/src/learn/content/how-social-security-is-taxed.ts +95 -0
  75. package/src/learn/content/how-the-optimizer-thinks.ts +102 -0
  76. package/src/learn/content/how-the-optimizer-values-after-tax-estate.ts +97 -0
  77. package/src/learn/content/how-to-model-accumulation.ts +67 -0
  78. package/src/learn/content/how-to-read-a-retirement-projection.ts +115 -0
  79. package/src/learn/content/hsas-and-qualified-medical-expenses.ts +108 -0
  80. package/src/learn/content/hsas-as-retirement-accounts.ts +101 -0
  81. package/src/learn/content/inflation-risk.ts +98 -0
  82. package/src/learn/content/inherited-ira-10-year-rule.ts +105 -0
  83. package/src/learn/content/insurance-in-your-retirement-plan.ts +103 -0
  84. package/src/learn/content/irmaa-two-year-lookback.ts +99 -0
  85. package/src/learn/content/long-term-care-costs-and-insurance.ts +103 -0
  86. package/src/learn/content/long-term-care-insurance-as-risk-transfer.ts +98 -0
  87. package/src/learn/content/longevity-risk.ts +99 -0
  88. package/src/learn/content/marginal-vs-effective-tax-rate.ts +98 -0
  89. package/src/learn/content/medicare-part-b-vs-part-d-irmaa.ts +102 -0
  90. package/src/learn/content/mortality-weighted-social-security.ts +113 -0
  91. package/src/learn/content/moving-to-retiregolden.ts +86 -0
  92. package/src/learn/content/niit-high-income-investment-tax.ts +98 -0
  93. package/src/learn/content/ordinary-income-vs-capital-gains.ts +103 -0
  94. package/src/learn/content/paying-conversion-taxes-taxable-vs-ira.ts +102 -0
  95. package/src/learn/content/pensions-and-annuities.ts +101 -0
  96. package/src/learn/content/permanent-life-insurance-in-a-plan.ts +106 -0
  97. package/src/learn/content/pia-aime-and-bend-points.ts +103 -0
  98. package/src/learn/content/planner-overview.ts +106 -0
  99. package/src/learn/content/planning-for-couples-and-survivor-years.ts +108 -0
  100. package/src/learn/content/privacy-what-stays-in-your-browser.ts +99 -0
  101. package/src/learn/content/qcds-qualified-charitable-distributions.ts +101 -0
  102. package/src/learn/content/reading-the-results-page.ts +96 -0
  103. package/src/learn/content/reading-the-social-security-analysis-page.ts +106 -0
  104. package/src/learn/content/real-estate-home-equity-and-debt.ts +100 -0
  105. package/src/learn/content/reports-csv-exports-and-sharing.ts +101 -0
  106. package/src/learn/content/risk-based-guardrails.ts +100 -0
  107. package/src/learn/content/rmds-required-minimum-distributions.ts +100 -0
  108. package/src/learn/content/roth-conversion-basics.ts +104 -0
  109. package/src/learn/content/rsus-and-espp.ts +101 -0
  110. package/src/learn/content/rule-of-55-and-72t.ts +107 -0
  111. package/src/learn/content/savings-rate-biggest-lever.ts +66 -0
  112. package/src/learn/content/seed-your-plan-from-your-tax-return.ts +93 -0
  113. package/src/learn/content/sensitivity-testing-what-changes-the-answer.ts +104 -0
  114. package/src/learn/content/sequence-of-returns-risk.ts +98 -0
  115. package/src/learn/content/social-security-bridge.ts +67 -0
  116. package/src/learn/content/social-security-claiming-age-basics.ts +113 -0
  117. package/src/learn/content/social-security-taxes-vs-benefits.ts +76 -0
  118. package/src/learn/content/spending-profiles-and-the-retirement-smile.ts +92 -0
  119. package/src/learn/content/spousal-and-survivor-benefits.ts +120 -0
  120. package/src/learn/content/ssdi-and-retirement-planning.ts +72 -0
  121. package/src/learn/content/standard-deduction-senior-deduction-and-itemizing.ts +97 -0
  122. package/src/learn/content/state-income-taxes-in-retirement.ts +97 -0
  123. package/src/learn/content/step-up-in-basis.ts +102 -0
  124. package/src/learn/content/survivor-planning-for-couples.ts +110 -0
  125. package/src/learn/content/survivor-spending-in-couple-plans.ts +98 -0
  126. package/src/learn/content/tax-cliffs-and-bracket-edges.ts +105 -0
  127. package/src/learn/content/tax-loss-and-gain-harvesting.ts +99 -0
  128. package/src/learn/content/taxable-brokerage-basis-and-capital-gains.ts +99 -0
  129. package/src/learn/content/three-big-questions-spending-time-risk.ts +103 -0
  130. package/src/learn/content/tips-ladders.ts +92 -0
  131. package/src/learn/content/todays-dollars-vs-future-dollars.ts +107 -0
  132. package/src/learn/content/traditional-vs-roth-contributions.ts +113 -0
  133. package/src/learn/content/troubleshooting-surprising-results.ts +105 -0
  134. package/src/learn/content/trust-fund-haircut-scenarios.ts +101 -0
  135. package/src/learn/content/understanding-monte-carlo-success-rate.ts +118 -0
  136. package/src/learn/content/understanding-your-plan-assumptions.ts +134 -0
  137. package/src/learn/content/using-assumptions-and-provenance.ts +98 -0
  138. package/src/learn/content/using-scenarios-to-compare-choices.ts +99 -0
  139. package/src/learn/content/what-changes-when-you-move-states.ts +141 -0
  140. package/src/learn/content/what-is-fire.ts +65 -0
  141. package/src/learn/content/what-monte-carlo-proves.ts +98 -0
  142. package/src/learn/content/what-retiregolden-models.ts +103 -0
  143. package/src/learn/content/what-retirement-healthcare-really-costs.ts +117 -0
  144. package/src/learn/content/why-95-percent-is-not-a-guarantee.ts +98 -0
  145. package/src/learn/content/why-roth-conversions-raise-other-costs.ts +106 -0
  146. package/src/learn/content/why-small-tax-cliffs-can-matter.ts +109 -0
  147. package/src/learn/content/widows-penalty-and-survivor-brackets.ts +106 -0
  148. package/src/learn/content/withdrawal-order-basics.ts +105 -0
  149. package/src/learn/glossary.ts +191 -0
  150. package/src/learn/inlineMarkdown.tsx +54 -0
  151. package/src/learn/learn.css +537 -0
  152. package/src/learn/learningRegistry.ts +502 -0
  153. package/src/longevity/LongevityResults.tsx +85 -0
  154. package/src/longevity/LongevityWizard.tsx +305 -0
  155. package/src/longevity/constants.ts +15 -0
  156. package/src/longevity/factors.ts +125 -0
  157. package/src/longevity/model.ts +31 -0
  158. package/src/longevity/persistedGuard.ts +129 -0
  159. package/src/longevity/storage.ts +40 -0
  160. package/src/mc/messages.ts +118 -0
  161. package/src/mc/monteCarlo.worker.ts +44 -0
  162. package/src/mc/pool.ts +267 -0
  163. package/src/mc/runRequest.ts +125 -0
  164. package/src/optimize/messages.ts +84 -0
  165. package/src/optimize/optimize.worker.ts +29 -0
  166. package/src/optimize/runOptimize.ts +92 -0
  167. package/src/optimize/runSpendingSolve.ts +47 -0
  168. package/src/optimize/runner.ts +21 -0
  169. package/src/optimize/spendingMessages.ts +44 -0
  170. package/src/optimize/spendingRunner.ts +21 -0
  171. package/src/optimize/spendingSolve.worker.ts +18 -0
  172. package/src/planner/AssumptionsCardPage.tsx +136 -0
  173. package/src/planner/BucketLensCard.tsx +114 -0
  174. package/src/planner/ComparePlansPage.tsx +219 -0
  175. package/src/planner/DisclaimerPage.tsx +88 -0
  176. package/src/planner/HowTestedPage.tsx +159 -0
  177. package/src/planner/LiveStatus.tsx +15 -0
  178. package/src/planner/LongevityModal.tsx +55 -0
  179. package/src/planner/Modal.tsx +97 -0
  180. package/src/planner/MonteCarloPage.tsx +907 -0
  181. package/src/planner/OptimizePage.tsx +611 -0
  182. package/src/planner/PlanContext.tsx +198 -0
  183. package/src/planner/PlanPickerPage.tsx +124 -0
  184. package/src/planner/PlanWorkspace.tsx +290 -0
  185. package/src/planner/ProvenancePanel.tsx +45 -0
  186. package/src/planner/RelocationComparePage.tsx +485 -0
  187. package/src/planner/ReportPage.tsx +375 -0
  188. package/src/planner/ResultsPage.tsx +817 -0
  189. package/src/planner/ScenariosPage.tsx +285 -0
  190. package/src/planner/SocialSecuritySection.tsx +556 -0
  191. package/src/planner/SpendingSolverPage.tsx +512 -0
  192. package/src/planner/SsAnalysisPage.tsx +1134 -0
  193. package/src/planner/SurvivalPercentileModal.tsx +161 -0
  194. package/src/planner/SurvivorTransitionPage.tsx +286 -0
  195. package/src/planner/assumptionsExport.ts +371 -0
  196. package/src/planner/bucketLens.ts +89 -0
  197. package/src/planner/chartFrame.ts +8 -0
  198. package/src/planner/chartStyle.ts +11 -0
  199. package/src/planner/dialogViews.tsx +184 -0
  200. package/src/planner/dialogs.tsx +133 -0
  201. package/src/planner/examples/ExampleLibrary.tsx +189 -0
  202. package/src/planner/examples/ExamplePreviewBanner.tsx +55 -0
  203. package/src/planner/examples/ExamplesPage.tsx +25 -0
  204. package/src/planner/examples/OpenExampleButton.tsx +61 -0
  205. package/src/planner/examples/buildAggressiveSaver.ts +102 -0
  206. package/src/planner/examples/buildAnnuityEstate.ts +137 -0
  207. package/src/planner/examples/buildBaristaFire.ts +115 -0
  208. package/src/planner/examples/buildBracketFillRoth.ts +65 -0
  209. package/src/planner/examples/buildBridgeEarlyRetirement.ts +94 -0
  210. package/src/planner/examples/buildBrokerageNoHsa.ts +109 -0
  211. package/src/planner/examples/buildCoastFire.ts +88 -0
  212. package/src/planner/examples/buildContext.ts +20 -0
  213. package/src/planner/examples/buildEarlyCareerMatch.ts +93 -0
  214. package/src/planner/examples/buildEarlyRetireeAca.ts +61 -0
  215. package/src/planner/examples/buildExampleCouple.ts +103 -0
  216. package/src/planner/examples/buildFixedTargetSpending.ts +74 -0
  217. package/src/planner/examples/buildGlidepathAllocation.ts +131 -0
  218. package/src/planner/examples/buildGuardrailsFlex.ts +120 -0
  219. package/src/planner/examples/buildHsaPropertyDepth.ts +109 -0
  220. package/src/planner/examples/buildHsaStealthRetirement.ts +97 -0
  221. package/src/planner/examples/buildLeanFatFire.ts +109 -0
  222. package/src/planner/examples/buildLtcShock.ts +62 -0
  223. package/src/planner/examples/buildMovingStateTax.ts +53 -0
  224. package/src/planner/examples/buildNoAnnuityBrokerage.ts +92 -0
  225. package/src/planner/examples/buildRmdIrmaa.ts +55 -0
  226. package/src/planner/examples/buildSalaryGrowthEscalation.ts +96 -0
  227. package/src/planner/examples/buildStaticAllocationControl.ts +96 -0
  228. package/src/planner/examples/buildSurvivorYears.ts +62 -0
  229. package/src/planner/examples/buildUnderSavedSingle.ts +51 -0
  230. package/src/planner/examples/exampleCopy.ts +23 -0
  231. package/src/planner/examples/loadExample.ts +90 -0
  232. package/src/planner/examples/registry.ts +313 -0
  233. package/src/planner/explainPanels.tsx +233 -0
  234. package/src/planner/fields.tsx +381 -0
  235. package/src/planner/format.ts +33 -0
  236. package/src/planner/home/DataAndPrivacyCard.tsx +56 -0
  237. package/src/planner/home/GettingStartedPaths.tsx +46 -0
  238. package/src/planner/home/GettingStartedReopener.tsx +32 -0
  239. package/src/planner/home/StartHereLinks.tsx +22 -0
  240. package/src/planner/home/WelcomeHero.tsx +39 -0
  241. package/src/planner/home/YourPlans.tsx +72 -0
  242. package/src/planner/home/importErrorMessage.ts +22 -0
  243. package/src/planner/home/startHereSlugs.ts +7 -0
  244. package/src/planner/home/useHomeData.ts +190 -0
  245. package/src/planner/home/useHomeMode.ts +47 -0
  246. package/src/planner/householdActions.ts +22 -0
  247. package/src/planner/insights/InsightCardView.tsx +340 -0
  248. package/src/planner/insights/InsightsPage.tsx +204 -0
  249. package/src/planner/insights/categoryLabels.ts +11 -0
  250. package/src/planner/learnLinks.ts +85 -0
  251. package/src/planner/marketModelPicker.ts +172 -0
  252. package/src/planner/optimizePageChart.ts +40 -0
  253. package/src/planner/optimizePageClaim.ts +64 -0
  254. package/src/planner/planCompleteness.ts +27 -0
  255. package/src/planner/planContextCore.ts +26 -0
  256. package/src/planner/planner.css +2304 -0
  257. package/src/planner/provenanceLinks.ts +25 -0
  258. package/src/planner/sections/AccountFields.tsx +872 -0
  259. package/src/planner/sections/AccountsSection.tsx +89 -0
  260. package/src/planner/sections/AllocationPanel.tsx +261 -0
  261. package/src/planner/sections/AssumptionsSection.tsx +256 -0
  262. package/src/planner/sections/HouseholdSection.tsx +243 -0
  263. package/src/planner/sections/IncomeFloorSection.tsx +418 -0
  264. package/src/planner/sections/IncomeSection.tsx +170 -0
  265. package/src/planner/sections/InsuranceSection.tsx +362 -0
  266. package/src/planner/sections/SpendingSection.tsx +904 -0
  267. package/src/planner/sections/StrategySection.tsx +349 -0
  268. package/src/planner/sections/UpdateBalancesPanel.tsx +182 -0
  269. package/src/planner/sections/sectionHelpers.ts +48 -0
  270. package/src/planner/sections/shared.tsx +15 -0
  271. package/src/planner/sections.tsx +15 -0
  272. package/src/planner/ssAnalysis.ts +325 -0
  273. package/src/planner/successBand.ts +20 -0
  274. package/src/planner/survivorAnalysis.ts +277 -0
  275. package/src/planner/usStates.ts +19 -0
  276. package/src/planner/useMcSuccessRate.ts +77 -0
  277. package/src/planner/useProjection.ts +63 -0
  278. package/src/relocation/messages.ts +21 -0
  279. package/src/relocation/relocation.worker.ts +18 -0
  280. package/src/relocation/runRelocation.ts +17 -0
  281. package/src/relocation/runner.ts +22 -0
  282. package/src/report/brandingContext.ts +15 -0
  283. package/src/report/downloadReport.ts +34 -0
  284. package/src/report/reportHtml.ts +547 -0
  285. package/src/routes/LearnRoutes.tsx +46 -0
  286. package/src/routes/PlanRoutes.tsx +55 -0
  287. package/src/routes/RouteFallback.tsx +9 -0
  288. package/src/socialSecurity/breakEven.ts +107 -0
  289. package/src/socialSecurity/expectedPv.ts +164 -0
  290. package/src/socialSecurity/explain.ts +92 -0
  291. package/src/socialSecurity/ficaReturn.ts +81 -0
  292. package/src/socialSecurity/persistedSsGuard.ts +138 -0
  293. package/src/socialSecurity/ssFormUtils.ts +48 -0
  294. package/src/socialSecurity/ssaStatementXml.ts +156 -0
  295. package/src/socialSecurity/storage.ts +69 -0
  296. package/src/socialSecurity/survivorSwitching.ts +153 -0
  297. package/src/testSupport/samplePlan.ts +2 -0
  298. 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
+ }