@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,349 @@
|
|
|
1
|
+
/** Strategy section: withdrawal order, Roth conversions, rebalancing. */
|
|
2
|
+
|
|
3
|
+
import { useId } from 'react'
|
|
4
|
+
|
|
5
|
+
import { usePlan } from '../planContextCore'
|
|
6
|
+
import { CheckboxField, MoneyField, NumberField, SelectField } from '../fields'
|
|
7
|
+
import { LearnAboutScreen } from '../../learn/LearnAboutScreen'
|
|
8
|
+
import { LearnLink } from '../../learn/LearnLink'
|
|
9
|
+
import { LEARN } from '../learnLinks'
|
|
10
|
+
import { fmtMoney } from '../format'
|
|
11
|
+
import { provenanceSource } from '../provenanceLinks'
|
|
12
|
+
import { Issues } from './shared'
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Full account-order detail for each withdrawal mode, shown as a hint under
|
|
16
|
+
* the Order select. The option labels stay short so the closed control reads
|
|
17
|
+
* cleanly at the default column width (the long labels used to clip).
|
|
18
|
+
*/
|
|
19
|
+
const WITHDRAWAL_ORDER_HINTS = {
|
|
20
|
+
sequential: 'Drains cash → taxable → vested equity comp → traditional → Roth, keeping HSA for last.',
|
|
21
|
+
proportional: 'Draws pro-rata across cash, taxable, vested equity comp, traditional, and Roth; HSA last.',
|
|
22
|
+
bracketTargeted:
|
|
23
|
+
'Fills traditional up to the target bracket first, then cash → taxable → vested equity comp → Roth, then traditional again; HSA last.',
|
|
24
|
+
} as const
|
|
25
|
+
|
|
26
|
+
export function StrategySection() {
|
|
27
|
+
const { plan, update } = usePlan()
|
|
28
|
+
const w = plan.strategies.withdrawalOrder
|
|
29
|
+
const rc = plan.strategies.rothConversion
|
|
30
|
+
const orderDetailId = useId()
|
|
31
|
+
const thisYear = new Date().getFullYear()
|
|
32
|
+
return (
|
|
33
|
+
<section>
|
|
34
|
+
<div className="card">
|
|
35
|
+
<h2>Withdrawal strategy</h2>
|
|
36
|
+
<p className="card-hint">How the engine drains accounts when spending exceeds income. RMDs always come first.</p>
|
|
37
|
+
<div className="form-grid">
|
|
38
|
+
<SelectField
|
|
39
|
+
label="Order"
|
|
40
|
+
help="Sets which accounts fund spending that income doesn't cover. The order changes when taxes fall due: pre-tax withdrawals add ordinary income now, while Roth and cash come out tax-free."
|
|
41
|
+
learn={LEARN.withdrawalOrder}
|
|
42
|
+
describedBy={orderDetailId}
|
|
43
|
+
value={w.mode}
|
|
44
|
+
options={[
|
|
45
|
+
{ value: 'sequential', label: 'Sequential' },
|
|
46
|
+
{ value: 'proportional', label: 'Proportional' },
|
|
47
|
+
{ value: 'bracketTargeted', label: 'Bracket-targeted' },
|
|
48
|
+
]}
|
|
49
|
+
onCommit={(v) =>
|
|
50
|
+
update((d) => {
|
|
51
|
+
d.strategies.withdrawalOrder = v === 'bracketTargeted' ? { mode: v, bracketPct: 22 } : { mode: v }
|
|
52
|
+
})
|
|
53
|
+
}
|
|
54
|
+
/>
|
|
55
|
+
{w.mode === 'bracketTargeted' ? (
|
|
56
|
+
<NumberField
|
|
57
|
+
label="Target bracket"
|
|
58
|
+
help="Higher brackets tax each extra dollar more; this caps how much ordinary income the strategy realizes each year."
|
|
59
|
+
hint="Fill ordinary income to the top of this bracket."
|
|
60
|
+
learn={LEARN.marginalVsEffective}
|
|
61
|
+
value={w.bracketPct}
|
|
62
|
+
suffix="%"
|
|
63
|
+
onCommit={(v) => update((d) => void (d.strategies.withdrawalOrder = { mode: 'bracketTargeted', bracketPct: v ?? 22 }))}
|
|
64
|
+
/>
|
|
65
|
+
) : null}
|
|
66
|
+
<MoneyField
|
|
67
|
+
label="Taxable safety-net floor"
|
|
68
|
+
help="An optional minimum cash + taxable reserve (today's dollars) the plan tries to keep liquid. Spending is funded from other accounts first so this cushion stays intact, and fill-to-target Roth conversions are trimmed so their tax bill never forces you below the floor. It is only dipped into as a last resort. Leave blank for no floor."
|
|
69
|
+
hint="Blank = no floor."
|
|
70
|
+
value={plan.strategies.taxableSafetyNetFloor ?? null}
|
|
71
|
+
allowNull
|
|
72
|
+
onCommit={(v) => update((d) => void (d.strategies.taxableSafetyNetFloor = v ?? undefined))}
|
|
73
|
+
/>
|
|
74
|
+
{plan.household.people.length === 2 ? (
|
|
75
|
+
<MoneyField
|
|
76
|
+
label="Survivor reserve target (today's $)"
|
|
77
|
+
help="The minimum investable balance the surviving spouse should have in the first survivor year, in today's dollars (deflated by inflation). Used as a hard constraint by the decision engine's protect-survivor-liquidity objective — candidates whose survivor-year investable falls below this target are disqualified. Leave blank for no reserve constraint."
|
|
78
|
+
hint="Blank = no survivor reserve constraint."
|
|
79
|
+
value={plan.strategies.survivorReserveTarget ?? null}
|
|
80
|
+
allowNull
|
|
81
|
+
onCommit={(v) => update((d) => void (d.strategies.survivorReserveTarget = v ?? undefined))}
|
|
82
|
+
/>
|
|
83
|
+
) : null}
|
|
84
|
+
</div>
|
|
85
|
+
{/* Always-visible account-order detail for the selected mode: the option
|
|
86
|
+
labels are short so the closed select never clips, and this line keeps
|
|
87
|
+
the full order on screen without opening the ⓘ bubble. The select
|
|
88
|
+
points here via aria-describedby so the detail is announced on focus,
|
|
89
|
+
not only when it changes. */}
|
|
90
|
+
<p className="card-hint" id={orderDetailId} aria-live="polite">
|
|
91
|
+
{WITHDRAWAL_ORDER_HINTS[w.mode]}
|
|
92
|
+
</p>
|
|
93
|
+
</div>
|
|
94
|
+
|
|
95
|
+
<div className="card">
|
|
96
|
+
<h2>Roth conversions</h2>
|
|
97
|
+
<p className="card-hint">
|
|
98
|
+
Taxes on conversions ride the normal withdrawal flow (cash/taxable first). Watch the IRMAA and ACA effects in
|
|
99
|
+
Results. <LearnLink {...LEARN.whyConversionsRaiseCosts} />
|
|
100
|
+
</p>
|
|
101
|
+
<div className="form-grid">
|
|
102
|
+
<SelectField
|
|
103
|
+
label="Mode"
|
|
104
|
+
help="Roth conversions move money from a pre-tax account into Roth, adding taxable income now to cut future taxes and RMDs."
|
|
105
|
+
learn={LEARN.rothConversionBasics}
|
|
106
|
+
value={rc.mode}
|
|
107
|
+
options={[
|
|
108
|
+
{ value: 'none', label: 'No conversions' },
|
|
109
|
+
{ value: 'manual', label: 'Manual amounts per year' },
|
|
110
|
+
{ value: 'fillToTarget', label: 'Fill to a target each year' },
|
|
111
|
+
// Display-only: set from the Optimize tab. Picking any other mode
|
|
112
|
+
// takes manual control and discards the optimized schedule.
|
|
113
|
+
...(rc.mode === 'optimized' ? [{ value: 'optimized', label: 'Optimized (from the Optimize tab)' }] : []),
|
|
114
|
+
]}
|
|
115
|
+
onCommit={(v) =>
|
|
116
|
+
update((d) => {
|
|
117
|
+
if (v === 'optimized') return // no-op; optimized is produced in the Optimize tab
|
|
118
|
+
d.strategies.rothConversion =
|
|
119
|
+
v === 'none'
|
|
120
|
+
? { mode: 'none' }
|
|
121
|
+
: v === 'manual'
|
|
122
|
+
? { mode: 'manual', conversions: [{ year: thisYear + 1, amount: 50_000 }] }
|
|
123
|
+
: { mode: 'fillToTarget', target: 'topOfBracket', targetValue: 22, startYear: thisYear + 1, endYear: thisYear + 10 }
|
|
124
|
+
})
|
|
125
|
+
}
|
|
126
|
+
/>
|
|
127
|
+
</div>
|
|
128
|
+
{rc.mode === 'optimized' ? (
|
|
129
|
+
<div className="callout callout--info">
|
|
130
|
+
This {rc.conversions.length}-year schedule (
|
|
131
|
+
{fmtMoney(rc.conversions.reduce((a, c) => a + c.amount, 0))} total) was produced by the{' '}
|
|
132
|
+
<strong>Optimize</strong> tab. Re-run or tune it there, choose <em>Accept as manual</em> to edit the amounts
|
|
133
|
+
here, or pick another mode above to take manual control.
|
|
134
|
+
</div>
|
|
135
|
+
) : null}
|
|
136
|
+
{rc.mode === 'manual' ? (
|
|
137
|
+
<>
|
|
138
|
+
{rc.conversions.map((c, i) => (
|
|
139
|
+
<div className="item-row" key={i}>
|
|
140
|
+
<div className="item-row-head">
|
|
141
|
+
<span className="item-row-title"><span className="type-chip">Convert</span>{c.year}</span>
|
|
142
|
+
<button
|
|
143
|
+
type="button"
|
|
144
|
+
className="btn-ghost btn-ghost-danger"
|
|
145
|
+
onClick={() =>
|
|
146
|
+
update((d) => {
|
|
147
|
+
const m = d.strategies.rothConversion
|
|
148
|
+
if (m.mode === 'manual') m.conversions.splice(i, 1)
|
|
149
|
+
})
|
|
150
|
+
}
|
|
151
|
+
>
|
|
152
|
+
Remove
|
|
153
|
+
</button>
|
|
154
|
+
</div>
|
|
155
|
+
<div className="form-grid">
|
|
156
|
+
<NumberField
|
|
157
|
+
label="Year"
|
|
158
|
+
value={c.year}
|
|
159
|
+
min={1900}
|
|
160
|
+
max={2200}
|
|
161
|
+
onCommit={(v) =>
|
|
162
|
+
update((d) => {
|
|
163
|
+
const m = d.strategies.rothConversion
|
|
164
|
+
if (m.mode === 'manual') m.conversions[i]!.year = Math.round(v ?? c.year)
|
|
165
|
+
})
|
|
166
|
+
}
|
|
167
|
+
/>
|
|
168
|
+
<MoneyField
|
|
169
|
+
label="Amount"
|
|
170
|
+
value={c.amount}
|
|
171
|
+
onCommit={(v) =>
|
|
172
|
+
update((d) => {
|
|
173
|
+
const m = d.strategies.rothConversion
|
|
174
|
+
if (m.mode === 'manual') m.conversions[i]!.amount = v ?? 0
|
|
175
|
+
})
|
|
176
|
+
}
|
|
177
|
+
/>
|
|
178
|
+
</div>
|
|
179
|
+
</div>
|
|
180
|
+
))}
|
|
181
|
+
<div className="add-row">
|
|
182
|
+
<button
|
|
183
|
+
type="button"
|
|
184
|
+
className="btn btn-secondary btn-small"
|
|
185
|
+
onClick={() =>
|
|
186
|
+
update((d) => {
|
|
187
|
+
const m = d.strategies.rothConversion
|
|
188
|
+
if (m.mode === 'manual') {
|
|
189
|
+
const last = m.conversions[m.conversions.length - 1]
|
|
190
|
+
m.conversions.push({ year: (last?.year ?? thisYear) + 1, amount: last?.amount ?? 50_000 })
|
|
191
|
+
}
|
|
192
|
+
})
|
|
193
|
+
}
|
|
194
|
+
>
|
|
195
|
+
+ Year
|
|
196
|
+
</button>
|
|
197
|
+
</div>
|
|
198
|
+
</>
|
|
199
|
+
) : null}
|
|
200
|
+
{rc.mode === 'fillToTarget' ? (
|
|
201
|
+
<div className="form-grid">
|
|
202
|
+
<SelectField
|
|
203
|
+
label="Target"
|
|
204
|
+
help="Each year, convert just enough to reach this ceiling — a bracket top, an IRMAA tier, the ACA cliff, or a fixed MAGI."
|
|
205
|
+
learn={LEARN.fillingTaxBracket}
|
|
206
|
+
source={provenanceSource(
|
|
207
|
+
rc.target === 'irmaaTier' ? 'medicare-irmaa' : rc.target === 'acaCliff' ? 'aca-ptc' : 'federal-brackets',
|
|
208
|
+
)}
|
|
209
|
+
value={rc.target}
|
|
210
|
+
options={[
|
|
211
|
+
{ value: 'topOfBracket', label: 'Top of tax bracket' },
|
|
212
|
+
{ value: 'irmaaTier', label: 'IRMAA tier edge' },
|
|
213
|
+
{ value: 'acaCliff', label: 'ACA 400% FPL cliff' },
|
|
214
|
+
{ value: 'fixedMagi', label: 'Fixed MAGI' },
|
|
215
|
+
]}
|
|
216
|
+
onCommit={(v) =>
|
|
217
|
+
update((d) => {
|
|
218
|
+
const m = d.strategies.rothConversion
|
|
219
|
+
if (m.mode === 'fillToTarget') {
|
|
220
|
+
m.target = v
|
|
221
|
+
m.targetValue = v === 'topOfBracket' ? 22 : v === 'irmaaTier' ? 1 : v === 'fixedMagi' ? 150_000 : null
|
|
222
|
+
}
|
|
223
|
+
})
|
|
224
|
+
}
|
|
225
|
+
/>
|
|
226
|
+
{rc.target !== 'acaCliff' ? (
|
|
227
|
+
<NumberField
|
|
228
|
+
label={rc.target === 'topOfBracket' ? 'Bracket (%)' : rc.target === 'irmaaTier' ? 'Tier index' : 'MAGI ($)'}
|
|
229
|
+
value={rc.targetValue}
|
|
230
|
+
allowNull
|
|
231
|
+
onCommit={(v) =>
|
|
232
|
+
update((d) => {
|
|
233
|
+
const m = d.strategies.rothConversion
|
|
234
|
+
if (m.mode === 'fillToTarget') m.targetValue = v
|
|
235
|
+
})
|
|
236
|
+
}
|
|
237
|
+
/>
|
|
238
|
+
) : null}
|
|
239
|
+
<NumberField
|
|
240
|
+
label="Start year"
|
|
241
|
+
value={rc.startYear}
|
|
242
|
+
min={1900}
|
|
243
|
+
max={2200}
|
|
244
|
+
onCommit={(v) =>
|
|
245
|
+
update((d) => {
|
|
246
|
+
const m = d.strategies.rothConversion
|
|
247
|
+
if (m.mode === 'fillToTarget') m.startYear = Math.round(v ?? thisYear)
|
|
248
|
+
})
|
|
249
|
+
}
|
|
250
|
+
/>
|
|
251
|
+
<NumberField
|
|
252
|
+
label="End year"
|
|
253
|
+
value={rc.endYear}
|
|
254
|
+
min={1900}
|
|
255
|
+
max={2200}
|
|
256
|
+
onCommit={(v) =>
|
|
257
|
+
update((d) => {
|
|
258
|
+
const m = d.strategies.rothConversion
|
|
259
|
+
if (m.mode === 'fillToTarget') m.endYear = Math.round(v ?? thisYear + 10)
|
|
260
|
+
})
|
|
261
|
+
}
|
|
262
|
+
/>
|
|
263
|
+
</div>
|
|
264
|
+
) : null}
|
|
265
|
+
</div>
|
|
266
|
+
|
|
267
|
+
<div className="card">
|
|
268
|
+
<h2>Charitable giving</h2>
|
|
269
|
+
<div className="form-grid">
|
|
270
|
+
<MoneyField
|
|
271
|
+
label="QCD per year (today's $)"
|
|
272
|
+
help="A qualified charitable distribution sends IRA money straight to charity from age 70½. It counts toward your RMD but never appears in taxable income — usually better than a deductible cash gift once you take the standard deduction."
|
|
273
|
+
learn={LEARN.qcd}
|
|
274
|
+
source={provenanceSource('rmd-qcd')}
|
|
275
|
+
hint="Routed out of RMDs from age 70½; excluded from income."
|
|
276
|
+
value={plan.strategies.qcdAnnual}
|
|
277
|
+
onCommit={(v) => update((d) => void (d.strategies.qcdAnnual = v ?? 0))}
|
|
278
|
+
/>
|
|
279
|
+
</div>
|
|
280
|
+
<Issues />
|
|
281
|
+
</div>
|
|
282
|
+
|
|
283
|
+
<div className="card">
|
|
284
|
+
<h2>Itemized deductions</h2>
|
|
285
|
+
<p className="card-hint">
|
|
286
|
+
Federal tax uses the larger of these and the standard deduction. Most retirees take the standard deduction —
|
|
287
|
+
turn this on only if your deductible taxes, mortgage interest, and charitable gifts together exceed it. Enter
|
|
288
|
+
today's dollars.
|
|
289
|
+
</p>
|
|
290
|
+
<div className="form-grid">
|
|
291
|
+
<CheckboxField
|
|
292
|
+
label="Itemize deductions"
|
|
293
|
+
help="When on, the projection compares your itemized total (SALT capped at the current-law limit) against the standard deduction each year and uses whichever is larger. The OBBBA senior deduction still applies on top either way."
|
|
294
|
+
learn={LEARN.itemizedDeductions}
|
|
295
|
+
value={plan.strategies.itemizedDeductions !== undefined}
|
|
296
|
+
onCommit={(v) =>
|
|
297
|
+
update((d) => void (d.strategies.itemizedDeductions = v ? { stateAndLocalTaxes: 0, mortgageInterest: 0, charitable: 0 } : undefined))
|
|
298
|
+
}
|
|
299
|
+
/>
|
|
300
|
+
</div>
|
|
301
|
+
{plan.strategies.itemizedDeductions ? (
|
|
302
|
+
<div className="form-grid">
|
|
303
|
+
<MoneyField
|
|
304
|
+
label="State & local taxes (SALT)"
|
|
305
|
+
hint="State income + property tax; capped at the current-law SALT limit."
|
|
306
|
+
value={plan.strategies.itemizedDeductions.stateAndLocalTaxes}
|
|
307
|
+
onCommit={(v) => update((d) => { if (d.strategies.itemizedDeductions) d.strategies.itemizedDeductions.stateAndLocalTaxes = v ?? 0 })}
|
|
308
|
+
/>
|
|
309
|
+
<MoneyField
|
|
310
|
+
label="Mortgage interest"
|
|
311
|
+
value={plan.strategies.itemizedDeductions.mortgageInterest}
|
|
312
|
+
onCommit={(v) => update((d) => { if (d.strategies.itemizedDeductions) d.strategies.itemizedDeductions.mortgageInterest = v ?? 0 })}
|
|
313
|
+
/>
|
|
314
|
+
<MoneyField
|
|
315
|
+
label="Charitable gifts"
|
|
316
|
+
value={plan.strategies.itemizedDeductions.charitable}
|
|
317
|
+
onCommit={(v) => update((d) => { if (d.strategies.itemizedDeductions) d.strategies.itemizedDeductions.charitable = v ?? 0 })}
|
|
318
|
+
/>
|
|
319
|
+
</div>
|
|
320
|
+
) : null}
|
|
321
|
+
</div>
|
|
322
|
+
|
|
323
|
+
<div className="card">
|
|
324
|
+
<h2>Capital loss carryforward</h2>
|
|
325
|
+
<p className="card-hint">
|
|
326
|
+
Net capital losses you're carrying from prior years' tax returns (Schedule D line 16). The plan nets them
|
|
327
|
+
against future realized gains first, then deducts up to $3,000/yr against other income, until used up —
|
|
328
|
+
lowering tax and AGI (and the IRMAA/ACA costs AGI drives) in early-retirement years spent drawing down a
|
|
329
|
+
brokerage. Most users leave this at $0. <LearnLink {...LEARN.lossHarvesting} />
|
|
330
|
+
</p>
|
|
331
|
+
<div className="form-grid">
|
|
332
|
+
<MoneyField
|
|
333
|
+
label="Capital loss carryforward"
|
|
334
|
+
help="The net capital loss carried into this plan's first year, in today's dollars. Treated as flat nominal (capital losses don't index). Nets against realized gains first, then up to $3,000/yr against other income."
|
|
335
|
+
value={plan.household.capitalLossCarryforward}
|
|
336
|
+
onCommit={(v) => update((d) => void (d.household.capitalLossCarryforward = Math.max(0, v ?? 0)))}
|
|
337
|
+
/>
|
|
338
|
+
</div>
|
|
339
|
+
</div>
|
|
340
|
+
|
|
341
|
+
<LearnAboutScreen route="/plan/:planId/strategy" />
|
|
342
|
+
</section>
|
|
343
|
+
)
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// ---------------------------------------------------------------------------
|
|
347
|
+
// Assumptions
|
|
348
|
+
// ---------------------------------------------------------------------------
|
|
349
|
+
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* "Update balances from a broker CSV" — the returning-user path of the broker
|
|
3
|
+
* import (onboarding-import-and-migration step 2, annual-checkup posture).
|
|
4
|
+
* Parses a Schwab/Fidelity/Vanguard positions CSV on-device and lets the user
|
|
5
|
+
* assign each account found in the file to a plan account before applying.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { useRef, useState } from 'react'
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
applyBrokerBalance,
|
|
12
|
+
BROKER_LABEL,
|
|
13
|
+
isBalanceUpdatable,
|
|
14
|
+
parseBrokerPositionsCsv,
|
|
15
|
+
type BrokerAccountBalance,
|
|
16
|
+
type BrokerId,
|
|
17
|
+
} from '../../import/brokerCsv'
|
|
18
|
+
import type { ImportReviewItem } from '../../import/reviewChecklist'
|
|
19
|
+
import { ReviewChecklist } from '../../import/ReviewChecklistView'
|
|
20
|
+
import { usePlan } from '../planContextCore'
|
|
21
|
+
import { fmtMoney } from '../format'
|
|
22
|
+
|
|
23
|
+
interface ParsedFile {
|
|
24
|
+
broker: BrokerId
|
|
25
|
+
accounts: BrokerAccountBalance[]
|
|
26
|
+
/** Selected plan-account id (or '') per parsed account, by index. */
|
|
27
|
+
targets: string[]
|
|
28
|
+
/** The parser's honesty checklist (partial basis, skipped rows, …). */
|
|
29
|
+
review: ImportReviewItem[]
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Prefer a plan account whose name shares words with the broker label. */
|
|
33
|
+
function guessTarget(label: string, candidates: Array<{ id: string; name: string }>): string {
|
|
34
|
+
const l = label.toLowerCase()
|
|
35
|
+
const hit = candidates.find((c) => l.includes(c.name.toLowerCase()) || c.name.toLowerCase().split(/\s+/).some((w) => w.length > 2 && l.includes(w)))
|
|
36
|
+
return hit?.id ?? ''
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function UpdateBalancesPanel() {
|
|
40
|
+
const { plan, update } = usePlan()
|
|
41
|
+
const [parsed, setParsed] = useState<ParsedFile | null>(null)
|
|
42
|
+
const [message, setMessage] = useState<string | null>(null)
|
|
43
|
+
const fileInput = useRef<HTMLInputElement>(null)
|
|
44
|
+
|
|
45
|
+
const updatable = plan.accounts.filter(isBalanceUpdatable).map((a) => ({ id: a.id, name: a.name }))
|
|
46
|
+
|
|
47
|
+
const handleFile = async (file: File) => {
|
|
48
|
+
setMessage(null)
|
|
49
|
+
const r = parseBrokerPositionsCsv(await file.text())
|
|
50
|
+
if (!r.ok) {
|
|
51
|
+
setParsed(null)
|
|
52
|
+
setMessage(r.message)
|
|
53
|
+
return
|
|
54
|
+
}
|
|
55
|
+
setParsed({
|
|
56
|
+
broker: r.broker,
|
|
57
|
+
accounts: r.accounts,
|
|
58
|
+
targets: r.accounts.map((acc) => guessTarget(acc.accountLabel, updatable)),
|
|
59
|
+
review: r.review,
|
|
60
|
+
})
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Two file accounts pointed at one plan account would silently last-write-win.
|
|
64
|
+
const duplicateTargets = parsed
|
|
65
|
+
? [...new Set(parsed.targets.filter((t, i) => t !== '' && parsed.targets.indexOf(t) !== i))]
|
|
66
|
+
: []
|
|
67
|
+
const duplicateNames = duplicateTargets.map((id) => updatable.find((a) => a.id === id)?.name ?? id)
|
|
68
|
+
|
|
69
|
+
const apply = () => {
|
|
70
|
+
if (!parsed || duplicateTargets.length > 0) return
|
|
71
|
+
let applied = 0
|
|
72
|
+
update((d) => {
|
|
73
|
+
parsed.accounts.forEach((source, i) => {
|
|
74
|
+
const targetId = parsed.targets[i]
|
|
75
|
+
if (!targetId) return
|
|
76
|
+
const idx = d.accounts.findIndex((a) => a.id === targetId)
|
|
77
|
+
if (idx === -1) return
|
|
78
|
+
d.accounts[idx] = applyBrokerBalance(d.accounts[idx]!, source)
|
|
79
|
+
applied++
|
|
80
|
+
})
|
|
81
|
+
})
|
|
82
|
+
setParsed(null)
|
|
83
|
+
setMessage(
|
|
84
|
+
applied === 0
|
|
85
|
+
? 'No accounts were assigned, so nothing changed.'
|
|
86
|
+
: `Updated ${applied} account${applied === 1 ? '' : 's'} from the ${BROKER_LABEL[parsed.broker]} file — balances, plus cost basis where the file carried it. Review taxable accounts whose basis the file lacked.`,
|
|
87
|
+
)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return (
|
|
91
|
+
<div className="card">
|
|
92
|
+
<h2>Update balances from a broker CSV</h2>
|
|
93
|
+
<p className="card-hint">
|
|
94
|
+
Download the positions/holdings CSV from Schwab, Fidelity, or Vanguard and refresh your account
|
|
95
|
+
balances (and cost basis where the file has it) without retyping. The file is read on this device
|
|
96
|
+
only. To start a whole new plan from a file, use Import & migrate on the home screen.
|
|
97
|
+
</p>
|
|
98
|
+
{message ? (
|
|
99
|
+
<div className="callout callout--info" role="status">
|
|
100
|
+
{message}
|
|
101
|
+
</div>
|
|
102
|
+
) : null}
|
|
103
|
+
{parsed ? (
|
|
104
|
+
<>
|
|
105
|
+
<div className="year-table-wrap">
|
|
106
|
+
<table className="year-table">
|
|
107
|
+
<thead>
|
|
108
|
+
<tr>
|
|
109
|
+
<th scope="col">In the {BROKER_LABEL[parsed.broker]} file</th>
|
|
110
|
+
<th scope="col">Value</th>
|
|
111
|
+
<th scope="col">Cost basis</th>
|
|
112
|
+
<th scope="col">Apply to plan account</th>
|
|
113
|
+
</tr>
|
|
114
|
+
</thead>
|
|
115
|
+
<tbody>
|
|
116
|
+
{parsed.accounts.map((acc, i) => (
|
|
117
|
+
<tr key={`${acc.accountLabel}-${i}`}>
|
|
118
|
+
<td>{acc.accountLabel}</td>
|
|
119
|
+
<td>{fmtMoney(acc.totalValue)}</td>
|
|
120
|
+
<td>{acc.costBasis === null ? '—' : fmtMoney(acc.costBasis)}</td>
|
|
121
|
+
<td>
|
|
122
|
+
<select
|
|
123
|
+
aria-label={`Plan account for ${acc.accountLabel}`}
|
|
124
|
+
value={parsed.targets[i] ?? ''}
|
|
125
|
+
onChange={(e) =>
|
|
126
|
+
setParsed((prev) =>
|
|
127
|
+
prev ? { ...prev, targets: prev.targets.map((t, j) => (j === i ? e.target.value : t)) } : prev,
|
|
128
|
+
)
|
|
129
|
+
}
|
|
130
|
+
>
|
|
131
|
+
<option value="">Don't update</option>
|
|
132
|
+
{updatable.map((a) => (
|
|
133
|
+
<option key={a.id} value={a.id}>
|
|
134
|
+
{a.name}
|
|
135
|
+
</option>
|
|
136
|
+
))}
|
|
137
|
+
</select>
|
|
138
|
+
</td>
|
|
139
|
+
</tr>
|
|
140
|
+
))}
|
|
141
|
+
</tbody>
|
|
142
|
+
</table>
|
|
143
|
+
</div>
|
|
144
|
+
{duplicateNames.length > 0 ? (
|
|
145
|
+
<div className="callout callout--warn" role="alert">
|
|
146
|
+
Two rows are assigned to the same plan account ({duplicateNames.join(', ')}) — the second would silently
|
|
147
|
+
overwrite the first. Assign each plan account at most once.
|
|
148
|
+
</div>
|
|
149
|
+
) : null}
|
|
150
|
+
<ReviewChecklist items={parsed.review} />
|
|
151
|
+
<div className="picker-actions">
|
|
152
|
+
<button type="button" className="btn btn-primary" onClick={apply} disabled={duplicateNames.length > 0}>
|
|
153
|
+
Apply selected balances
|
|
154
|
+
</button>
|
|
155
|
+
<button type="button" className="btn btn-secondary" onClick={() => setParsed(null)}>
|
|
156
|
+
Cancel
|
|
157
|
+
</button>
|
|
158
|
+
</div>
|
|
159
|
+
</>
|
|
160
|
+
) : (
|
|
161
|
+
<div className="picker-actions" style={{ margin: 0 }}>
|
|
162
|
+
<button type="button" className="btn btn-secondary" onClick={() => fileInput.current?.click()}>
|
|
163
|
+
Choose broker CSV
|
|
164
|
+
</button>
|
|
165
|
+
</div>
|
|
166
|
+
)}
|
|
167
|
+
<input
|
|
168
|
+
ref={fileInput}
|
|
169
|
+
type="file"
|
|
170
|
+
accept=".csv,text/csv"
|
|
171
|
+
className="sr-only"
|
|
172
|
+
tabIndex={-1}
|
|
173
|
+
aria-hidden="true"
|
|
174
|
+
onChange={(e) => {
|
|
175
|
+
const f = e.target.files?.[0]
|
|
176
|
+
if (f) void handleFile(f)
|
|
177
|
+
e.target.value = ''
|
|
178
|
+
}}
|
|
179
|
+
/>
|
|
180
|
+
</div>
|
|
181
|
+
)
|
|
182
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Non-component helpers shared by the plan entry sections (kept out of the
|
|
3
|
+
* component files so react-refresh sees component-only modules).
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { Account, AllocationWeights } from '@retiregolden/engine/model/plan'
|
|
7
|
+
|
|
8
|
+
export const newId = () => crypto.randomUUID()
|
|
9
|
+
|
|
10
|
+
export const MONTH_OPTIONS = [
|
|
11
|
+
'January',
|
|
12
|
+
'February',
|
|
13
|
+
'March',
|
|
14
|
+
'April',
|
|
15
|
+
'May',
|
|
16
|
+
'June',
|
|
17
|
+
'July',
|
|
18
|
+
'August',
|
|
19
|
+
'September',
|
|
20
|
+
'October',
|
|
21
|
+
'November',
|
|
22
|
+
'December',
|
|
23
|
+
].map((label, i) => ({ value: String(i + 1), label }))
|
|
24
|
+
|
|
25
|
+
export function isIndividuallyOwnedAccount(type: Account['type']): boolean {
|
|
26
|
+
return type === 'traditional' || type === 'roth' || type === 'hsa'
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export const ACCOUNT_LABEL: Record<Account['type'], string> = {
|
|
30
|
+
cash: 'Cash',
|
|
31
|
+
taxable: 'Brokerage',
|
|
32
|
+
equityComp: 'Equity comp',
|
|
33
|
+
traditional: 'Traditional',
|
|
34
|
+
roth: 'Roth',
|
|
35
|
+
hsa: 'HSA',
|
|
36
|
+
pension: 'Pension',
|
|
37
|
+
annuity: 'Annuity',
|
|
38
|
+
property: 'Property',
|
|
39
|
+
debt: 'Debt',
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export type AllocatableAccount = Extract<Account, { type: 'taxable' | 'traditional' | 'roth' | 'hsa' }>
|
|
43
|
+
|
|
44
|
+
export const EVEN_START_WEIGHTS: AllocationWeights = { usStocks: 60, intlStocks: 10, bonds: 25, cash: 5 }
|
|
45
|
+
|
|
46
|
+
export function isAllocatable(account: Account): account is AllocatableAccount {
|
|
47
|
+
return account.type === 'taxable' || account.type === 'traditional' || account.type === 'roth' || account.type === 'hsa'
|
|
48
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** Validation-issue list shared by the plan entry sections. */
|
|
2
|
+
|
|
3
|
+
import { usePlan } from '../planContextCore'
|
|
4
|
+
|
|
5
|
+
export function Issues() {
|
|
6
|
+
const { issues } = usePlan()
|
|
7
|
+
if (issues.length === 0) return null
|
|
8
|
+
return (
|
|
9
|
+
<ul className="issue-list">
|
|
10
|
+
{issues.map((i) => (
|
|
11
|
+
<li key={i}>{i}</li>
|
|
12
|
+
))}
|
|
13
|
+
</ul>
|
|
14
|
+
)
|
|
15
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plan entry sections. Each card edits one slice of the Plan via
|
|
3
|
+
* usePlan().update; validation issues from the Zod schema surface beneath
|
|
4
|
+
* the active section. The section components live in ./sections/; this
|
|
5
|
+
* barrel keeps existing import paths stable.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export { HouseholdSection } from './sections/HouseholdSection'
|
|
9
|
+
export { AccountsSection } from './sections/AccountsSection'
|
|
10
|
+
export { IncomeSection } from './sections/IncomeSection'
|
|
11
|
+
export { IncomeFloorSection } from './sections/IncomeFloorSection'
|
|
12
|
+
export { SpendingSection } from './sections/SpendingSection'
|
|
13
|
+
export { StrategySection } from './sections/StrategySection'
|
|
14
|
+
export { AssumptionsSection } from './sections/AssumptionsSection'
|
|
15
|
+
export { InsuranceSection } from './sections/InsuranceSection'
|