@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,56 @@
|
|
|
1
|
+
import type { RefObject } from 'react'
|
|
2
|
+
|
|
3
|
+
import type { PlanSummary } from '../../data/planStore'
|
|
4
|
+
|
|
5
|
+
type DataAndPrivacyCardProps = {
|
|
6
|
+
plans: PlanSummary[] | null
|
|
7
|
+
fileInput: RefObject<HTMLInputElement | null>
|
|
8
|
+
onExportAll: () => void
|
|
9
|
+
onImportFile: (file: File) => void
|
|
10
|
+
onClearAll: () => void
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function DataAndPrivacyCard({
|
|
14
|
+
plans,
|
|
15
|
+
fileInput,
|
|
16
|
+
onExportAll,
|
|
17
|
+
onImportFile,
|
|
18
|
+
onClearAll,
|
|
19
|
+
}: DataAndPrivacyCardProps) {
|
|
20
|
+
return (
|
|
21
|
+
<div className="card home-privacy-card">
|
|
22
|
+
<h2>Your data stays on your device, not on our servers</h2>
|
|
23
|
+
<p className="card-hint">
|
|
24
|
+
RetireGolden has no accounts and no server storage. Everything you enter stays on this device, which means we
|
|
25
|
+
can never see it — and we can never recover it either. To keep a plan safe or move it to another device,
|
|
26
|
+
download a plan backup and take it with you.
|
|
27
|
+
</p>
|
|
28
|
+
<div className="picker-actions" style={{ margin: 0 }}>
|
|
29
|
+
<button type="button" className="btn btn-secondary" onClick={() => void onExportAll()} disabled={!plans || plans.length === 0}>
|
|
30
|
+
Download plan backup
|
|
31
|
+
</button>
|
|
32
|
+
<button type="button" className="btn btn-secondary" onClick={() => fileInput.current?.click()}>
|
|
33
|
+
Import previous backup
|
|
34
|
+
</button>
|
|
35
|
+
<button type="button" className="btn btn-secondary btn-danger" onClick={() => void onClearAll()}>
|
|
36
|
+
Clear all data
|
|
37
|
+
</button>
|
|
38
|
+
</div>
|
|
39
|
+
{/* Reached only through the visible "Import previous backup" button;
|
|
40
|
+
keeping it out of the tab order stops an invisible tab stop. */}
|
|
41
|
+
<input
|
|
42
|
+
ref={fileInput}
|
|
43
|
+
type="file"
|
|
44
|
+
accept="application/json"
|
|
45
|
+
className="sr-only"
|
|
46
|
+
tabIndex={-1}
|
|
47
|
+
aria-hidden="true"
|
|
48
|
+
onChange={(e) => {
|
|
49
|
+
const f = e.target.files?.[0]
|
|
50
|
+
if (f) void onImportFile(f)
|
|
51
|
+
e.target.value = ''
|
|
52
|
+
}}
|
|
53
|
+
/>
|
|
54
|
+
</div>
|
|
55
|
+
)
|
|
56
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { Link } from 'react-router-dom'
|
|
2
|
+
|
|
3
|
+
import { createEmptyPlan } from '@retiregolden/engine/model/plan'
|
|
4
|
+
|
|
5
|
+
type GettingStartedPathsProps = {
|
|
6
|
+
onCreatePlan: (plan: ReturnType<typeof createEmptyPlan>) => void
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function GettingStartedPaths({ onCreatePlan }: GettingStartedPathsProps) {
|
|
10
|
+
return (
|
|
11
|
+
<section className="home-paths" aria-labelledby="getting-started-paths-heading">
|
|
12
|
+
<h2 id="getting-started-paths-heading">Getting started</h2>
|
|
13
|
+
<div className="home-paths-grid plan-grid">
|
|
14
|
+
<Link to="/learn" className="home-path-card plan-card">
|
|
15
|
+
<span className="home-path-card-title">Learn the basics</span>
|
|
16
|
+
<span className="home-path-card-desc">
|
|
17
|
+
Read short guides on privacy, the planner sections, and what RetireGolden models.
|
|
18
|
+
</span>
|
|
19
|
+
</Link>
|
|
20
|
+
<Link to="/examples" className="home-path-card plan-card">
|
|
21
|
+
<span className="home-path-card-title">Try an example</span>
|
|
22
|
+
<span className="home-path-card-desc">
|
|
23
|
+
Open a curated household with realistic numbers — explore freely without saving anything yet.
|
|
24
|
+
</span>
|
|
25
|
+
</Link>
|
|
26
|
+
<button
|
|
27
|
+
type="button"
|
|
28
|
+
className="home-path-card plan-card"
|
|
29
|
+
onClick={() => void onCreatePlan(createEmptyPlan())}
|
|
30
|
+
>
|
|
31
|
+
<span className="home-path-card-title">Build your own plan</span>
|
|
32
|
+
<span className="home-path-card-desc">
|
|
33
|
+
Start from a blank slate and enter your household, accounts, and spending.
|
|
34
|
+
</span>
|
|
35
|
+
</button>
|
|
36
|
+
<Link to="/import" className="home-path-card plan-card">
|
|
37
|
+
<span className="home-path-card-title">Import from a file</span>
|
|
38
|
+
<span className="home-path-card-desc">
|
|
39
|
+
Seed a plan from a broker CSV, a ProjectionLab export, a spreadsheet, or last year's tax
|
|
40
|
+
return — parsed on this device, never uploaded.
|
|
41
|
+
</span>
|
|
42
|
+
</Link>
|
|
43
|
+
</div>
|
|
44
|
+
</section>
|
|
45
|
+
)
|
|
46
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { ReactNode } from 'react'
|
|
2
|
+
|
|
3
|
+
type GettingStartedReopenerProps = {
|
|
4
|
+
expanded: boolean
|
|
5
|
+
onToggle: () => void
|
|
6
|
+
panelId: string
|
|
7
|
+
children: ReactNode
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function GettingStartedReopener({ expanded, onToggle, panelId, children }: GettingStartedReopenerProps) {
|
|
11
|
+
return (
|
|
12
|
+
<div className="home-getting-started-reopener">
|
|
13
|
+
<button
|
|
14
|
+
type="button"
|
|
15
|
+
className="home-reopener-btn"
|
|
16
|
+
aria-expanded={expanded}
|
|
17
|
+
aria-controls={panelId}
|
|
18
|
+
onClick={onToggle}
|
|
19
|
+
>
|
|
20
|
+
<span className="home-reopener-icon" aria-hidden="true">
|
|
21
|
+
{expanded ? '▾' : '▸'}
|
|
22
|
+
</span>
|
|
23
|
+
New here? Getting started
|
|
24
|
+
</button>
|
|
25
|
+
{expanded ? (
|
|
26
|
+
<div id={panelId} className="home-getting-started-panel">
|
|
27
|
+
{children}
|
|
28
|
+
</div>
|
|
29
|
+
) : null}
|
|
30
|
+
</div>
|
|
31
|
+
)
|
|
32
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { LearnLink } from '../../learn/LearnLink'
|
|
2
|
+
import { getArticle } from '../../learn/learningRegistry'
|
|
3
|
+
import { START_HERE_SLUGS } from './startHereSlugs'
|
|
4
|
+
|
|
5
|
+
export function StartHereLinks() {
|
|
6
|
+
return (
|
|
7
|
+
<section className="home-start-here" aria-labelledby="start-here-heading">
|
|
8
|
+
<h2 id="start-here-heading">Start here</h2>
|
|
9
|
+
<ul className="home-start-here-list">
|
|
10
|
+
{START_HERE_SLUGS.map((slug) => {
|
|
11
|
+
const article = getArticle(slug)
|
|
12
|
+
if (!article) return null
|
|
13
|
+
return (
|
|
14
|
+
<li key={slug}>
|
|
15
|
+
<LearnLink slug={slug} label={article.title} variant="inline" />
|
|
16
|
+
</li>
|
|
17
|
+
)
|
|
18
|
+
})}
|
|
19
|
+
</ul>
|
|
20
|
+
</section>
|
|
21
|
+
)
|
|
22
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
type WelcomeHeroProps = {
|
|
2
|
+
onTryExample: () => void
|
|
3
|
+
onDismiss?: () => void
|
|
4
|
+
showDismiss?: boolean
|
|
5
|
+
headingLevel?: 'h1' | 'h2'
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function WelcomeHero({
|
|
9
|
+
onTryExample,
|
|
10
|
+
onDismiss,
|
|
11
|
+
showDismiss = false,
|
|
12
|
+
headingLevel = 'h1',
|
|
13
|
+
}: WelcomeHeroProps) {
|
|
14
|
+
const Heading = headingLevel
|
|
15
|
+
return (
|
|
16
|
+
<section className="home-hero" aria-labelledby="home-hero-heading">
|
|
17
|
+
<Heading id="home-hero-heading">Plan your retirement — privately, in your browser</Heading>
|
|
18
|
+
<p className="home-hero-subhead lede">
|
|
19
|
+
Model your household, stress-test taxes and withdrawals, and explore scenarios — with no account and no data
|
|
20
|
+
leaving this device.
|
|
21
|
+
</p>
|
|
22
|
+
<ul className="trust-chips" aria-label="Why RetireGolden">
|
|
23
|
+
<li>No accounts</li>
|
|
24
|
+
<li>Nothing leaves your device</li>
|
|
25
|
+
<li>Educational, with sources</li>
|
|
26
|
+
</ul>
|
|
27
|
+
<div className="home-hero-cta">
|
|
28
|
+
<button type="button" className="btn btn-primary" onClick={onTryExample}>
|
|
29
|
+
Try an example
|
|
30
|
+
</button>
|
|
31
|
+
{showDismiss && onDismiss ? (
|
|
32
|
+
<button type="button" className="btn btn-ghost btn-small" onClick={onDismiss}>
|
|
33
|
+
Hide getting started
|
|
34
|
+
</button>
|
|
35
|
+
) : null}
|
|
36
|
+
</div>
|
|
37
|
+
</section>
|
|
38
|
+
)
|
|
39
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { ReactNode } from 'react'
|
|
2
|
+
|
|
3
|
+
import type { PlanSummary } from '../../data/planStore'
|
|
4
|
+
|
|
5
|
+
function fmtUpdated(iso: string): string {
|
|
6
|
+
const d = new Date(iso)
|
|
7
|
+
return Number.isNaN(d.getTime()) ? '' : `Updated ${d.toLocaleDateString()} ${d.toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' })}`
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
type YourPlansProps = {
|
|
11
|
+
plans: PlanSummary[] | null
|
|
12
|
+
headingLevel?: 'h1' | 'h2'
|
|
13
|
+
actions?: ReactNode
|
|
14
|
+
onOpenPlan: (id: string) => void
|
|
15
|
+
onDuplicate: (s: PlanSummary) => void
|
|
16
|
+
onDelete: (s: PlanSummary) => void
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function YourPlans({ plans, headingLevel = 'h2', actions, onOpenPlan, onDuplicate, onDelete }: YourPlansProps) {
|
|
20
|
+
const Heading = headingLevel
|
|
21
|
+
|
|
22
|
+
if (plans === null) {
|
|
23
|
+
return <div className="skeleton" style={{ height: '8rem' }} aria-label="Loading plans" />
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (plans.length === 0) return null
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<section className="home-your-plans" aria-labelledby="your-plans-heading">
|
|
30
|
+
<div className="home-your-plans-head">
|
|
31
|
+
<Heading id="your-plans-heading">Your plans</Heading>
|
|
32
|
+
{actions ? <div className="picker-actions home-returning-actions">{actions}</div> : null}
|
|
33
|
+
</div>
|
|
34
|
+
<div className="plan-grid">
|
|
35
|
+
{plans.map((s) => (
|
|
36
|
+
/* The open action is a real button stretched over the card via CSS
|
|
37
|
+
(::after), so the whole card stays clickable without nesting the
|
|
38
|
+
Duplicate/Delete buttons inside another interactive element. */
|
|
39
|
+
<div key={s.id} className="plan-card">
|
|
40
|
+
<button
|
|
41
|
+
type="button"
|
|
42
|
+
className="plan-card-open"
|
|
43
|
+
aria-label={`Open plan ${s.name}`}
|
|
44
|
+
onClick={() => onOpenPlan(s.id)}
|
|
45
|
+
>
|
|
46
|
+
<span className="plan-card-name">{s.name}</span>
|
|
47
|
+
</button>
|
|
48
|
+
<span className="plan-card-meta">{fmtUpdated(s.updatedAtIso)}</span>
|
|
49
|
+
<span className="plan-card-actions">
|
|
50
|
+
<button
|
|
51
|
+
type="button"
|
|
52
|
+
className="btn-ghost"
|
|
53
|
+
aria-label={`Duplicate plan ${s.name}`}
|
|
54
|
+
onClick={() => void onDuplicate(s)}
|
|
55
|
+
>
|
|
56
|
+
Duplicate
|
|
57
|
+
</button>
|
|
58
|
+
<button
|
|
59
|
+
type="button"
|
|
60
|
+
className="btn-ghost btn-ghost-danger"
|
|
61
|
+
aria-label={`Delete plan ${s.name}`}
|
|
62
|
+
onClick={() => void onDelete(s)}
|
|
63
|
+
>
|
|
64
|
+
Delete
|
|
65
|
+
</button>
|
|
66
|
+
</span>
|
|
67
|
+
</div>
|
|
68
|
+
))}
|
|
69
|
+
</div>
|
|
70
|
+
</section>
|
|
71
|
+
)
|
|
72
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { parseV2Backup } from '../../data/v2Backup'
|
|
2
|
+
|
|
3
|
+
type ImportFailure = Extract<ReturnType<typeof parseV2Backup>, { ok: false }>['reason']
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Human import-failure copy with a next step (UI/UX round 2, Step 6) — never the
|
|
7
|
+
* raw enum reason with underscores, which leaked schema jargon at the user.
|
|
8
|
+
*/
|
|
9
|
+
export function importErrorMessage(reason: ImportFailure | string): string {
|
|
10
|
+
switch (reason) {
|
|
11
|
+
case 'too_large':
|
|
12
|
+
return 'That file is too large to be a RetireGolden backup. Pick the JSON file you downloaded from Data & privacy → Download backup.'
|
|
13
|
+
case 'unsupported_version':
|
|
14
|
+
return 'That backup was made by a different version of RetireGolden, so it can’t be imported here.'
|
|
15
|
+
case 'no_valid_plans':
|
|
16
|
+
return 'That backup didn’t contain any readable plans. Export a fresh one from Data & privacy → Download backup.'
|
|
17
|
+
case 'not_json':
|
|
18
|
+
case 'wrong_kind':
|
|
19
|
+
default:
|
|
20
|
+
return 'That file isn’t a RetireGolden backup. Export one from Data & privacy → Download backup, then import that file.'
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from 'react'
|
|
2
|
+
import { useNavigate } from 'react-router-dom'
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
clearAllPlans,
|
|
6
|
+
duplicatePlan,
|
|
7
|
+
listUserPlanSummaries,
|
|
8
|
+
loadPlan,
|
|
9
|
+
savePlan,
|
|
10
|
+
deletePlan,
|
|
11
|
+
type PlanSummary,
|
|
12
|
+
} from '../../data/planStore'
|
|
13
|
+
import { normalizePlansForImport, parseV2Backup, serializeV2Backup } from '../../data/v2Backup'
|
|
14
|
+
import { type Plan } from '@retiregolden/engine/model/plan'
|
|
15
|
+
import { useDialogs } from '../dialogs'
|
|
16
|
+
import { importErrorMessage } from './importErrorMessage'
|
|
17
|
+
|
|
18
|
+
export function useHomeData() {
|
|
19
|
+
const navigate = useNavigate()
|
|
20
|
+
const [plans, setPlans] = useState<PlanSummary[] | null>(null)
|
|
21
|
+
const [notice, setNotice] = useState<string | null>(null)
|
|
22
|
+
// A just-deleted plan, held in memory for a brief undo window. The delete is
|
|
23
|
+
// already committed to storage, so an expired or navigated-away toast never
|
|
24
|
+
// leaves a plan half-deleted; Undo simply re-saves the in-memory copy.
|
|
25
|
+
const [undoPlan, setUndoPlan] = useState<Plan | null>(null)
|
|
26
|
+
const undoTimer = useRef<number | null>(null)
|
|
27
|
+
const fileInput = useRef<HTMLInputElement>(null)
|
|
28
|
+
const { confirm, prompt, dialogs } = useDialogs()
|
|
29
|
+
|
|
30
|
+
const refresh = useCallback(() => {
|
|
31
|
+
void listUserPlanSummaries().then(setPlans)
|
|
32
|
+
}, [])
|
|
33
|
+
|
|
34
|
+
const clearUndoTimer = () => {
|
|
35
|
+
if (undoTimer.current !== null) {
|
|
36
|
+
clearTimeout(undoTimer.current)
|
|
37
|
+
undoTimer.current = null
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
refresh()
|
|
43
|
+
}, [refresh])
|
|
44
|
+
|
|
45
|
+
useEffect(() => () => clearUndoTimer(), [])
|
|
46
|
+
|
|
47
|
+
const openPlan = (id: string) => navigate(`/plan/${id}`)
|
|
48
|
+
|
|
49
|
+
const createAndOpen = async (plan: Plan) => {
|
|
50
|
+
const r = await savePlan(plan)
|
|
51
|
+
if (r.ok) openPlan(r.plan.id)
|
|
52
|
+
else setNotice(`Could not save the new plan: ${r.issues.join('; ')}`)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const handleExportAll = async () => {
|
|
56
|
+
const summaries = await listUserPlanSummaries()
|
|
57
|
+
const loaded: Plan[] = []
|
|
58
|
+
for (const s of summaries) {
|
|
59
|
+
const r = await loadPlan(s.id)
|
|
60
|
+
if (r.ok) loaded.push(r.plan)
|
|
61
|
+
}
|
|
62
|
+
const blob = new Blob([serializeV2Backup(loaded)], { type: 'application/json' })
|
|
63
|
+
const a = document.createElement('a')
|
|
64
|
+
a.href = URL.createObjectURL(blob)
|
|
65
|
+
a.download = `retiregolden-backup-${new Date().toISOString().slice(0, 10)}.json`
|
|
66
|
+
a.click()
|
|
67
|
+
URL.revokeObjectURL(a.href)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const handleImportFile = async (file: File) => {
|
|
71
|
+
const r = parseV2Backup(await file.text())
|
|
72
|
+
if (!r.ok) {
|
|
73
|
+
setNotice(importErrorMessage(r.reason))
|
|
74
|
+
return
|
|
75
|
+
}
|
|
76
|
+
const normalized = await normalizePlansForImport(r.plans)
|
|
77
|
+
for (const p of normalized) await savePlan(p)
|
|
78
|
+
setNotice(
|
|
79
|
+
`Imported ${normalized.length} plan${normalized.length === 1 ? '' : 's'}.` +
|
|
80
|
+
(r.warnings.length > 0 ? ` Skipped: ${r.warnings.join('; ')}` : ''),
|
|
81
|
+
)
|
|
82
|
+
refresh()
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const handleDuplicate = async (s: PlanSummary) => {
|
|
86
|
+
const name = await prompt({
|
|
87
|
+
title: 'Duplicate plan',
|
|
88
|
+
label: 'Name for the duplicated plan',
|
|
89
|
+
defaultValue: `Copy of ${s.name}`,
|
|
90
|
+
confirmLabel: 'Duplicate',
|
|
91
|
+
})
|
|
92
|
+
if (name === null) return
|
|
93
|
+
const r = await duplicatePlan(s.id, { name })
|
|
94
|
+
if (r.ok) {
|
|
95
|
+
setNotice(`Duplicated "${s.name}" as "${r.plan.name}".`)
|
|
96
|
+
refresh()
|
|
97
|
+
} else {
|
|
98
|
+
setNotice(`Could not duplicate "${s.name}": ${r.issues.join('; ')}`)
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const handleDelete = async (s: PlanSummary) => {
|
|
103
|
+
const ok = await confirm({
|
|
104
|
+
title: 'Delete plan',
|
|
105
|
+
body: `Delete "${s.name}"? You'll have a few seconds to undo.`,
|
|
106
|
+
confirmLabel: 'Delete plan',
|
|
107
|
+
danger: true,
|
|
108
|
+
})
|
|
109
|
+
if (!ok) return
|
|
110
|
+
const loaded = await loadPlan(s.id)
|
|
111
|
+
await deletePlan(s.id)
|
|
112
|
+
refresh()
|
|
113
|
+
if (loaded.ok) {
|
|
114
|
+
clearUndoTimer()
|
|
115
|
+
setUndoPlan(loaded.plan)
|
|
116
|
+
undoTimer.current = window.setTimeout(() => setUndoPlan(null), 5000)
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const undoDelete = async () => {
|
|
121
|
+
if (!undoPlan) return
|
|
122
|
+
clearUndoTimer()
|
|
123
|
+
const restored = undoPlan
|
|
124
|
+
// Keep the in-memory plan (and the toast) until the restore actually
|
|
125
|
+
// lands — if the save fails, the user must not lose both the plan and
|
|
126
|
+
// the affordance at once.
|
|
127
|
+
try {
|
|
128
|
+
const r = await savePlan(restored)
|
|
129
|
+
if (r.ok) {
|
|
130
|
+
setUndoPlan(null)
|
|
131
|
+
refresh()
|
|
132
|
+
} else {
|
|
133
|
+
setNotice(`Could not restore "${restored.name}": ${r.issues.join('; ')}`)
|
|
134
|
+
}
|
|
135
|
+
} catch {
|
|
136
|
+
setNotice(`Could not restore "${restored.name}" — storage is unavailable in this browser right now.`)
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const dismissUndo = () => {
|
|
141
|
+
clearUndoTimer()
|
|
142
|
+
setUndoPlan(null)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const handleClearAll = async () => {
|
|
146
|
+
const ok = await confirm({
|
|
147
|
+
title: 'Clear all data',
|
|
148
|
+
body:
|
|
149
|
+
'This erases ALL RetireGolden data from this browser — every plan, plus Social Security and ' +
|
|
150
|
+
'life-expectancy entries. It cannot be undone. Download a plan backup first if you want to keep anything.',
|
|
151
|
+
confirmLabel: 'Erase everything',
|
|
152
|
+
danger: true,
|
|
153
|
+
typedConfirmation: 'delete',
|
|
154
|
+
extraAction: { label: 'Download backup', onClick: () => void handleExportAll() },
|
|
155
|
+
})
|
|
156
|
+
if (!ok) return
|
|
157
|
+
// A pending delete-undo would let "Undo" resurrect a plan after the
|
|
158
|
+
// erasure — drop it before clearing so "erases ALL data" stays true.
|
|
159
|
+
dismissUndo()
|
|
160
|
+
await clearAllPlans()
|
|
161
|
+
try {
|
|
162
|
+
for (const key of Object.keys(localStorage)) {
|
|
163
|
+
if (key.startsWith('retiregolden.')) localStorage.removeItem(key)
|
|
164
|
+
}
|
|
165
|
+
} catch {
|
|
166
|
+
/* localStorage unavailable — IndexedDB is already cleared */
|
|
167
|
+
}
|
|
168
|
+
setNotice('All RetireGolden data has been erased from this browser.')
|
|
169
|
+
refresh()
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return {
|
|
173
|
+
plans,
|
|
174
|
+
notice,
|
|
175
|
+
setNotice,
|
|
176
|
+
undoPlan,
|
|
177
|
+
undoDelete,
|
|
178
|
+
dismissUndo,
|
|
179
|
+
fileInput,
|
|
180
|
+
refresh,
|
|
181
|
+
createAndOpen,
|
|
182
|
+
handleExportAll,
|
|
183
|
+
handleImportFile,
|
|
184
|
+
handleDuplicate,
|
|
185
|
+
handleDelete,
|
|
186
|
+
handleClearAll,
|
|
187
|
+
openPlan,
|
|
188
|
+
dialogs,
|
|
189
|
+
}
|
|
190
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { useCallback, useState } from 'react'
|
|
2
|
+
|
|
3
|
+
import { readLocal, STORAGE_KEYS, writeLocal } from '../../data/localStore'
|
|
4
|
+
import type { PlanSummary } from '../../data/planStore'
|
|
5
|
+
|
|
6
|
+
export const WELCOME_DISMISSED_KEY = STORAGE_KEYS.homeWelcomeDismissed
|
|
7
|
+
|
|
8
|
+
export type HomeMode = 'first-run' | 'returning'
|
|
9
|
+
|
|
10
|
+
function readWelcomeDismissed(): boolean {
|
|
11
|
+
if (typeof window === 'undefined') return true
|
|
12
|
+
return readLocal(WELCOME_DISMISSED_KEY) !== 'false'
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function writeWelcomeDismissed(dismissed: boolean): void {
|
|
16
|
+
if (typeof window === 'undefined') return
|
|
17
|
+
writeLocal(WELCOME_DISMISSED_KEY, dismissed ? 'true' : 'false')
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function deriveHomeMode(plans: PlanSummary[] | null): HomeMode {
|
|
21
|
+
if (plans !== null && plans.length === 0) return 'first-run'
|
|
22
|
+
return 'returning'
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function isWelcomeExpanded(mode: HomeMode, welcomeDismissed: boolean): boolean {
|
|
26
|
+
if (mode === 'first-run') return true
|
|
27
|
+
return !welcomeDismissed
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function useHomeMode(plans: PlanSummary[] | null) {
|
|
31
|
+
const [welcomeDismissed, setWelcomeDismissed] = useState(readWelcomeDismissed)
|
|
32
|
+
|
|
33
|
+
const mode = deriveHomeMode(plans)
|
|
34
|
+
const welcomeExpanded = isWelcomeExpanded(mode, welcomeDismissed)
|
|
35
|
+
|
|
36
|
+
const dismissWelcome = useCallback(() => {
|
|
37
|
+
setWelcomeDismissed(true)
|
|
38
|
+
writeWelcomeDismissed(true)
|
|
39
|
+
}, [])
|
|
40
|
+
|
|
41
|
+
const showWelcome = useCallback(() => {
|
|
42
|
+
setWelcomeDismissed(false)
|
|
43
|
+
writeWelcomeDismissed(false)
|
|
44
|
+
}, [])
|
|
45
|
+
|
|
46
|
+
return { mode, welcomeExpanded, dismissWelcome, showWelcome }
|
|
47
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/** Pure plan mutations for household edits (kept out of the component file so
|
|
2
|
+
* they're testable and don't trip react-refresh's only-export-components rule). */
|
|
3
|
+
|
|
4
|
+
import type { Plan } from '@retiregolden/engine/model/plan'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Remove a partner and re-home everything that referenced them so the plan stays
|
|
8
|
+
* valid: accounts move to the primary, the removed person's incomes and policies
|
|
9
|
+
* drop, and any permanent-life beneficiary pointing at them falls back to the
|
|
10
|
+
* estate. Pure mutator (works on an Immer draft or a plain Plan).
|
|
11
|
+
*/
|
|
12
|
+
export function removePartner(d: Plan, removedId: string) {
|
|
13
|
+
d.household.people = d.household.people.filter((p) => p.id !== removedId)
|
|
14
|
+
d.household.filingStatus = 'single'
|
|
15
|
+
const primaryId = d.household.people[0]!.id
|
|
16
|
+
d.accounts = d.accounts.map((a) => (a.ownerPersonId === removedId ? { ...a, ownerPersonId: primaryId } : a))
|
|
17
|
+
d.incomes = d.incomes.filter((s) => !('personId' in s) || s.personId !== removedId)
|
|
18
|
+
d.insurance = d.insurance
|
|
19
|
+
.filter((p) => (p.kind === 'ltc' ? p.owner : p.insured) !== removedId)
|
|
20
|
+
.map((p) => (p.kind === 'permanentLife' && p.beneficiary === removedId ? { ...p, beneficiary: 'estate' as const } : p))
|
|
21
|
+
d.careEvents = d.careEvents.filter((c) => c.personId !== removedId)
|
|
22
|
+
}
|