Added interest rate to Retirement Savings.
This commit is contained in:
parent
1e84c8b38a
commit
29bdb17321
@ -268,6 +268,11 @@ export default function MilestoneTracker({ selectedCareer: initialCareer }) {
|
|||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const apiURL = process.env.REACT_APP_API_URL;
|
const apiURL = process.env.REACT_APP_API_URL;
|
||||||
|
|
||||||
|
const [interestStrategy, setInterestStrategy] = useState('NONE'); // 'NONE' | 'FLAT' | 'MONTE_CARLO'
|
||||||
|
const [flatAnnualRate, setFlatAnnualRate] = useState(0.06); // 6% default
|
||||||
|
const [randomRangeMin, setRandomRangeMin] = useState(-0.03); // -3% monthly
|
||||||
|
const [randomRangeMax, setRandomRangeMax] = useState(0.08); // 8% monthly
|
||||||
|
|
||||||
// Basic states
|
// Basic states
|
||||||
const [userProfile, setUserProfile] = useState(null);
|
const [userProfile, setUserProfile] = useState(null);
|
||||||
const [financialProfile, setFinancialProfile] = useState(null);
|
const [financialProfile, setFinancialProfile] = useState(null);
|
||||||
@ -276,9 +281,6 @@ export default function MilestoneTracker({ selectedCareer: initialCareer }) {
|
|||||||
const [selectedCareer, setSelectedCareer] = useState(initialCareer || null);
|
const [selectedCareer, setSelectedCareer] = useState(initialCareer || null);
|
||||||
const [careerProfileId, setCareerProfileId] = useState(null);
|
const [careerProfileId, setCareerProfileId] = useState(null);
|
||||||
const [scenarioRow, setScenarioRow] = useState(null);
|
const [scenarioRow, setScenarioRow] = useState(null);
|
||||||
const [aiRisk, setAiRisk] = useState(null);
|
|
||||||
const [aiRiskLoading, setAiRiskLoading] = useState(false);
|
|
||||||
const [aiRiskError, setAiRiskError] = useState(null);
|
|
||||||
const [collegeProfile, setCollegeProfile] = useState(null);
|
const [collegeProfile, setCollegeProfile] = useState(null);
|
||||||
|
|
||||||
const [strippedSocCode, setStrippedSocCode] = useState(null);
|
const [strippedSocCode, setStrippedSocCode] = useState(null);
|
||||||
@ -458,81 +460,6 @@ export default function MilestoneTracker({ selectedCareer: initialCareer }) {
|
|||||||
setStrippedSocCode(stripSocCode(found.soc_code));
|
setStrippedSocCode(stripSocCode(found.soc_code));
|
||||||
}, [scenarioRow, masterCareerRatings]);
|
}, [scenarioRow, masterCareerRatings]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
// If we have no SOC code or scenarioRow is missing, reset
|
|
||||||
if (!strippedSocCode || !scenarioRow?.career_name) {
|
|
||||||
setAiRisk(null);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function fetchAiRisk() {
|
|
||||||
setAiRiskLoading(true);
|
|
||||||
setAiRiskError(null);
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 1) Attempt local DB first
|
|
||||||
const localRes = await fetch(`${apiURL}/ai-risk/${strippedSocCode}`);
|
|
||||||
if (localRes.ok) {
|
|
||||||
const localData = await localRes.json();
|
|
||||||
setAiRisk(localData);
|
|
||||||
} else if (localRes.status === 404) {
|
|
||||||
// 2) If not found => call GPT route
|
|
||||||
// We'll pass minimal data if we don't have job description or tasks
|
|
||||||
const chatPayload = {
|
|
||||||
socCode: strippedSocCode,
|
|
||||||
careerName: scenarioRow.career_name,
|
|
||||||
jobDescription: '', // or optionally fetch from O*NET / tasks
|
|
||||||
tasks: []
|
|
||||||
};
|
|
||||||
|
|
||||||
// POST to your server3 public route
|
|
||||||
const gptRes = await fetch(`${apiURL}/public/ai-risk-analysis`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify(chatPayload),
|
|
||||||
});
|
|
||||||
if (!gptRes.ok) {
|
|
||||||
throw new Error('GPT call failed');
|
|
||||||
}
|
|
||||||
const gptData = await gptRes.json();
|
|
||||||
|
|
||||||
// 3) Store in server2 so we skip GPT next time
|
|
||||||
await fetch(`${apiURL}/ai-risk`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({
|
|
||||||
socCode: strippedSocCode,
|
|
||||||
careerName: gptData.careerName,
|
|
||||||
jobDescription: gptData.jobDescription,
|
|
||||||
tasks: gptData.tasks,
|
|
||||||
riskLevel: gptData.riskLevel,
|
|
||||||
reasoning: gptData.reasoning,
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
// 4) Set it in state
|
|
||||||
setAiRisk({
|
|
||||||
socCode: strippedSocCode,
|
|
||||||
careerName: scenarioRow.career_name,
|
|
||||||
riskLevel: gptData.riskLevel,
|
|
||||||
reasoning: gptData.reasoning
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// Some other error code
|
|
||||||
throw new Error(`AI Risk fetch error: ${localRes.status}`);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Error fetching AI risk =>', err);
|
|
||||||
setAiRiskError('Failed to load AI risk data.');
|
|
||||||
setAiRisk(null);
|
|
||||||
} finally {
|
|
||||||
setAiRiskLoading(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fetchAiRisk();
|
|
||||||
}, [strippedSocCode, scenarioRow?.career_name, apiURL]);
|
|
||||||
|
|
||||||
// 6) Salary
|
// 6) Salary
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!strippedSocCode) {
|
if (!strippedSocCode) {
|
||||||
@ -717,7 +644,13 @@ export default function MilestoneTracker({ selectedCareer: initialCareer }) {
|
|||||||
|
|
||||||
startDate: new Date().toISOString(),
|
startDate: new Date().toISOString(),
|
||||||
simulationYears,
|
simulationYears,
|
||||||
milestoneImpacts: allImpacts
|
milestoneImpacts: allImpacts,
|
||||||
|
|
||||||
|
interestStrategy,
|
||||||
|
flatAnnualRate,
|
||||||
|
monthlyReturnSamples: [], // or keep an array if you have historical data
|
||||||
|
randomRangeMin,
|
||||||
|
randomRangeMax
|
||||||
};
|
};
|
||||||
|
|
||||||
const { projectionData: pData, loanPaidOffMonth } =
|
const { projectionData: pData, loanPaidOffMonth } =
|
||||||
@ -739,7 +672,7 @@ export default function MilestoneTracker({ selectedCareer: initialCareer }) {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!financialProfile || !scenarioRow || !collegeProfile) return;
|
if (!financialProfile || !scenarioRow || !collegeProfile) return;
|
||||||
buildProjection();
|
buildProjection();
|
||||||
}, [financialProfile, scenarioRow, collegeProfile, careerProfileId, apiURL, simulationYears]);
|
}, [financialProfile, scenarioRow, collegeProfile, careerProfileId, apiURL, simulationYears, interestStrategy, flatAnnualRate, randomRangeMin, randomRangeMax]);
|
||||||
|
|
||||||
// Build chart datasets / annotations
|
// Build chart datasets / annotations
|
||||||
const milestoneAnnotationLines = {};
|
const milestoneAnnotationLines = {};
|
||||||
@ -976,22 +909,6 @@ if (aiLoading || clickCount >= DAILY_CLICK_LIMIT) {
|
|||||||
{yearsInCareer === '<1' ? 'year' : 'years'}
|
{yearsInCareer === '<1' ? 'year' : 'years'}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="mt-2">
|
|
||||||
{aiRiskLoading ? (
|
|
||||||
<p className="text-sm text-gray-500">Loading AI risk...</p>
|
|
||||||
) : aiRiskError ? (
|
|
||||||
<p className="text-sm text-red-500">{aiRiskError}</p>
|
|
||||||
) : aiRisk ? (
|
|
||||||
<div className="text-sm text-gray-700">
|
|
||||||
<strong>AI Risk Level:</strong> {aiRisk.riskLevel}
|
|
||||||
<br />
|
|
||||||
<em>{aiRisk.reasoning}</em>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<p className="text-sm text-gray-500">No AI risk data available</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 2) Salary Benchmarks */}
|
{/* 2) Salary Benchmarks */}
|
||||||
@ -1151,6 +1068,54 @@ if (aiLoading || clickCount >= DAILY_CLICK_LIMIT) {
|
|||||||
authFetch={authFetch}
|
authFetch={authFetch}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* (E1) Interest Strategy */}
|
||||||
|
<label className="ml-4 font-medium">Interest Strategy:</label>
|
||||||
|
<select
|
||||||
|
value={interestStrategy}
|
||||||
|
onChange={(e) => setInterestStrategy(e.target.value)}
|
||||||
|
className="border rounded p-1"
|
||||||
|
>
|
||||||
|
<option value="NONE">No Interest</option>
|
||||||
|
<option value="FLAT">Flat Rate</option>
|
||||||
|
<option value="MONTE_CARLO">Monte Carlo</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
{/* (E2) If FLAT => show the annual rate */}
|
||||||
|
{interestStrategy === 'FLAT' && (
|
||||||
|
<div className="inline-block ml-4">
|
||||||
|
<label className="mr-1">Annual Rate (%):</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
step="0.01"
|
||||||
|
value={flatAnnualRate}
|
||||||
|
onChange={(e) => setFlatAnnualRate(parseFloatOrZero(e.target.value, 0.06))}
|
||||||
|
className="border rounded p-1 w-20"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* (E3) If MONTE_CARLO => show the random range */}
|
||||||
|
{interestStrategy === 'MONTE_CARLO' && (
|
||||||
|
<div className="inline-block ml-4">
|
||||||
|
<label className="mr-1">Min Return (%):</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
step="0.01"
|
||||||
|
value={randomRangeMin}
|
||||||
|
onChange={(e) => setRandomRangeMin(parseFloatOrZero(e.target.value, -0.03))}
|
||||||
|
className="border rounded p-1 w-20 mr-2"
|
||||||
|
/>
|
||||||
|
<label className="mr-1">Max Return (%):</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
step="0.01"
|
||||||
|
value={randomRangeMax}
|
||||||
|
onChange={(e) => setRandomRangeMax(parseFloatOrZero(e.target.value, 0.08))}
|
||||||
|
className="border rounded p-1 w-20"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* 7) AI Next Steps */}
|
{/* 7) AI Next Steps */}
|
||||||
<div className="bg-white p-4 rounded shadow mt-4">
|
<div className="bg-white p-4 rounded shadow mt-4">
|
||||||
<Button onClick={handleAiClick} disabled={aiLoading || clickCount >= DAILY_CLICK_LIMIT}>
|
<Button onClick={handleAiClick} disabled={aiLoading || clickCount >= DAILY_CLICK_LIMIT}>
|
||||||
|
@ -137,10 +137,40 @@ export function simulateFinancialProjection(userProfile) {
|
|||||||
milestoneImpacts = [],
|
milestoneImpacts = [],
|
||||||
|
|
||||||
// Simulation duration
|
// Simulation duration
|
||||||
simulationYears = 20
|
simulationYears = 20,
|
||||||
|
|
||||||
|
interestStrategy = 'NONE', // 'NONE' | 'FLAT' | 'MONTE_CARLO'
|
||||||
|
flatAnnualRate = 0.06, // 6% default if using FLAT
|
||||||
|
monthlyReturnSamples = [], // if using historical-based random sampling
|
||||||
|
randomRangeMin = -0.03, // if using a random range approach
|
||||||
|
randomRangeMax = 0.08,
|
||||||
|
|
||||||
} = userProfile;
|
} = userProfile;
|
||||||
|
|
||||||
|
|
||||||
|
/***************************************************
|
||||||
|
* HELPER: Retirement Interest Rate
|
||||||
|
***************************************************/
|
||||||
|
function getMonthlyInterestRate() {
|
||||||
|
// e.g. a switch or if-else:
|
||||||
|
if (interestStrategy === 'NONE') {
|
||||||
|
return 0;
|
||||||
|
} else if (interestStrategy === 'FLAT') {
|
||||||
|
// e.g. 6% annual => 0.5% per month
|
||||||
|
return flatAnnualRate / 12;
|
||||||
|
} else if (interestStrategy === 'MONTE_CARLO') {
|
||||||
|
// if using a random range or historical sample
|
||||||
|
if (monthlyReturnSamples.length > 0) {
|
||||||
|
const idx = Math.floor(Math.random() * monthlyReturnSamples.length);
|
||||||
|
return monthlyReturnSamples[idx]; // already monthly
|
||||||
|
} else {
|
||||||
|
const range = randomRangeMax - randomRangeMin;
|
||||||
|
return randomRangeMin + (Math.random() * range);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0; // fallback
|
||||||
|
}
|
||||||
|
|
||||||
/***************************************************
|
/***************************************************
|
||||||
* 2) CLAMP THE SCENARIO START TO MONTH-BEGIN
|
* 2) CLAMP THE SCENARIO START TO MONTH-BEGIN
|
||||||
***************************************************/
|
***************************************************/
|
||||||
@ -401,6 +431,10 @@ export function simulateFinancialProjection(userProfile) {
|
|||||||
currentRetirementSavings += retPortion;
|
currentRetirementSavings += retPortion;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const monthlyReturnRate = getMonthlyInterestRate();
|
||||||
|
if (monthlyReturnRate !== 0) {
|
||||||
|
currentRetirementSavings *= (1 + monthlyReturnRate);
|
||||||
|
}
|
||||||
const netSavings = netMonthlyIncome - actualExpensesPaid;
|
const netSavings = netMonthlyIncome - actualExpensesPaid;
|
||||||
|
|
||||||
// (UPDATED) add inCollege, stillInCollege, loanDeferralUntilGraduation to the result
|
// (UPDATED) add inCollege, stillInCollege, loanDeferralUntilGraduation to the result
|
||||||
|
Loading…
Reference in New Issue
Block a user