@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,170 @@
|
|
|
1
|
+
/** Income section: wages, pensions, annuities, other streams. */
|
|
2
|
+
|
|
3
|
+
import { Link } from 'react-router-dom'
|
|
4
|
+
|
|
5
|
+
import type { IncomeStream } from '@retiregolden/engine/model/plan'
|
|
6
|
+
import { usePlan } from '../planContextCore'
|
|
7
|
+
import { CheckboxField, MoneyField, NumberField, PercentField, ReadonlyField, SelectField, TextField } from '../fields'
|
|
8
|
+
import { LEARN } from '../learnLinks'
|
|
9
|
+
import { fmtMoney } from '../format'
|
|
10
|
+
import { resolvePia } from '../ssAnalysis'
|
|
11
|
+
import { Issues } from './shared'
|
|
12
|
+
import { newId } from './sectionHelpers'
|
|
13
|
+
|
|
14
|
+
const INCOME_LABEL: Record<IncomeStream['type'], string> = {
|
|
15
|
+
wages: 'Wages',
|
|
16
|
+
socialSecurity: 'Social Security',
|
|
17
|
+
recurring: 'Recurring',
|
|
18
|
+
oneTime: 'One-time',
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function makeIncome(type: IncomeStream['type'], personId: string): IncomeStream {
|
|
22
|
+
switch (type) {
|
|
23
|
+
case 'wages':
|
|
24
|
+
return { type, id: newId(), personId, annualGross: 0, endAge: null, realGrowthPct: 0 }
|
|
25
|
+
case 'socialSecurity':
|
|
26
|
+
return { type, id: newId(), personId, piaMonthly: 0, earnings: null, claimAge: { years: 67, months: 0 } }
|
|
27
|
+
case 'recurring':
|
|
28
|
+
return { type, id: newId(), label: 'Rental income', annualAmount: 0, startYear: null, endYear: null, inflationAdjusted: true, taxTreatment: 'ordinary' }
|
|
29
|
+
case 'oneTime':
|
|
30
|
+
return { type, id: newId(), label: 'Inheritance', year: new Date().getFullYear() + 1, amount: 0, taxTreatment: 'none' }
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function IncomeFields({ stream, index }: { stream: IncomeStream; index: number }) {
|
|
35
|
+
const { plan, update } = usePlan()
|
|
36
|
+
const set = (key: string, value: unknown) =>
|
|
37
|
+
update((d) => {
|
|
38
|
+
;(d.incomes[index] as unknown as Record<string, unknown>)[key] = value
|
|
39
|
+
})
|
|
40
|
+
const personOpts = plan.household.people.map((p) => ({ value: p.id, label: p.name }))
|
|
41
|
+
switch (stream.type) {
|
|
42
|
+
case 'wages': {
|
|
43
|
+
const wagePerson = plan.household.people.find((p) => p.id === stream.personId)
|
|
44
|
+
const retireAge = wagePerson?.retirementAge ?? null
|
|
45
|
+
return (
|
|
46
|
+
<div className="form-grid">
|
|
47
|
+
<SelectField label="Person" value={stream.personId} options={personOpts} onCommit={(v) => set('personId', v)} />
|
|
48
|
+
<MoneyField label="Annual gross" value={stream.annualGross} onCommit={(v) => set('annualGross', v ?? 0)} />
|
|
49
|
+
<PercentField
|
|
50
|
+
label="Real raise rate"
|
|
51
|
+
help="Annual wage growth rate on top of inflation (e.g. raises or promotions). A 1% rate means wages grow 1% faster than inflation each year."
|
|
52
|
+
learn={LEARN.accumulation}
|
|
53
|
+
value={stream.realGrowthPct ?? 0}
|
|
54
|
+
onCommit={(v) => set('realGrowthPct', v ?? 0)}
|
|
55
|
+
/>
|
|
56
|
+
<NumberField
|
|
57
|
+
label="Stop age"
|
|
58
|
+
help="Only set this to end wages at a different age than the retirement age from the Household form — e.g. part-time work that winds down earlier."
|
|
59
|
+
hint={retireAge !== null ? `Blank = retirement age (${retireAge}).` : 'Blank = retirement age.'}
|
|
60
|
+
value={stream.endAge}
|
|
61
|
+
allowNull
|
|
62
|
+
min={30}
|
|
63
|
+
max={80}
|
|
64
|
+
onCommit={(v) => set('endAge', v)}
|
|
65
|
+
/>
|
|
66
|
+
</div>
|
|
67
|
+
)
|
|
68
|
+
}
|
|
69
|
+
case 'socialSecurity': {
|
|
70
|
+
const ssPerson = plan.household.people.find((p) => p.id === stream.personId)
|
|
71
|
+
const resolved = ssPerson ? resolvePia(ssPerson, stream) : null
|
|
72
|
+
const pia = resolved?.piaMonthly ?? stream.piaMonthly
|
|
73
|
+
const sourceLabel = stream.piaMonthly === null ? 'earnings record' : 'quick PIA'
|
|
74
|
+
const claim = `${stream.claimAge.years}y${stream.claimAge.months ? ` ${stream.claimAge.months}m` : ''}`
|
|
75
|
+
return (
|
|
76
|
+
<>
|
|
77
|
+
<div className="form-grid">
|
|
78
|
+
<ReadonlyField label="Person" value={ssPerson?.name ?? '—'} />
|
|
79
|
+
<ReadonlyField label="PIA (monthly at FRA)" value={pia != null ? `${fmtMoney(pia)} (${sourceLabel})` : 'Not set'} />
|
|
80
|
+
<ReadonlyField label="Claim age" value={claim} />
|
|
81
|
+
</div>
|
|
82
|
+
<p className="field-hint">
|
|
83
|
+
Social Security is managed on the <Link to="../social-security">Social Security</Link> step so the
|
|
84
|
+
earnings-derived benefit stays in one place. Edit the benefit and claim age there; the{' '}
|
|
85
|
+
<Link to="../social-security-analysis">Social Security analysis</Link> can apply the best claim age.
|
|
86
|
+
</p>
|
|
87
|
+
</>
|
|
88
|
+
)
|
|
89
|
+
}
|
|
90
|
+
case 'recurring':
|
|
91
|
+
return (
|
|
92
|
+
<div className="form-grid">
|
|
93
|
+
<TextField label="Label" value={stream.label} onCommit={(v) => set('label', v || 'Income')} />
|
|
94
|
+
<MoneyField label="Annual amount" value={stream.annualAmount} onCommit={(v) => set('annualAmount', v ?? 0)} />
|
|
95
|
+
<NumberField label="Start year" value={stream.startYear} allowNull min={1900} max={2200} onCommit={(v) => set('startYear', v === null ? null : Math.round(v))} />
|
|
96
|
+
<NumberField label="End year" value={stream.endYear} allowNull min={1900} max={2200} onCommit={(v) => set('endYear', v === null ? null : Math.round(v))} />
|
|
97
|
+
<SelectField
|
|
98
|
+
label="Tax treatment"
|
|
99
|
+
value={stream.taxTreatment}
|
|
100
|
+
options={[
|
|
101
|
+
{ value: 'ordinary', label: 'Ordinary income' },
|
|
102
|
+
{ value: 'none', label: 'Not taxed' },
|
|
103
|
+
]}
|
|
104
|
+
onCommit={(v) => set('taxTreatment', v)}
|
|
105
|
+
/>
|
|
106
|
+
<CheckboxField label="Inflation-adjusted" value={stream.inflationAdjusted} onCommit={(v) => set('inflationAdjusted', v)} />
|
|
107
|
+
</div>
|
|
108
|
+
)
|
|
109
|
+
case 'oneTime':
|
|
110
|
+
return (
|
|
111
|
+
<div className="form-grid">
|
|
112
|
+
<TextField label="Label" value={stream.label} onCommit={(v) => set('label', v || 'Event')} />
|
|
113
|
+
<NumberField label="Year" value={stream.year} min={1900} max={2200} onCommit={(v) => set('year', Math.round(v ?? new Date().getFullYear()))} />
|
|
114
|
+
<MoneyField label="Amount" value={stream.amount} onCommit={(v) => set('amount', v ?? 0)} />
|
|
115
|
+
<SelectField
|
|
116
|
+
label="Tax treatment"
|
|
117
|
+
value={stream.taxTreatment}
|
|
118
|
+
options={[
|
|
119
|
+
{ value: 'none', label: 'Not taxed' },
|
|
120
|
+
{ value: 'ordinary', label: 'Ordinary income' },
|
|
121
|
+
{ value: 'capitalGain', label: 'Capital gain' },
|
|
122
|
+
]}
|
|
123
|
+
onCommit={(v) => set('taxTreatment', v)}
|
|
124
|
+
/>
|
|
125
|
+
</div>
|
|
126
|
+
)
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function IncomeSection() {
|
|
131
|
+
const { plan, update } = usePlan()
|
|
132
|
+
const firstPerson = plan.household.people[0]!.id
|
|
133
|
+
return (
|
|
134
|
+
<section>
|
|
135
|
+
<div className="card">
|
|
136
|
+
<h2>Income</h2>
|
|
137
|
+
<p className="card-hint">Wages run until each person's retirement age and drive contributions and the SS earnings test. Social Security uses monthly claiming factors.</p>
|
|
138
|
+
{plan.incomes.map((s, i) => (
|
|
139
|
+
<div className="item-row" key={s.id}>
|
|
140
|
+
<div className="item-row-head">
|
|
141
|
+
<span className="item-row-title">
|
|
142
|
+
<span className="type-chip">{INCOME_LABEL[s.type]}</span>
|
|
143
|
+
{'label' in s ? s.label : (plan.household.people.find((p) => 'personId' in s && p.id === s.personId)?.name ?? '')}
|
|
144
|
+
</span>
|
|
145
|
+
<button type="button" className="btn-ghost btn-ghost-danger" onClick={() => update((d) => void d.incomes.splice(i, 1))}>
|
|
146
|
+
Remove
|
|
147
|
+
</button>
|
|
148
|
+
</div>
|
|
149
|
+
<IncomeFields stream={s} index={i} />
|
|
150
|
+
</div>
|
|
151
|
+
))}
|
|
152
|
+
<div className="add-row">
|
|
153
|
+
{(Object.keys(INCOME_LABEL) as IncomeStream['type'][])
|
|
154
|
+
.filter((t) => t !== 'socialSecurity')
|
|
155
|
+
.map((t) => (
|
|
156
|
+
<button key={t} type="button" className="btn btn-secondary btn-small" onClick={() => update((d) => void d.incomes.push(makeIncome(t, firstPerson)))}>
|
|
157
|
+
+ {INCOME_LABEL[t]}
|
|
158
|
+
</button>
|
|
159
|
+
))}
|
|
160
|
+
</div>
|
|
161
|
+
<Issues />
|
|
162
|
+
</div>
|
|
163
|
+
</section>
|
|
164
|
+
)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// ---------------------------------------------------------------------------
|
|
168
|
+
// Spending
|
|
169
|
+
// ---------------------------------------------------------------------------
|
|
170
|
+
|
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
/** Insurance section: policies, care events, LTC stress. */
|
|
2
|
+
|
|
3
|
+
import { useMemo } from 'react'
|
|
4
|
+
|
|
5
|
+
import type { CareEvent, InsurancePolicy, Plan } from '@retiregolden/engine/model/plan'
|
|
6
|
+
import { compareLtcStress } from '@retiregolden/engine/projection/compare'
|
|
7
|
+
import { usePlan } from '../planContextCore'
|
|
8
|
+
import { CheckboxField, MoneyField, NumberField, PercentField, SelectField, TextField } from '../fields'
|
|
9
|
+
import { LearnAboutScreen } from '../../learn/LearnAboutScreen'
|
|
10
|
+
import { LearnLink } from '../../learn/LearnLink'
|
|
11
|
+
import { LEARN } from '../learnLinks'
|
|
12
|
+
import { fmtMoneyCompact } from '../format'
|
|
13
|
+
import { currentStartYear, taxCalculatorFor } from '../useProjection'
|
|
14
|
+
import { Issues } from './shared'
|
|
15
|
+
import { newId } from './sectionHelpers'
|
|
16
|
+
|
|
17
|
+
const INSURANCE_LABEL: Record<InsurancePolicy['kind'], string> = {
|
|
18
|
+
permanentLife: 'Permanent life',
|
|
19
|
+
ltc: 'Long-term care',
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function personOptions(plan: Plan) {
|
|
23
|
+
return plan.household.people.map((p) => ({ value: p.id, label: p.name }))
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function makeInsurance(kind: InsurancePolicy['kind'], personId: string): InsurancePolicy {
|
|
27
|
+
if (kind === 'ltc') {
|
|
28
|
+
return { kind, id: newId(), name: 'LTC policy', owner: personId, annualPremium: 0, premiumMode: 'lifetime', benefitMonthly: 0, benefitPeriodYears: 3, eliminationPeriodDays: 90 }
|
|
29
|
+
}
|
|
30
|
+
return { kind, id: newId(), name: 'Whole life', insured: personId, beneficiary: 'estate', annualPremium: 0, premiumMode: 'lifetime', deathBenefit: 0, cashValue: 0, cashValueMode: 'flatRate', cashValueGrowthPct: 4 }
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const PREMIUM_MODE_OPTIONS = [
|
|
34
|
+
{ value: 'lifetime', label: 'Pay for life' },
|
|
35
|
+
{ value: 'paidUp', label: 'Paid up (none)' },
|
|
36
|
+
{ value: 'untilAge', label: 'Until an age' },
|
|
37
|
+
] as const
|
|
38
|
+
|
|
39
|
+
function InsuranceFields({ policy, index }: { policy: InsurancePolicy; index: number }) {
|
|
40
|
+
const { plan, update } = usePlan()
|
|
41
|
+
const set = <K extends string>(key: K, value: unknown) =>
|
|
42
|
+
update((d) => {
|
|
43
|
+
;(d.insurance[index] as unknown as Record<string, unknown>)[key] = value
|
|
44
|
+
})
|
|
45
|
+
const policyLearn = policy.kind === 'ltc' ? LEARN.ltcInsurance : LEARN.permanentLife
|
|
46
|
+
return (
|
|
47
|
+
<div className="form-grid">
|
|
48
|
+
<TextField label="Name" value={policy.name} onCommit={(v) => set('name', v || INSURANCE_LABEL[policy.kind])} />
|
|
49
|
+
<SelectField
|
|
50
|
+
label={policy.kind === 'ltc' ? 'Owner' : 'Insured'}
|
|
51
|
+
value={policy.kind === 'ltc' ? policy.owner : policy.insured}
|
|
52
|
+
options={personOptions(plan)}
|
|
53
|
+
onCommit={(v) => set(policy.kind === 'ltc' ? 'owner' : 'insured', v)}
|
|
54
|
+
/>
|
|
55
|
+
|
|
56
|
+
<MoneyField
|
|
57
|
+
label="Annual premium"
|
|
58
|
+
help="The yearly cost of keeping this policy in force. Premiums reduce spending capacity even in years when no claim or death benefit is paid."
|
|
59
|
+
learn={policyLearn}
|
|
60
|
+
value={policy.annualPremium}
|
|
61
|
+
onCommit={(v) => set('annualPremium', v ?? 0)}
|
|
62
|
+
/>
|
|
63
|
+
<SelectField
|
|
64
|
+
label="Premium"
|
|
65
|
+
help="Permanent-life and most LTC premiums are level (fixed nominal), so they aren't inflation-adjusted."
|
|
66
|
+
learn={policyLearn}
|
|
67
|
+
value={policy.premiumMode}
|
|
68
|
+
options={PREMIUM_MODE_OPTIONS}
|
|
69
|
+
onCommit={(v) => set('premiumMode', v)}
|
|
70
|
+
/>
|
|
71
|
+
{policy.premiumMode === 'untilAge' ? (
|
|
72
|
+
<NumberField
|
|
73
|
+
label="Premiums end at age"
|
|
74
|
+
help="The age when scheduled premiums stop. Leave the mode as pay for life if premiums continue indefinitely."
|
|
75
|
+
learn={policyLearn}
|
|
76
|
+
value={policy.premiumEndAge ?? 65}
|
|
77
|
+
min={40}
|
|
78
|
+
max={110}
|
|
79
|
+
onCommit={(v) => set('premiumEndAge', Math.round(v ?? 65))}
|
|
80
|
+
/>
|
|
81
|
+
) : null}
|
|
82
|
+
|
|
83
|
+
{policy.kind === 'permanentLife' ? (
|
|
84
|
+
<>
|
|
85
|
+
<SelectField
|
|
86
|
+
label="Beneficiary"
|
|
87
|
+
value={policy.beneficiary === 'estate' ? 'estate' : policy.beneficiary}
|
|
88
|
+
options={[{ value: 'estate', label: 'Estate' }, ...personOptions(plan)]}
|
|
89
|
+
onCommit={(v) => set('beneficiary', v)}
|
|
90
|
+
/>
|
|
91
|
+
<MoneyField
|
|
92
|
+
label="Death benefit"
|
|
93
|
+
help="The modeled face amount paid when the insured person dies. RetireGolden treats it as income-tax-free at death, while avoiding double-counting cash value and death benefit."
|
|
94
|
+
learn={LEARN.permanentLife}
|
|
95
|
+
hint="Face amount, paid income-tax-free at death."
|
|
96
|
+
value={policy.deathBenefit}
|
|
97
|
+
onCommit={(v) => set('deathBenefit', v ?? 0)}
|
|
98
|
+
/>
|
|
99
|
+
<MoneyField
|
|
100
|
+
label="Cash value (today)"
|
|
101
|
+
help="Current policy cash value while the insured is alive. Use the policy statement value, not the death benefit."
|
|
102
|
+
learn={LEARN.permanentLife}
|
|
103
|
+
value={policy.cashValue}
|
|
104
|
+
onCommit={(v) => set('cashValue', v ?? 0)}
|
|
105
|
+
/>
|
|
106
|
+
<SelectField
|
|
107
|
+
label="Cash value grows by"
|
|
108
|
+
help="Flat rate is a rough estimate. A pasted illustration schedule is accurate — whole-life cash value is front-loaded-poor, back-loaded-rich, not linear."
|
|
109
|
+
learn={LEARN.permanentLife}
|
|
110
|
+
value={policy.cashValueMode}
|
|
111
|
+
options={[
|
|
112
|
+
{ value: 'flatRate', label: 'Flat rate' },
|
|
113
|
+
{ value: 'schedule', label: 'Illustration schedule' },
|
|
114
|
+
]}
|
|
115
|
+
onCommit={(v) => set('cashValueMode', v)}
|
|
116
|
+
/>
|
|
117
|
+
{policy.cashValueMode === 'flatRate' ? (
|
|
118
|
+
<PercentField
|
|
119
|
+
label="Cash value growth"
|
|
120
|
+
help="A simple annual growth assumption for cash value. If you have an illustration, use the schedule instead because real policy values rarely grow in a straight line."
|
|
121
|
+
learn={LEARN.permanentLife}
|
|
122
|
+
value={policy.cashValueGrowthPct ?? 0}
|
|
123
|
+
onCommit={(v) => set('cashValueGrowthPct', v ?? 0)}
|
|
124
|
+
/>
|
|
125
|
+
) : (
|
|
126
|
+
<div className="field field-span-full">
|
|
127
|
+
<span className="field-label">Cash-value schedule (age → value)</span>
|
|
128
|
+
{(policy.cashValueSchedule ?? []).map((row, ri) => (
|
|
129
|
+
<div className="add-row" key={ri} style={{ alignItems: 'flex-end' }}>
|
|
130
|
+
<NumberField label="Age" value={row.age} min={0} max={120} onCommit={(v) => update((d) => { const p = d.insurance[index]; if (p.kind === 'permanentLife' && p.cashValueSchedule) p.cashValueSchedule[ri]!.age = Math.round(v ?? row.age) })} />
|
|
131
|
+
<MoneyField label="Value" value={row.value} onCommit={(v) => update((d) => { const p = d.insurance[index]; if (p.kind === 'permanentLife' && p.cashValueSchedule) p.cashValueSchedule[ri]!.value = v ?? 0 })} />
|
|
132
|
+
<button type="button" className="btn-ghost btn-ghost-danger" onClick={() => update((d) => { const p = d.insurance[index]; if (p.kind === 'permanentLife' && p.cashValueSchedule) p.cashValueSchedule.splice(ri, 1) })}>Remove</button>
|
|
133
|
+
</div>
|
|
134
|
+
))}
|
|
135
|
+
<button type="button" className="btn btn-secondary btn-small" onClick={() => update((d) => { const p = d.insurance[index]; if (p.kind === 'permanentLife') p.cashValueSchedule = [...(p.cashValueSchedule ?? []), { age: 65, value: 0 }] })}>+ Schedule row</button>
|
|
136
|
+
</div>
|
|
137
|
+
)}
|
|
138
|
+
</>
|
|
139
|
+
) : (
|
|
140
|
+
<>
|
|
141
|
+
<MoneyField
|
|
142
|
+
label="Monthly benefit"
|
|
143
|
+
help="The maximum monthly amount the LTC policy can pay toward a modeled care event, before benefit-period limits and the inflation rider."
|
|
144
|
+
learn={LEARN.ltcInsurance}
|
|
145
|
+
hint="Maximum the policy pays toward care per month."
|
|
146
|
+
value={policy.benefitMonthly}
|
|
147
|
+
onCommit={(v) => set('benefitMonthly', v ?? 0)}
|
|
148
|
+
/>
|
|
149
|
+
<CheckboxField
|
|
150
|
+
label="Lifetime benefit period"
|
|
151
|
+
help="When checked, benefits do not run out by years in the model. Most real policies have a finite benefit pool, so use this only if the contract says lifetime."
|
|
152
|
+
learn={LEARN.ltcInsurance}
|
|
153
|
+
value={policy.benefitPeriodYears === 'lifetime'}
|
|
154
|
+
onCommit={(v) => set('benefitPeriodYears', v ? 'lifetime' : 3)}
|
|
155
|
+
/>
|
|
156
|
+
{policy.benefitPeriodYears !== 'lifetime' ? (
|
|
157
|
+
<NumberField
|
|
158
|
+
label="Benefit period (years)"
|
|
159
|
+
help="How many years benefits can continue. A care episode that lasts longer than this is partly self-funded."
|
|
160
|
+
learn={LEARN.ltcInsurance}
|
|
161
|
+
value={policy.benefitPeriodYears}
|
|
162
|
+
min={1}
|
|
163
|
+
max={20}
|
|
164
|
+
onCommit={(v) => set('benefitPeriodYears', Math.round(v ?? 3))}
|
|
165
|
+
/>
|
|
166
|
+
) : null}
|
|
167
|
+
<NumberField
|
|
168
|
+
label="Elimination period (days)"
|
|
169
|
+
help="The waiting period before benefits begin. Care costs during this period are self-funded."
|
|
170
|
+
learn={LEARN.ltcInsurance}
|
|
171
|
+
hint="Waiting period before benefits begin."
|
|
172
|
+
value={policy.eliminationPeriodDays}
|
|
173
|
+
min={0}
|
|
174
|
+
max={365}
|
|
175
|
+
onCommit={(v) => set('eliminationPeriodDays', Math.round(v ?? 90))}
|
|
176
|
+
/>
|
|
177
|
+
<PercentField
|
|
178
|
+
label="Inflation rider"
|
|
179
|
+
help="Annual compound growth applied to the monthly benefit cap. This protects an old policy from covering less care as costs rise."
|
|
180
|
+
learn={LEARN.ltcInsurance}
|
|
181
|
+
hint="Compound growth of the benefit cap."
|
|
182
|
+
value={policy.inflationRiderPct ?? 0}
|
|
183
|
+
onCommit={(v) => set('inflationRiderPct', v ?? 0)}
|
|
184
|
+
/>
|
|
185
|
+
</>
|
|
186
|
+
)}
|
|
187
|
+
</div>
|
|
188
|
+
)
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function makeCareEvent(personId: string): CareEvent {
|
|
192
|
+
return { id: newId(), personId, startAge: 85, durationYears: 3, annualCost: 90_000 }
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function CareEventFields({ event, index }: { event: CareEvent; index: number }) {
|
|
196
|
+
const { plan, update } = usePlan()
|
|
197
|
+
const set = (key: keyof CareEvent, value: unknown) =>
|
|
198
|
+
update((d) => {
|
|
199
|
+
;(d.careEvents[index] as unknown as Record<string, unknown>)[key] = value
|
|
200
|
+
})
|
|
201
|
+
return (
|
|
202
|
+
<div className="form-grid">
|
|
203
|
+
<SelectField
|
|
204
|
+
label="Who needs care"
|
|
205
|
+
help="The person whose late-life care costs are being stress-tested. Only an LTC policy on this same person offsets the event."
|
|
206
|
+
learn={LEARN.ltcCosts}
|
|
207
|
+
value={event.personId}
|
|
208
|
+
options={personOptions(plan)}
|
|
209
|
+
onCommit={(v) => set('personId', v)}
|
|
210
|
+
/>
|
|
211
|
+
<NumberField
|
|
212
|
+
label="Starts at age"
|
|
213
|
+
help="The age when the care event begins for the selected person."
|
|
214
|
+
learn={LEARN.ltcCosts}
|
|
215
|
+
value={event.startAge}
|
|
216
|
+
min={40}
|
|
217
|
+
max={110}
|
|
218
|
+
onCommit={(v) => set('startAge', Math.round(v ?? 85))}
|
|
219
|
+
/>
|
|
220
|
+
<NumberField
|
|
221
|
+
label="Duration (years)"
|
|
222
|
+
help="How long the care event lasts. Longer episodes can outlast a policy benefit period and shift more cost back to the household."
|
|
223
|
+
learn={LEARN.ltcCosts}
|
|
224
|
+
value={event.durationYears}
|
|
225
|
+
min={1}
|
|
226
|
+
max={25}
|
|
227
|
+
onCommit={(v) => set('durationYears', Math.round(v ?? 3))}
|
|
228
|
+
/>
|
|
229
|
+
<MoneyField
|
|
230
|
+
label="Annual cost (today's $)"
|
|
231
|
+
help="The annual care cost in today's dollars before any LTC policy benefit. This is added on top of baseline spending during the event."
|
|
232
|
+
learn={LEARN.ltcCosts}
|
|
233
|
+
hint="Additive to baseline spending; an LTC policy on this person offsets it."
|
|
234
|
+
value={event.annualCost}
|
|
235
|
+
onCommit={(v) => set('annualCost', v ?? 0)}
|
|
236
|
+
/>
|
|
237
|
+
</div>
|
|
238
|
+
)
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const lastsLabel = (depletionYear: number | null) => (depletionYear === null ? 'full plan' : `until ${depletionYear}`)
|
|
242
|
+
|
|
243
|
+
function LtcStressPanel() {
|
|
244
|
+
const { plan } = usePlan()
|
|
245
|
+
const cmp = useMemo(
|
|
246
|
+
() => compareLtcStress(plan, { startYear: currentStartYear(), taxCalculator: taxCalculatorFor(plan) }),
|
|
247
|
+
[plan],
|
|
248
|
+
)
|
|
249
|
+
if (!cmp.hasCareEvents) return null
|
|
250
|
+
const policyValue = cmp.careInsured.endingNetWorth - cmp.careUninsured.endingNetWorth
|
|
251
|
+
const careShock = cmp.noCare.endingNetWorth - cmp.careUninsured.endingNetWorth
|
|
252
|
+
return (
|
|
253
|
+
<div className="card" style={{ marginTop: '1.25rem' }}>
|
|
254
|
+
<h3>LTC stress test</h3>
|
|
255
|
+
<p className="card-hint">Ending net worth if the care episode happens, with and without your LTC coverage offsetting it.</p>
|
|
256
|
+
<table className="compare-table">
|
|
257
|
+
<thead>
|
|
258
|
+
<tr>
|
|
259
|
+
<th scope="col">Scenario</th>
|
|
260
|
+
<th scope="col">Ending net worth</th>
|
|
261
|
+
<th scope="col">Money lasts</th>
|
|
262
|
+
</tr>
|
|
263
|
+
</thead>
|
|
264
|
+
<tbody>
|
|
265
|
+
<tr>
|
|
266
|
+
<th scope="row">No care needed</th>
|
|
267
|
+
<td>{fmtMoneyCompact(cmp.noCare.endingNetWorth)}</td>
|
|
268
|
+
<td>{lastsLabel(cmp.noCare.depletionYear)}</td>
|
|
269
|
+
</tr>
|
|
270
|
+
<tr>
|
|
271
|
+
<th scope="row">Care, self-funded</th>
|
|
272
|
+
<td>{fmtMoneyCompact(cmp.careUninsured.endingNetWorth)}</td>
|
|
273
|
+
<td>{lastsLabel(cmp.careUninsured.depletionYear)}</td>
|
|
274
|
+
</tr>
|
|
275
|
+
<tr>
|
|
276
|
+
<th scope="row">Care, insured</th>
|
|
277
|
+
<td>{fmtMoneyCompact(cmp.careInsured.endingNetWorth)}</td>
|
|
278
|
+
<td>{lastsLabel(cmp.careInsured.depletionYear)}</td>
|
|
279
|
+
</tr>
|
|
280
|
+
</tbody>
|
|
281
|
+
</table>
|
|
282
|
+
<p className="card-hint" style={{ marginTop: '0.75rem' }}>
|
|
283
|
+
Unprotected, the care episode would cost your estate <strong>{fmtMoneyCompact(careShock)}</strong>.{' '}
|
|
284
|
+
{!cmp.hasLtcPolicy ? (
|
|
285
|
+
<>You have no LTC policy to offset it.</>
|
|
286
|
+
) : policyValue >= 0 ? (
|
|
287
|
+
<>Your LTC policy improves the outcome by <strong>{fmtMoneyCompact(policyValue)}</strong>, net of its premiums.</>
|
|
288
|
+
) : (
|
|
289
|
+
<>Here the policy's lifetime premiums outweigh its benefits by <strong>{fmtMoneyCompact(-policyValue)}</strong>.</>
|
|
290
|
+
)}
|
|
291
|
+
</p>
|
|
292
|
+
</div>
|
|
293
|
+
)
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
export function InsuranceSection() {
|
|
297
|
+
const { plan, update } = usePlan()
|
|
298
|
+
const firstPerson = plan.household.people[0]!.id
|
|
299
|
+
return (
|
|
300
|
+
<section>
|
|
301
|
+
<div className="card">
|
|
302
|
+
<h2>Insurance</h2>
|
|
303
|
+
<p className="card-hint">
|
|
304
|
+
Long-term-care and permanent-life policies. Premiums feed your spending; permanent-life cash value is an
|
|
305
|
+
asset in net worth, and the death benefit pays income-tax-free at the insured's death. Add care events to
|
|
306
|
+
test whether an LTC policy offsets a late-life care shock. <LearnLink {...LEARN.insuranceOverview} />
|
|
307
|
+
</p>
|
|
308
|
+
{plan.insurance.length === 0 ? <div className="empty-state"><p>No policies yet — add one below.</p></div> : null}
|
|
309
|
+
{plan.insurance.map((p, i) => (
|
|
310
|
+
<div className="item-row" key={p.id}>
|
|
311
|
+
<div className="item-row-head">
|
|
312
|
+
<span className="item-row-title">
|
|
313
|
+
<span className="type-chip">{INSURANCE_LABEL[p.kind]}</span>
|
|
314
|
+
{p.name}
|
|
315
|
+
</span>
|
|
316
|
+
<button type="button" className="btn-ghost btn-ghost-danger" onClick={() => update((d) => void d.insurance.splice(i, 1))}>
|
|
317
|
+
Remove
|
|
318
|
+
</button>
|
|
319
|
+
</div>
|
|
320
|
+
<InsuranceFields policy={p} index={i} />
|
|
321
|
+
</div>
|
|
322
|
+
))}
|
|
323
|
+
<div className="add-row">
|
|
324
|
+
{(Object.keys(INSURANCE_LABEL) as InsurancePolicy['kind'][]).map((k) => (
|
|
325
|
+
<button key={k} type="button" className="btn btn-secondary btn-small" onClick={() => update((d) => void d.insurance.push(makeInsurance(k, firstPerson)))}>
|
|
326
|
+
+ {INSURANCE_LABEL[k]}
|
|
327
|
+
</button>
|
|
328
|
+
))}
|
|
329
|
+
</div>
|
|
330
|
+
|
|
331
|
+
<h3>Care events</h3>
|
|
332
|
+
<p className="card-hint">
|
|
333
|
+
A deterministic late-life care episode — a spending spike an LTC policy on the same person can absorb.
|
|
334
|
+
Add one to see the stress test below. <LearnLink {...LEARN.ltcCosts} />
|
|
335
|
+
</p>
|
|
336
|
+
{plan.careEvents.map((c, i) => (
|
|
337
|
+
<div className="item-row" key={c.id}>
|
|
338
|
+
<div className="item-row-head">
|
|
339
|
+
<span className="item-row-title">
|
|
340
|
+
<span className="type-chip">Care</span>
|
|
341
|
+
{plan.household.people.find((p) => p.id === c.personId)?.name ?? 'Care'} · age {c.startAge}
|
|
342
|
+
</span>
|
|
343
|
+
<button type="button" className="btn-ghost btn-ghost-danger" onClick={() => update((d) => void d.careEvents.splice(i, 1))}>
|
|
344
|
+
Remove
|
|
345
|
+
</button>
|
|
346
|
+
</div>
|
|
347
|
+
<CareEventFields event={c} index={i} />
|
|
348
|
+
</div>
|
|
349
|
+
))}
|
|
350
|
+
<div className="add-row">
|
|
351
|
+
<button type="button" className="btn btn-secondary btn-small" onClick={() => update((d) => void d.careEvents.push(makeCareEvent(firstPerson)))}>
|
|
352
|
+
+ Care event
|
|
353
|
+
</button>
|
|
354
|
+
</div>
|
|
355
|
+
|
|
356
|
+
<LtcStressPanel />
|
|
357
|
+
<Issues />
|
|
358
|
+
</div>
|
|
359
|
+
<LearnAboutScreen route="/plan/:planId/insurance" />
|
|
360
|
+
</section>
|
|
361
|
+
)
|
|
362
|
+
}
|