@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,485 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Relocation Compare (Explore rail): "where should I retire?" priced on the
|
|
3
|
+
* user's actual plan. Picks up to MAX_RELOCATION_CANDIDATES candidate states
|
|
4
|
+
* (each optionally with a move year, a local-rate knob, and a flat spending
|
|
5
|
+
* delta), runs the real plan once per candidate in a Web Worker — shared
|
|
6
|
+
* market paths for the success rate — and ranks the rows with a per-state
|
|
7
|
+
* driver drill-down. Income tax only; the page names what's out of scope.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { useMemo, useRef, useState } from 'react'
|
|
11
|
+
import { useNavigate } from 'react-router-dom'
|
|
12
|
+
|
|
13
|
+
import type { StateRetirementExclusion } from '@retiregolden/engine/params/state'
|
|
14
|
+
import {
|
|
15
|
+
MAX_RELOCATION_CANDIDATES,
|
|
16
|
+
relocationScenarioPatch,
|
|
17
|
+
type RelocationCandidate,
|
|
18
|
+
type RelocationCandidateRow,
|
|
19
|
+
type RelocationComparison,
|
|
20
|
+
} from '@retiregolden/engine/projection/relocation'
|
|
21
|
+
import type { Plan } from '@retiregolden/engine/model/plan'
|
|
22
|
+
import { runRelocationCompare } from '../relocation/runner'
|
|
23
|
+
import { LearnAboutScreen } from '../learn/LearnAboutScreen'
|
|
24
|
+
import { LearnLink } from '../learn/LearnLink'
|
|
25
|
+
import { HelpTip, NumberField, PercentField, SelectField } from './fields'
|
|
26
|
+
import { fmtMoney, fmtPct } from './format'
|
|
27
|
+
import { LEARN } from './learnLinks'
|
|
28
|
+
import { buildModel } from './marketModelPicker'
|
|
29
|
+
import { usePlan } from './planContextCore'
|
|
30
|
+
import { currentStartYear, seedFromPlanId } from './useProjection'
|
|
31
|
+
import { US_STATES } from './usStates'
|
|
32
|
+
|
|
33
|
+
const RELOCATION_MC_PATHS = 500
|
|
34
|
+
|
|
35
|
+
type RankMetric = 'lifetimeTax' | 'estate' | 'success'
|
|
36
|
+
|
|
37
|
+
function makeScenarioId(): string {
|
|
38
|
+
return typeof crypto !== 'undefined' && 'randomUUID' in crypto
|
|
39
|
+
? `scenario-${crypto.randomUUID()}`
|
|
40
|
+
: `scenario-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function exclusionLabel(rule: StateRetirementExclusion): string {
|
|
44
|
+
if (rule.kind === 'full') return rule.minAge !== undefined ? `fully excluded from age ${rule.minAge}` : 'fully excluded'
|
|
45
|
+
if (rule.kind === 'capped') {
|
|
46
|
+
const cap = fmtMoney(rule.capPerPerson ?? 0)
|
|
47
|
+
return rule.minAge !== undefined ? `up to ${cap}/person from age ${rule.minAge}` : `up to ${cap}/person`
|
|
48
|
+
}
|
|
49
|
+
return 'taxed like other income'
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function DriverRow({ label, value, note }: { label: string; value: string; note?: string }) {
|
|
53
|
+
return (
|
|
54
|
+
<tr>
|
|
55
|
+
<td>{label}</td>
|
|
56
|
+
<td style={{ textAlign: 'right' }}>{value}</td>
|
|
57
|
+
<td className="muted">{note ?? ''}</td>
|
|
58
|
+
</tr>
|
|
59
|
+
)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function DriverDetails({ row }: { row: RelocationCandidateRow }) {
|
|
63
|
+
const d = row.drivers
|
|
64
|
+
if (!d) return null
|
|
65
|
+
if (!d.facts) {
|
|
66
|
+
return (
|
|
67
|
+
<p className="field-hint">
|
|
68
|
+
{row.modeled
|
|
69
|
+
? 'Driver attribution is unavailable for this row.'
|
|
70
|
+
: 'This row was priced without a modeled state pack (flat override or unmodeled state), so per-driver attribution is unavailable.'}
|
|
71
|
+
</p>
|
|
72
|
+
)
|
|
73
|
+
}
|
|
74
|
+
const f = d.facts
|
|
75
|
+
if (!f.hasIncomeTax) {
|
|
76
|
+
// A structural $0 only holds when the whole residency path was untaxed;
|
|
77
|
+
// a move-year candidate still pays the origin state before the move.
|
|
78
|
+
return row.lifetimeStateLocalTax === 0 ? (
|
|
79
|
+
<p className="field-hint">
|
|
80
|
+
{f.stateName} levies no broad income tax — the whole state+local line is $0 by construction. Property, sales,
|
|
81
|
+
and other taxes are outside this model.
|
|
82
|
+
</p>
|
|
83
|
+
) : (
|
|
84
|
+
<p className="field-hint">
|
|
85
|
+
{f.stateName} levies no broad income tax, so the years lived there contribute $0 — the{' '}
|
|
86
|
+
{fmtMoney(row.lifetimeStateLocalTax)} lifetime state+local line comes from the years before the move (or other
|
|
87
|
+
residences on this row's path). Property, sales, and other taxes are outside this model.
|
|
88
|
+
</p>
|
|
89
|
+
)
|
|
90
|
+
}
|
|
91
|
+
return (
|
|
92
|
+
<div className="table-scroll">
|
|
93
|
+
<table>
|
|
94
|
+
<thead>
|
|
95
|
+
<tr>
|
|
96
|
+
<th style={{ textAlign: 'left' }}>Driver ({f.stateName})</th>
|
|
97
|
+
<th style={{ textAlign: 'right' }}>Lifetime state tax it saves</th>
|
|
98
|
+
<th style={{ textAlign: 'left' }}>Rule</th>
|
|
99
|
+
</tr>
|
|
100
|
+
</thead>
|
|
101
|
+
<tbody>
|
|
102
|
+
<DriverRow
|
|
103
|
+
label="Social Security treatment"
|
|
104
|
+
value={f.taxesSocialSecurity ? '—' : fmtMoney(d.ssTreatmentSavings)}
|
|
105
|
+
note={
|
|
106
|
+
f.taxesSocialSecurity
|
|
107
|
+
? 'Taxes the federally taxable part of Social Security'
|
|
108
|
+
: 'Leaves Social Security out of the state base'
|
|
109
|
+
}
|
|
110
|
+
/>
|
|
111
|
+
<DriverRow
|
|
112
|
+
label={f.retirementRuleShared ? 'Retirement-income exclusion (one shared rule)' : 'Retirement-income exclusions (both buckets)'}
|
|
113
|
+
value={fmtMoney(d.retirementExclusionSavings)}
|
|
114
|
+
note={
|
|
115
|
+
f.retirementRuleShared
|
|
116
|
+
? `All retirement income: ${exclusionLabel(f.retirementPrivate)}`
|
|
117
|
+
: `Private: ${exclusionLabel(f.retirementPrivate)}; public: ${exclusionLabel(f.retirementPublic)}`
|
|
118
|
+
}
|
|
119
|
+
/>
|
|
120
|
+
{!f.retirementRuleShared ? (
|
|
121
|
+
<DriverRow
|
|
122
|
+
label="— of which the separate public-pension rule"
|
|
123
|
+
value={fmtMoney(d.publicPensionExclusionSavings)}
|
|
124
|
+
note={exclusionLabel(f.retirementPublic)}
|
|
125
|
+
/>
|
|
126
|
+
) : null}
|
|
127
|
+
<DriverRow
|
|
128
|
+
label="Capital-gain treatment"
|
|
129
|
+
value={fmtMoney(d.capitalGainsTreatmentSavings)}
|
|
130
|
+
note={
|
|
131
|
+
f.capitalGainsTaxablePct >= 100
|
|
132
|
+
? f.capitalLossCarryforwardConformity === 'currentYearOnly'
|
|
133
|
+
? 'Gains fully taxed; prior-year loss carryforwards not honored'
|
|
134
|
+
: 'Net gains taxed as ordinary income'
|
|
135
|
+
: `${f.capitalGainsTaxablePct}% of net gains in the state base`
|
|
136
|
+
}
|
|
137
|
+
/>
|
|
138
|
+
</tbody>
|
|
139
|
+
</table>
|
|
140
|
+
<p className="field-hint" style={{ marginTop: '0.5rem' }}>
|
|
141
|
+
Each figure re-prices every ledger year with that one rule neutralized, so the figures explain — but need not
|
|
142
|
+
sum to — the {fmtMoney(d.totalStateLocalTax)} lifetime state+local tax this row actually paid (top marginal
|
|
143
|
+
rate {f.topRatePct}%). A negative figure means the rule costs more than the benchmark.
|
|
144
|
+
</p>
|
|
145
|
+
</div>
|
|
146
|
+
)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
interface CandidateDraft {
|
|
150
|
+
state: string
|
|
151
|
+
moveYear: number | null
|
|
152
|
+
localRatePct: number
|
|
153
|
+
spendingDeltaPct: number
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function draftToCandidate(draft: CandidateDraft): RelocationCandidate {
|
|
157
|
+
return {
|
|
158
|
+
state: draft.state,
|
|
159
|
+
moveYear: draft.moveYear ?? undefined,
|
|
160
|
+
localRatePct: draft.localRatePct > 0 ? draft.localRatePct : 0,
|
|
161
|
+
spendingDeltaPct: draft.spendingDeltaPct !== 0 ? draft.spendingDeltaPct : undefined,
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function RelocationComparePage() {
|
|
166
|
+
const { plan, update } = usePlan()
|
|
167
|
+
const navigate = useNavigate()
|
|
168
|
+
const startYear = currentStartYear()
|
|
169
|
+
|
|
170
|
+
const [drafts, setDrafts] = useState<CandidateDraft[]>([
|
|
171
|
+
{ state: 'FL', moveYear: null, localRatePct: 0, spendingDeltaPct: 0 },
|
|
172
|
+
])
|
|
173
|
+
const [includeMc, setIncludeMc] = useState(true)
|
|
174
|
+
const [rankBy, setRankBy] = useState<RankMetric>('lifetimeTax')
|
|
175
|
+
const [running, setRunning] = useState(false)
|
|
176
|
+
const runToken = useRef(0)
|
|
177
|
+
const [compareState, setCompareState] = useState<{
|
|
178
|
+
forPlan: Plan
|
|
179
|
+
forDrafts: CandidateDraft[]
|
|
180
|
+
result: RelocationComparison | null
|
|
181
|
+
error: string | null
|
|
182
|
+
} | null>(null)
|
|
183
|
+
|
|
184
|
+
// Results are keyed to the plan object AND the candidate drafts they ran
|
|
185
|
+
// on: any plan or candidate edit makes them disappear instead of showing
|
|
186
|
+
// rows that no longer match the form.
|
|
187
|
+
const fresh = compareState !== null && compareState.forPlan === plan && compareState.forDrafts === drafts
|
|
188
|
+
const result = fresh ? compareState.result : null
|
|
189
|
+
const error = fresh ? compareState.error : null
|
|
190
|
+
|
|
191
|
+
const overrideActive = plan.assumptions.stateEffectiveTaxPct > 0
|
|
192
|
+
|
|
193
|
+
const run = () => {
|
|
194
|
+
// Only the latest run may commit results, so a slow worker completion can
|
|
195
|
+
// never present rows for a form the user has since edited.
|
|
196
|
+
const token = ++runToken.current
|
|
197
|
+
const forPlan = plan
|
|
198
|
+
const forDrafts = drafts
|
|
199
|
+
setRunning(true)
|
|
200
|
+
setCompareState(null)
|
|
201
|
+
const monteCarlo = includeMc
|
|
202
|
+
? {
|
|
203
|
+
model: buildModel('lognormal', plan.assumptions.inflationPct, 12, 60, plan),
|
|
204
|
+
pathCount: RELOCATION_MC_PATHS,
|
|
205
|
+
seed: seedFromPlanId(plan.id),
|
|
206
|
+
}
|
|
207
|
+
: null
|
|
208
|
+
runRelocationCompare({ plan: forPlan, candidates: drafts.map(draftToCandidate), startYear, monteCarlo })
|
|
209
|
+
.then((r) => {
|
|
210
|
+
if (token === runToken.current) setCompareState({ forPlan, forDrafts, result: r, error: null })
|
|
211
|
+
})
|
|
212
|
+
.catch((e: unknown) => {
|
|
213
|
+
if (token === runToken.current) {
|
|
214
|
+
setCompareState({ forPlan, forDrafts, result: null, error: e instanceof Error ? e.message : String(e) })
|
|
215
|
+
}
|
|
216
|
+
})
|
|
217
|
+
.finally(() => {
|
|
218
|
+
if (token === runToken.current) setRunning(false)
|
|
219
|
+
})
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const addScenario = (row: RelocationCandidateRow) => {
|
|
223
|
+
if (!row.candidate) return
|
|
224
|
+
const candidate = row.candidate
|
|
225
|
+
const baseName = candidate.moveYear !== undefined
|
|
226
|
+
? `Relocate to ${candidate.state} in ${candidate.moveYear}`
|
|
227
|
+
: `Relocate to ${candidate.state}`
|
|
228
|
+
const names = new Set(plan.scenarios.map((s) => s.name))
|
|
229
|
+
let name = baseName
|
|
230
|
+
let suffix = 2
|
|
231
|
+
while (names.has(name)) name = `${baseName} (${suffix++})`
|
|
232
|
+
const patch = relocationScenarioPatch(plan, candidate, startYear)
|
|
233
|
+
update((d) => {
|
|
234
|
+
d.scenarios.push({ id: makeScenarioId(), name, patch })
|
|
235
|
+
})
|
|
236
|
+
navigate(`/plan/${plan.id}/scenarios`)
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const baseline = result?.rows.find((r) => r.id === 'baseline') ?? null
|
|
240
|
+
// "Success rate" is only a real ordering when the last run priced one.
|
|
241
|
+
const effectiveRankBy: RankMetric = rankBy === 'success' && !result?.monteCarlo ? 'lifetimeTax' : rankBy
|
|
242
|
+
const rankedCandidates = useMemo(() => {
|
|
243
|
+
if (!result) return []
|
|
244
|
+
const rows = result.rows.filter((r) => r.id !== 'baseline')
|
|
245
|
+
const key = (r: RelocationCandidateRow): number => {
|
|
246
|
+
if (r.error) return Number.POSITIVE_INFINITY
|
|
247
|
+
if (effectiveRankBy === 'estate') return -r.endingAfterTaxEstate
|
|
248
|
+
if (effectiveRankBy === 'success') return -(r.successRate ?? -1)
|
|
249
|
+
return r.lifetimeTaxesAndPenalties
|
|
250
|
+
}
|
|
251
|
+
return [...rows].sort((a, b) => key(a) - key(b) || a.destinationState.localeCompare(b.destinationState))
|
|
252
|
+
}, [result, effectiveRankBy])
|
|
253
|
+
|
|
254
|
+
const deflateEnd = (row: RelocationCandidateRow, amount: number) =>
|
|
255
|
+
amount / Math.pow(1 + plan.assumptions.inflationPct / 100, row.endYear - startYear)
|
|
256
|
+
|
|
257
|
+
return (
|
|
258
|
+
<section>
|
|
259
|
+
<div className="card">
|
|
260
|
+
<h2>Relocation Compare</h2>
|
|
261
|
+
<p className="card-hint">
|
|
262
|
+
"Which state should I retire in?" — priced on <em>your</em> plan, not a generic ranking. Each
|
|
263
|
+
candidate state re-runs your full year-by-year projection (federal + modeled state tax, retirement-income
|
|
264
|
+
exclusions, Social Security treatment, capital-gain rules, split-year moves), so the rows differ only by
|
|
265
|
+
where you live. <LearnLink {...LEARN.stateRelocation} />
|
|
266
|
+
</p>
|
|
267
|
+
<div className="callout callout--info">
|
|
268
|
+
<p className="card-hint">
|
|
269
|
+
<strong>Income tax is one relocation factor, not the decision.</strong> Property and sales taxes, housing
|
|
270
|
+
and living costs, healthcare access, and being near people you love are all outside this model. The
|
|
271
|
+
optional local-rate and spending-adjustment knobs are blunt approximations you control — nothing here
|
|
272
|
+
recommends a "best state." <LearnLink {...LEARN.stateRelocation} />
|
|
273
|
+
</p>
|
|
274
|
+
</div>
|
|
275
|
+
{overrideActive ? (
|
|
276
|
+
<div className="callout callout--warn">
|
|
277
|
+
<p className="card-hint">
|
|
278
|
+
Your plan sets a flat state-tax override ({plan.assumptions.stateEffectiveTaxPct}% under Assumptions),
|
|
279
|
+
which replaces the modeled per-state rules. The baseline row keeps it; candidate rows clear it so the
|
|
280
|
+
destination's modeled rules can apply — so part of any difference below comes from dropping the
|
|
281
|
+
override itself.
|
|
282
|
+
</p>
|
|
283
|
+
</div>
|
|
284
|
+
) : null}
|
|
285
|
+
</div>
|
|
286
|
+
|
|
287
|
+
<div className="card">
|
|
288
|
+
<h2>Candidate states</h2>
|
|
289
|
+
<p className="card-hint">
|
|
290
|
+
Up to {MAX_RELOCATION_CANDIDATES} states. Leave "Move year" blank to price the whole plan as a
|
|
291
|
+
resident there; set a year to model a split-year move (July, like the Household screen). Nothing here
|
|
292
|
+
changes your plan — use "Add as scenario" on a result row to keep one.
|
|
293
|
+
</p>
|
|
294
|
+
{drafts.map((draft, i) => (
|
|
295
|
+
<div className="item-row" key={i} style={{ marginTop: i === 0 ? 0 : '0.75rem' }}>
|
|
296
|
+
<div className="item-row-head">
|
|
297
|
+
<span className="item-row-title">
|
|
298
|
+
<span className="type-chip">Candidate</span>
|
|
299
|
+
{US_STATES.find((s) => s.value === draft.state)?.label ?? draft.state}
|
|
300
|
+
</span>
|
|
301
|
+
<button
|
|
302
|
+
type="button"
|
|
303
|
+
className="btn-ghost btn-ghost-danger"
|
|
304
|
+
onClick={() => setDrafts((ds) => ds.filter((_, j) => j !== i))}
|
|
305
|
+
>
|
|
306
|
+
Remove
|
|
307
|
+
</button>
|
|
308
|
+
</div>
|
|
309
|
+
<div className="form-grid">
|
|
310
|
+
<SelectField
|
|
311
|
+
label="State"
|
|
312
|
+
value={draft.state}
|
|
313
|
+
options={US_STATES}
|
|
314
|
+
onCommit={(v) => setDrafts((ds) => ds.map((d, j) => (j === i ? { ...d, state: v } : d)))}
|
|
315
|
+
/>
|
|
316
|
+
<NumberField
|
|
317
|
+
label="Move year (optional)"
|
|
318
|
+
help="Blank prices the whole plan as a resident of the candidate state (your planned moves are replaced). A year models staying put until a split-year July move — the move-year is taxed part-year in each state."
|
|
319
|
+
value={draft.moveYear}
|
|
320
|
+
allowNull
|
|
321
|
+
min={startYear}
|
|
322
|
+
max={startYear + 60}
|
|
323
|
+
onCommit={(v) =>
|
|
324
|
+
setDrafts((ds) => ds.map((d, j) => (j === i ? { ...d, moveYear: v === null ? null : Math.round(v) } : d)))
|
|
325
|
+
}
|
|
326
|
+
/>
|
|
327
|
+
<PercentField
|
|
328
|
+
label="Local income tax (optional)"
|
|
329
|
+
help="Flat local/city rate on state taxable income in the destination (e.g. ~3.9% in NYC). Replaces the plan's local rate for this candidate, across the whole plan — exact for a from-the-start candidate, an approximation across a mid-plan move."
|
|
330
|
+
value={draft.localRatePct}
|
|
331
|
+
min={0}
|
|
332
|
+
max={10}
|
|
333
|
+
onCommit={(v) => setDrafts((ds) => ds.map((d, j) => (j === i ? { ...d, localRatePct: v ?? 0 } : d)))}
|
|
334
|
+
/>
|
|
335
|
+
<PercentField
|
|
336
|
+
label="Spending adjustment (optional)"
|
|
337
|
+
help="Blunt cost-of-living knob: changes baseline lifestyle spending by this percent for the candidate (e.g. -10 for a cheaper metro, +15 for a pricier one). Applies to the whole plan, not just after the move."
|
|
338
|
+
value={draft.spendingDeltaPct}
|
|
339
|
+
min={-50}
|
|
340
|
+
max={50}
|
|
341
|
+
onCommit={(v) => setDrafts((ds) => ds.map((d, j) => (j === i ? { ...d, spendingDeltaPct: v ?? 0 } : d)))}
|
|
342
|
+
/>
|
|
343
|
+
</div>
|
|
344
|
+
</div>
|
|
345
|
+
))}
|
|
346
|
+
<div style={{ display: 'flex', gap: '0.75rem', flexWrap: 'wrap', alignItems: 'center', marginTop: '0.75rem' }}>
|
|
347
|
+
<button
|
|
348
|
+
type="button"
|
|
349
|
+
className="btn btn-secondary btn-small"
|
|
350
|
+
disabled={drafts.length >= MAX_RELOCATION_CANDIDATES}
|
|
351
|
+
onClick={() => setDrafts((ds) => [...ds, { state: 'TX', moveYear: null, localRatePct: 0, spendingDeltaPct: 0 }])}
|
|
352
|
+
>
|
|
353
|
+
Add state
|
|
354
|
+
</button>
|
|
355
|
+
<label style={{ display: 'inline-flex', alignItems: 'center', gap: '0.4rem' }}>
|
|
356
|
+
<input type="checkbox" checked={includeMc} onChange={(e) => setIncludeMc(e.target.checked)} />
|
|
357
|
+
<span className="field-label" style={{ margin: 0 }}>
|
|
358
|
+
Success rate ({RELOCATION_MC_PATHS} shared market paths per state)
|
|
359
|
+
</span>
|
|
360
|
+
<HelpTip text="Runs a Monte Carlo per row with the same seed and market paths, so path N is the identical market history for every state — differences are the state's doing, not luck. Uses the smooth-randomness model; the Monte Carlo page has the full model catalog." />
|
|
361
|
+
</label>
|
|
362
|
+
<button type="button" className="btn btn-primary btn-small" disabled={running || drafts.length === 0} onClick={run}>
|
|
363
|
+
{running ? 'Comparing…' : 'Run compare'}
|
|
364
|
+
</button>
|
|
365
|
+
</div>
|
|
366
|
+
{running ? <div className="skeleton" style={{ height: '2rem', marginTop: '0.75rem' }} aria-label="Comparing states" /> : null}
|
|
367
|
+
{error ? <p style={{ color: 'var(--bad)' }}>Compare error: {error}</p> : null}
|
|
368
|
+
</div>
|
|
369
|
+
|
|
370
|
+
{result && baseline && !running ? (
|
|
371
|
+
<div className="card">
|
|
372
|
+
<h2>Ranked results</h2>
|
|
373
|
+
<p className="card-hint">
|
|
374
|
+
Every row is your full plan, identical except for residence (and any knobs you set on the candidate).
|
|
375
|
+
Deltas are vs. staying in {baseline.destinationState}. Dollar columns are nominal lifetime sums; the
|
|
376
|
+
estate column is deflated to today's dollars.
|
|
377
|
+
</p>
|
|
378
|
+
<div className="form-grid">
|
|
379
|
+
<SelectField
|
|
380
|
+
label="Rank by"
|
|
381
|
+
value={effectiveRankBy}
|
|
382
|
+
options={[
|
|
383
|
+
{ value: 'lifetimeTax', label: 'Lifetime taxes & penalties (low to high)' },
|
|
384
|
+
{ value: 'estate', label: 'Ending after-tax estate (high to low)' },
|
|
385
|
+
// Only a real ordering when this run priced a success rate.
|
|
386
|
+
...(result.monteCarlo ? [{ value: 'success' as const, label: 'Success rate (high to low)' }] : []),
|
|
387
|
+
]}
|
|
388
|
+
onCommit={setRankBy}
|
|
389
|
+
/>
|
|
390
|
+
</div>
|
|
391
|
+
<div className="table-scroll">
|
|
392
|
+
<table>
|
|
393
|
+
<thead>
|
|
394
|
+
<tr>
|
|
395
|
+
<th style={{ textAlign: 'left' }}>Residence</th>
|
|
396
|
+
<th style={{ textAlign: 'right' }}>
|
|
397
|
+
Lifetime state+local tax <HelpTip text="Sum of the ledger's per-year state and local income-tax lines over the whole projection (nominal). The drill-down explains which state rules drive it." />
|
|
398
|
+
</th>
|
|
399
|
+
<th style={{ textAlign: 'right' }}>
|
|
400
|
+
Lifetime taxes & penalties <HelpTip text="Federal + state + local + penalties over the whole projection (nominal) — the ranking default, since a state change also moves federal interactions like deduction and bracket timing." />
|
|
401
|
+
</th>
|
|
402
|
+
<th style={{ textAlign: 'right' }}>Δ vs staying</th>
|
|
403
|
+
<th style={{ textAlign: 'right' }}>Ending after-tax estate (today's $)</th>
|
|
404
|
+
{result.monteCarlo ? <th style={{ textAlign: 'right' }}>Success rate</th> : null}
|
|
405
|
+
</tr>
|
|
406
|
+
</thead>
|
|
407
|
+
<tbody>
|
|
408
|
+
{[baseline, ...rankedCandidates].map((row) => {
|
|
409
|
+
const delta = row.error ? null : row.lifetimeTaxesAndPenalties - baseline.lifetimeTaxesAndPenalties
|
|
410
|
+
return (
|
|
411
|
+
<tr key={row.id}>
|
|
412
|
+
<td>
|
|
413
|
+
{row.id === 'baseline' ? <strong>{row.label}</strong> : row.label}
|
|
414
|
+
{!row.error && !row.modeled ? (
|
|
415
|
+
<>
|
|
416
|
+
{' '}
|
|
417
|
+
<HelpTip text="Part of this row was priced without a modeled state pack (unmodeled state, or the flat override) — treat its state-tax line as approximate." />
|
|
418
|
+
</>
|
|
419
|
+
) : null}
|
|
420
|
+
</td>
|
|
421
|
+
{row.error ? (
|
|
422
|
+
<td colSpan={result.monteCarlo ? 5 : 4} style={{ color: 'var(--bad)' }}>
|
|
423
|
+
{row.error}
|
|
424
|
+
</td>
|
|
425
|
+
) : (
|
|
426
|
+
<>
|
|
427
|
+
<td style={{ textAlign: 'right' }}>{fmtMoney(row.lifetimeStateLocalTax)}</td>
|
|
428
|
+
<td style={{ textAlign: 'right' }}>{fmtMoney(row.lifetimeTaxesAndPenalties)}</td>
|
|
429
|
+
<td style={{ textAlign: 'right', color: delta !== null && delta < 0 ? 'var(--good)' : undefined }}>
|
|
430
|
+
{row.id === 'baseline' || delta === null ? '—' : `${delta > 0 ? '+' : ''}${fmtMoney(delta)}`}
|
|
431
|
+
</td>
|
|
432
|
+
<td style={{ textAlign: 'right' }}>{fmtMoney(deflateEnd(row, row.endingAfterTaxEstate))}</td>
|
|
433
|
+
</>
|
|
434
|
+
)}
|
|
435
|
+
{result.monteCarlo && !row.error ? (
|
|
436
|
+
<td style={{ textAlign: 'right' }}>{row.successRate !== null ? fmtPct(row.successRate, 1) : '—'}</td>
|
|
437
|
+
) : null}
|
|
438
|
+
</tr>
|
|
439
|
+
)
|
|
440
|
+
})}
|
|
441
|
+
</tbody>
|
|
442
|
+
</table>
|
|
443
|
+
</div>
|
|
444
|
+
{[baseline, ...rankedCandidates]
|
|
445
|
+
.filter((row) => !row.error)
|
|
446
|
+
.map((row) => (
|
|
447
|
+
<details className="ss-explainer" key={row.id}>
|
|
448
|
+
<summary>What drives "{row.label}"?</summary>
|
|
449
|
+
<DriverDetails row={row} />
|
|
450
|
+
{row.candidate ? (
|
|
451
|
+
<div style={{ marginTop: '0.5rem' }}>
|
|
452
|
+
<button type="button" className="btn btn-secondary btn-small" onClick={() => addScenario(row)}>
|
|
453
|
+
Add as scenario
|
|
454
|
+
</button>
|
|
455
|
+
<p className="field-hint" style={{ marginTop: '0.5rem' }}>
|
|
456
|
+
Creates a side-by-side scenario under Scenarios using the planner's existing state-move
|
|
457
|
+
fields — your plan itself is not changed. The scenario reruns to exactly this row.
|
|
458
|
+
</p>
|
|
459
|
+
</div>
|
|
460
|
+
) : null}
|
|
461
|
+
{row.depletionYear !== null ? (
|
|
462
|
+
<p className="field-hint" style={{ color: 'var(--bad)' }}>
|
|
463
|
+
This row's deterministic projection runs short of money in {row.depletionYear}.
|
|
464
|
+
</p>
|
|
465
|
+
) : null}
|
|
466
|
+
{row.warnings.map((warning) => (
|
|
467
|
+
<p className="field-hint" key={warning}>
|
|
468
|
+
⚠ {warning}
|
|
469
|
+
</p>
|
|
470
|
+
))}
|
|
471
|
+
</details>
|
|
472
|
+
))}
|
|
473
|
+
<p className="field-hint" style={{ marginTop: '0.75rem' }}>
|
|
474
|
+
Rows use the same start year, market assumptions, and (when enabled) the same {result.monteCarlo?.pathCount}{' '}
|
|
475
|
+
seeded market paths, so differences are attributable to residence, not simulation noise. State rules come
|
|
476
|
+
from the modeled per-state packs (all 50 states + DC); property, sales, and estate/inheritance taxes are
|
|
477
|
+
not modeled.
|
|
478
|
+
</p>
|
|
479
|
+
</div>
|
|
480
|
+
) : null}
|
|
481
|
+
|
|
482
|
+
<LearnAboutScreen route="/plan/:planId/relocation" />
|
|
483
|
+
</section>
|
|
484
|
+
)
|
|
485
|
+
}
|