345 lines
12 KiB
JavaScript
345 lines
12 KiB
JavaScript
// FinancialOnboarding.js
|
|
import React, { useState } from 'react';
|
|
import Modal from '../ui/modal.js';
|
|
import ExpensesWizard from '../../components/ExpensesWizard.js'; // path to your wizard
|
|
import { Button } from '../../components/ui/button.js'; // using your Tailwind-based button
|
|
import { saveDraft, clearDraft, loadDraft } from '../../utils/onboardingDraftApi.js';
|
|
|
|
const FinancialOnboarding = ({ nextStep, prevStep, data, setData }) => {
|
|
const {
|
|
currently_working = '',
|
|
current_salary = 0,
|
|
additional_income = 0,
|
|
monthly_expenses = 0,
|
|
monthly_debt_payments = 0,
|
|
retirement_savings = 0,
|
|
retirement_contribution = 0,
|
|
emergency_fund = 0,
|
|
emergency_contribution = 0,
|
|
extra_cash_emergency_pct = 50,
|
|
extra_cash_retirement_pct = 50,
|
|
} = data;
|
|
|
|
const [showExpensesWizard, setShowExpensesWizard] = useState(false);
|
|
|
|
const handleNeedHelpExpenses = () => {
|
|
setShowExpensesWizard(true);
|
|
};
|
|
|
|
const handleExpensesCalculated = (total) => {
|
|
setData(prev => ({...prev, monthly_expenses: total }));
|
|
saveDraft({ financialData: { monthly_expenses: total } }).catch(() => {});
|
|
};
|
|
|
|
const infoIcon = (msg) => (
|
|
<span
|
|
className="ml-1 inline-flex h-4 w-4 items-center justify-center rounded-full bg-blue-500 text-white text-xs cursor-help"
|
|
title={msg}
|
|
>
|
|
i
|
|
</span>
|
|
);
|
|
|
|
|
|
const handleChange = (e) => {
|
|
const { name, value, type, checked } = e.target;
|
|
let val = parseFloat(value) || 0;
|
|
if (type === 'checkbox') {
|
|
val = checked;
|
|
}
|
|
if (name === 'extra_cash_emergency_pct') {
|
|
val = Math.min(Math.max(val, 0), 100);
|
|
setData(prevData => ({
|
|
...prevData,
|
|
extra_cash_emergency_pct: val,
|
|
extra_cash_retirement_pct: 100 - val
|
|
}));
|
|
saveDraft({
|
|
financialData: {
|
|
extra_cash_emergency_pct: val,
|
|
extra_cash_retirement_pct: 100 - val
|
|
}
|
|
}).catch(() => {});
|
|
} else if (name === 'extra_cash_retirement_pct') {
|
|
val = Math.min(Math.max(val, 0), 100);
|
|
setData(prevData => ({
|
|
...prevData,
|
|
extra_cash_retirement_pct: val,
|
|
extra_cash_emergency_pct: 100 - val
|
|
}));
|
|
saveDraft({
|
|
financialData: {
|
|
extra_cash_retirement_pct: val,
|
|
extra_cash_emergency_pct: 100 - val
|
|
}
|
|
}).catch(() => {});
|
|
} else {
|
|
setData(prev => ({ ...prev, [name]: val }));
|
|
saveDraft({
|
|
financialData: {
|
|
[name]: val,
|
|
extra_cash_emergency_pct: Number.isFinite(extra_cash_emergency_pct) ? extra_cash_emergency_pct : 50,
|
|
extra_cash_retirement_pct: Number.isFinite(extra_cash_retirement_pct) ? extra_cash_retirement_pct : 50
|
|
}
|
|
}).catch(()=>{});
|
|
setData(prev => ({ ...prev, [name]: val }));
|
|
// Persist with 50/50 fallback if split is invalid or both 0
|
|
let ePct = Number(extra_cash_emergency_pct);
|
|
let rPct = Number(extra_cash_retirement_pct);
|
|
if (!Number.isFinite(ePct)) ePct = 0;
|
|
if (!Number.isFinite(rPct)) rPct = 0;
|
|
if ((ePct + rPct) === 0) { ePct = 50; rPct = 50; }
|
|
saveDraft({ financialData: {
|
|
[name]: val,
|
|
extra_cash_emergency_pct: ePct,
|
|
extra_cash_retirement_pct: rPct
|
|
}}).catch(()=>{});
|
|
}
|
|
};
|
|
|
|
const handleSubmit = () => {
|
|
// Final guard: coerce to numbers, clamp, and 50/50 if both resolve to 0
|
|
let ePct = Number(extra_cash_emergency_pct);
|
|
let rPct = Number(extra_cash_retirement_pct);
|
|
if (!Number.isFinite(ePct)) ePct = 0;
|
|
if (!Number.isFinite(rPct)) rPct = 0;
|
|
ePct = Math.min(Math.max(ePct, 0), 100);
|
|
rPct = Math.min(Math.max(rPct, 0), 100);
|
|
if ((ePct + rPct) === 0) { ePct = 50; rPct = 50; }
|
|
saveDraft({ financialData: {
|
|
extra_cash_emergency_pct: ePct,
|
|
extra_cash_retirement_pct: rPct
|
|
}}).catch(()=>{});
|
|
nextStep();
|
|
};
|
|
|
|
return (
|
|
<div className="max-w-md mx-auto p-6 space-y-6">
|
|
<h2 className="text-2xl font-semibold">Financial Details</h2>
|
|
|
|
{currently_working === 'yes' && (
|
|
<div className="space-y-4">
|
|
<div>
|
|
<label className="block font-medium">Current Annual Salary
|
|
{infoIcon("Gross annual salary before taxes")}
|
|
</label>
|
|
<input
|
|
name="current_salary"
|
|
type="number"
|
|
placeholder="e.g. 110000"
|
|
value={current_salary || ''}
|
|
onChange={handleChange}
|
|
onBlur={(e) => {
|
|
const v = parseFloat(e.target.value) || 0;
|
|
saveDraft({ financialData: { current_salary: v } }).catch(() => {});
|
|
}}
|
|
className="w-full border rounded p-2"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block font-medium">Additional Annual Income (optional)
|
|
{infoIcon("Yearly bonuses, investment returns, side jobs, etc.")}
|
|
</label>
|
|
<input
|
|
name="additional_income"
|
|
type="number"
|
|
placeholder="e.g. 2400"
|
|
value={additional_income || ''}
|
|
onChange={handleChange}
|
|
onBlur={(e) => {
|
|
const v = parseFloat(e.target.value) || 0;
|
|
saveDraft({ financialData: { additional_income: v } }).catch(() => {});
|
|
}}
|
|
className="w-full border rounded p-2"
|
|
/>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
<div className="space-y-4">
|
|
<label className="block font-medium"> Monthly Expenses
|
|
{infoIcon("The total amount you spend on rent, utilities, groceries, etc.")}
|
|
</label>
|
|
<div className="flex space-x-2 items-center">
|
|
<input
|
|
type="number"
|
|
className="w-full border rounded p-2"
|
|
name="monthly_expenses"
|
|
value={monthly_expenses}
|
|
onChange={handleChange}
|
|
onBlur={(e) => {
|
|
const v = parseFloat(e.target.value) || 0;
|
|
saveDraft({ financialData: { monthly_expenses: v } }).catch(() => {});
|
|
}}
|
|
placeholder="e.g. 1500"
|
|
/>
|
|
<Button
|
|
className="bg-blue-600 text-center px-3 py-2 rounded"
|
|
onClick={handleNeedHelpExpenses}
|
|
>
|
|
Need Help?
|
|
</Button>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block font-medium">Monthly Debt Payments (optional)
|
|
{infoIcon("If you keep installment loans on cars, credit cards, etc. separate from other living expenses")}
|
|
</label>
|
|
<input
|
|
name="monthly_debt_payments"
|
|
type="number"
|
|
placeholder="e.g. 500"
|
|
value={monthly_debt_payments || ''}
|
|
onChange={handleChange}
|
|
onBlur={(e) => {
|
|
const v = parseFloat(e.target.value) || 0;
|
|
saveDraft({ financialData: { monthly_debt_payments: v } }).catch(() => {});
|
|
}}
|
|
className="w-full border rounded p-2"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block font-medium">Retirement Savings (optional)
|
|
{infoIcon("Current Retirement Balance")}
|
|
</label>
|
|
<input
|
|
name="retirement_savings"
|
|
type="number"
|
|
placeholder="e.g. 50000"
|
|
value={retirement_savings || ''}
|
|
onChange={handleChange}
|
|
onBlur={(e) => {
|
|
const v = parseFloat(e.target.value) || 0;
|
|
saveDraft({ financialData: { retirement_savings: v } }).catch(() => {});
|
|
}}
|
|
className="w-full border rounded p-2"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block font-medium">Monthly Retirement Contribution (optional)
|
|
{infoIcon("Dollar value (not percentage) of monthly retirement contribution")}
|
|
</label>
|
|
<input
|
|
name="retirement_contribution"
|
|
type="number"
|
|
placeholder="e.g. 300"
|
|
value={retirement_contribution || ''}
|
|
onChange={handleChange}
|
|
onBlur={(e) => {
|
|
const v = parseFloat(e.target.value) || 0;
|
|
saveDraft({ financialData: { retirement_contribution: v } }).catch(() => {});
|
|
}}
|
|
className="w-full border rounded p-2"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block font-medium">Emergency Fund Savings (optional)
|
|
{infoIcon("Balance of your emergency fund for job loss, medical emergencies, etc.")}
|
|
</label>
|
|
<input
|
|
name="emergency_fund"
|
|
type="number"
|
|
placeholder="e.g. 10000"
|
|
value={emergency_fund || ''}
|
|
onChange={handleChange}
|
|
onBlur={(e) => {
|
|
const v = parseFloat(e.target.value) || 0;
|
|
saveDraft({ financialData: { emergency_fund: v } }).catch(() => {});
|
|
}}
|
|
className="w-full border rounded p-2"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block font-medium">Monthly Emergency Savings Contribution (optional)
|
|
{infoIcon("Dollar value (not percentage) of monthly emergency savings contribution")}
|
|
</label>
|
|
<input
|
|
name="emergency_contribution"
|
|
type="number"
|
|
placeholder="e.g. 300"
|
|
value={emergency_contribution || ''}
|
|
onChange={handleChange}
|
|
onBlur={(e) => {
|
|
const v = parseFloat(e.target.value) || 0;
|
|
saveDraft({ financialData: { emergency_contribution: v } }).catch(() => {});
|
|
}}
|
|
className="w-full border rounded p-2"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<h3 className="text-lg font-medium">Extra Monthly Cash Allocation</h3>
|
|
<p className="text-gray-600">
|
|
If you have extra money left each month after expenses, how would you like to allocate it?
|
|
(Must add to 100%)
|
|
</p>
|
|
|
|
<div>
|
|
<label className="block font-medium">Extra Monthly Cash to Emergency Fund (%)</label>
|
|
<input
|
|
name="extra_cash_emergency_pct"
|
|
type="number"
|
|
placeholder="% to Emergency Savings (e.g., 30)"
|
|
value={extra_cash_emergency_pct}
|
|
onChange={handleChange}
|
|
onBlur={(e) => {
|
|
const v = parseFloat(e.target.value) || 50;
|
|
saveDraft({ financialData: { extra_cash_emergency_pct: v } }).catch(() => {});
|
|
}}
|
|
className="w-full border rounded p-2"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block font-medium">Extra Monthly Cash to Retirement Fund (%)</label>
|
|
<input
|
|
name="extra_cash_retirement_pct"
|
|
type="number"
|
|
placeholder="% to Retirement Savings (e.g., 70)"
|
|
value={extra_cash_retirement_pct}
|
|
onChange={handleChange}
|
|
onBlur={(e) => {
|
|
const v = parseFloat(e.target.value) || 50;
|
|
saveDraft({ financialData: { extra_cash_retirement_pct: v } }).catch(() => {});
|
|
}}
|
|
className="w-full border rounded p-2"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex justify-between pt-4">
|
|
<button
|
|
onClick={prevStep}
|
|
className="bg-gray-200 hover:bg-gray-300 text-gray-700 font-semibold py-2 px-4 rounded"
|
|
>
|
|
← Previous: Career
|
|
</button>
|
|
<button
|
|
onClick={handleSubmit}
|
|
className="bg-blue-500 hover:bg-blue-600 text-white font-semibold py-2 px-4 rounded"
|
|
>
|
|
Next: College →
|
|
</button>
|
|
|
|
{showExpensesWizard && (
|
|
<Modal onClose={() => setShowExpensesWizard(false)}>
|
|
<ExpensesWizard
|
|
onClose={() => setShowExpensesWizard(false)}
|
|
onExpensesCalculated={handleExpensesCalculated}
|
|
/>
|
|
</Modal>
|
|
)}
|
|
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default FinancialOnboarding;
|