FinancialProfileForm updates to accommodate school/program/degree selection and addition of tuition field.

This commit is contained in:
Josh 2025-04-04 11:49:52 +00:00
parent 8dc4755911
commit 0ddc848371
12 changed files with 631 additions and 161 deletions

View File

@ -10,6 +10,9 @@ import { v4 as uuidv4 } from 'uuid';
import path from 'path';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
@ -374,11 +377,11 @@ app.post('/api/premium/financial-profile', authenticatePremiumUser, async (req,
const {
currentSalary, additionalIncome, monthlyExpenses, monthlyDebtPayments,
retirementSavings, retirementContribution, emergencyFund,
inCollege, expectedGraduation, partTimeIncome, tuitionPaid, collegeLoanTotal
inCollege, expectedGraduation, partTimeIncome, tuitionPaid, collegeLoanTotal,
careerPathId // ✅ Required to run simulation
} = req.body;
try {
// Upsert-style logic: Check if exists
const existing = await db.get(`SELECT id FROM financial_profile WHERE user_id = ?`, [req.userId]);
if (existing) {
@ -409,7 +412,36 @@ app.post('/api/premium/financial-profile', authenticatePremiumUser, async (req,
]);
}
res.status(200).json({ message: 'Financial profile saved.' });
// ✅ Run projection only if careerPathId is provided
if (!careerPathId) {
return res.status(200).json({ message: 'Financial profile saved. No projection generated.' });
}
const milestones = await db.all(
`SELECT * FROM milestones WHERE user_id = ? AND career_path_id = ? ORDER BY date ASC`,
[req.userId, careerPathId]
);
const projectionData = simulateFinancialProjection({
currentSalary,
additionalIncome,
monthlyExpenses,
monthlyDebtPayments,
retirementSavings,
retirementContribution,
emergencySavings: emergencyFund,
studentLoanAmount: collegeLoanTotal,
studentLoanAPR: 5.5, // placeholder default, can be user-supplied later
loanTermYears: 10, // placeholder default, can be user-supplied later
milestones,
gradDate: expectedGraduation,
fullTimeCollegeStudent: !!inCollege,
partTimeIncome,
startDate: moment()
});
return res.status(200).json({ message: 'Financial profile saved.', projectionData });
} catch (error) {
console.error('Error saving financial profile:', error);
res.status(500).json({ error: 'Failed to save financial profile.' });
@ -417,6 +449,7 @@ app.post('/api/premium/financial-profile', authenticatePremiumUser, async (req,
});
// Retrieve career history
app.get('/api/premium/career-history', authenticatePremiumUser, async (req, res) => {
try {

28
package-lock.json generated
View File

@ -16,6 +16,7 @@
"axios": "^1.7.9",
"bcrypt": "^5.1.1",
"chart.js": "^4.4.7",
"chartjs-plugin-annotation": "^3.1.0",
"class-variance-authority": "^0.7.1",
"classnames": "^2.5.1",
"clsx": "^2.1.1",
@ -26,6 +27,7 @@
"jsonwebtoken": "^9.0.2",
"jwt-decode": "^4.0.0",
"lucide-react": "^0.483.0",
"moment": "^2.30.1",
"react": "^18.2.0",
"react-chartjs-2": "^5.2.0",
"react-dom": "^18.2.0",
@ -6191,6 +6193,15 @@
"pnpm": ">=8"
}
},
"node_modules/chartjs-plugin-annotation": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/chartjs-plugin-annotation/-/chartjs-plugin-annotation-3.1.0.tgz",
"integrity": "sha512-EkAed6/ycXD/7n0ShrlT1T2Hm3acnbFhgkIEJLa0X+M6S16x0zwj1Fv4suv/2bwayCT3jGPdAtI9uLcAMToaQQ==",
"license": "MIT",
"peerDependencies": {
"chart.js": ">=4.0.0"
}
},
"node_modules/check-types": {
"version": "11.2.3",
"resolved": "https://registry.npmjs.org/check-types/-/check-types-11.2.3.tgz",
@ -12830,6 +12841,15 @@
"integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==",
"license": "MIT"
},
"node_modules/moment": {
"version": "2.30.1",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz",
"integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==",
"license": "MIT",
"engines": {
"node": "*"
}
},
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
@ -18429,9 +18449,9 @@
}
},
"node_modules/typescript": {
"version": "5.8.2",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.2.tgz",
"integrity": "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==",
"version": "4.9.5",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
"integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
"license": "Apache-2.0",
"peer": true,
"bin": {
@ -18439,7 +18459,7 @@
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
"node": ">=4.2.0"
}
},
"node_modules/unbox-primitive": {

View File

@ -11,6 +11,7 @@
"axios": "^1.7.9",
"bcrypt": "^5.1.1",
"chart.js": "^4.4.7",
"chartjs-plugin-annotation": "^3.1.0",
"class-variance-authority": "^0.7.1",
"classnames": "^2.5.1",
"clsx": "^2.1.1",
@ -21,6 +22,7 @@
"jsonwebtoken": "^9.0.2",
"jwt-decode": "^4.0.0",
"lucide-react": "^0.483.0",
"moment": "^2.30.1",
"react": "^18.2.0",
"react-chartjs-2": "^5.2.0",
"react-dom": "^18.2.0",

View File

@ -12,7 +12,8 @@ import MilestoneTracker from "./components/MilestoneTracker.js";
import './App.css';
function App() {
console.log("App rendered");
const [isAuthenticated, setIsAuthenticated] = useState(() => {
return !!localStorage.getItem('token'); // Check localStorage
});
@ -29,37 +30,22 @@ function App() {
<Route path="/signup" element={<SignUp />} />
{/* Protected routes */}
<Route
path="/getting-started"
element={isAuthenticated ? <GettingStarted /> : <Navigate to="/signin" />}
/>
<Route
path="/interest-inventory"
element={isAuthenticated ? <InterestInventory /> : <Navigate to="/signin" />}
/>
<Route
path="/dashboard"
element={isAuthenticated ? <Dashboard /> : <Navigate to="/signin" />}
/>
<Route
path="/profile"
element={isAuthenticated ? <UserProfile /> : <Navigate to="/signin" />}
/>
<Route
path="/milestone-tracker"
element={isAuthenticated ? <MilestoneTracker /> : <Navigate to="/signin" />}
/>
<Route
path="/financial-profile"
element={isAuthenticated ? <FinancialProfileForm /> : <Navigate to="/signin" />}
/>
{isAuthenticated && (
<>
<Route path="/getting-started" element={<GettingStarted />} />
<Route path="/interest-inventory" element={<InterestInventory />} />
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/profile" element={<UserProfile />} />
<Route path="/milestone-tracker" element={<MilestoneTracker />} />
<Route path="/financial-profile" element={<FinancialProfileForm />} />
</>
)}
{/* Catch-all for unknown routes */}
<Route path="*" element={<Navigate to="/signin" />} />
</Routes>
<SessionExpiredHandler />
</div>
);
}

View File

@ -2,11 +2,21 @@
import React, { useEffect, useState } from 'react';
const AISuggestedMilestones = ({ userId, career, careerPathId, authFetch, activeView }) => {
const AISuggestedMilestones = ({ userId, career, careerPathId, authFetch, activeView, projectionData }) => {
const [suggestedMilestones, setSuggestedMilestones] = useState([]);
const [selected, setSelected] = useState([]);
const [loading, setLoading] = useState(false);
useEffect(() => {
if (!Array.isArray(projectionData)) {
console.warn('⚠️ projectionData is not an array:', projectionData);
return;
}
console.log('📊 projectionData sample:', projectionData.slice(0, 3));
}, [projectionData]);
useEffect(() => {
if (!career) return;
setSuggestedMilestones([

View File

@ -1,75 +1,199 @@
// Updated FinancialProfileForm.js with autosuggest for school and full field list restored
import React, { useState, useEffect } from "react";
import { useLocation, useNavigate } from 'react-router-dom';
import authFetch from '../utils/authFetch.js';
function FinancialProfileForm() {
const navigate = useNavigate();
const location = useLocation();
export default function FinancialProfileForm() {
const location = useLocation();
const navigate = useNavigate();
console.log("🔍 FinancialProfileForm mounted");
console.log("🔍 location.state:", location.state);
const initialCareer = location?.state?.selectedCareer;
const [selectedCareer, setSelectedCareer] = useState(initialCareer || null);
const userId = localStorage.getItem("userId");
const [formData, setFormData] = useState({
currentSalary: "",
additionalIncome: "",
monthlyExpenses: "",
monthlyDebtPayments: "",
retirementSavings: "",
retirementContribution: "",
emergencyFund: "",
inCollege: false,
expectedGraduation: "",
partTimeIncome: "",
tuitionPaid: "",
collegeLoanTotal: ""
});
const [userId] = useState(() => localStorage.getItem("userId"));
const [selectedCareer] = useState(() => location.state?.selectedCareer || null);
const [currentSalary, setCurrentSalary] = useState("");
const [additionalIncome, setAdditionalIncome] = useState("");
const [monthlyExpenses, setMonthlyExpenses] = useState("");
const [monthlyDebtPayments, setMonthlyDebtPayments] = useState("");
const [retirementSavings, setRetirementSavings] = useState("");
const [retirementContribution, setRetirementContribution] = useState("");
const [emergencyFund, setEmergencyFund] = useState("");
const [inCollege, setInCollege] = useState(false);
const [expectedGraduation, setExpectedGraduation] = useState("");
const [partTimeIncome, setPartTimeIncome] = useState("");
const [tuitionPaid, setTuitionPaid] = useState("");
const [collegeLoanTotal, setCollegeLoanTotal] = useState("");
const [existingCollegeDebt, setExistingCollegeDebt] = useState("");
const [creditHoursPerYear, setCreditHoursPerYear] = useState("");
const [programType, setProgramType] = useState("");
const [isFullyOnline, setIsFullyOnline] = useState(false);
const [selectedSchool, setSelectedSchool] = useState("");
const [selectedProgram, setSelectedProgram] = useState("");
const [manualTuition, setManualTuition] = useState("");
const [schoolData, setSchoolData] = useState([]);
const [schoolSuggestions, setSchoolSuggestions] = useState([]);
const [programSuggestions, setProgramSuggestions] = useState([]);
const [availableProgramTypes, setAvailableProgramTypes] = useState([]);
const [calculatedTuition, setCalculatedTuition] = useState(0);
useEffect(() => {
console.log("✅ selectedCareer in useEffect:", selectedCareer);
}, [selectedCareer]);
// Fetch existing data on mount
useEffect(() => {
async function fetchFinancialProfile() {
try {
const res = await authFetch("/api/premium/financial-profile", {
method: "GET",
headers: {
"Authorization": `Bearer ${localStorage.getItem('token')}`
}
});
if (res.ok) {
const data = await res.json();
if (data && Object.keys(data).length > 0) {
setFormData((prev) => ({ ...prev, ...data }));
} else {
console.log("No existing financial profile. Starting fresh.");
async function fetchSchoolData() {
const res = await fetch('/cip_institution_mapping_new.json');
const text = await res.text();
const lines = text.split('\n');
const parsed = lines.map(line => {
try {
return JSON.parse(line);
} catch {
return null;
}
} else {
console.warn("Response not OK when fetching financial profile:", res.status);
}
} catch (err) {
console.error("Failed to fetch financial profile", err);
}).filter(Boolean);
setSchoolData(parsed);
}
}
fetchSchoolData();
}, []);
fetchFinancialProfile();
}, [userId]);
useEffect(() => {
async function fetchFinancialProfile() {
try {
const res = await authFetch("/api/premium/financial-profile", {
method: "GET",
headers: { "Authorization": `Bearer ${localStorage.getItem('token')}` }
});
if (res.ok) {
const data = await res.json();
if (data && Object.keys(data).length > 0) {
setCurrentSalary(data.current_salary || "");
setAdditionalIncome(data.additional_income || "");
setMonthlyExpenses(data.monthly_expenses || "");
setMonthlyDebtPayments(data.monthly_debt_payments || "");
setRetirementSavings(data.retirement_savings || "");
setRetirementContribution(data.retirement_contribution || "");
setEmergencyFund(data.emergency_fund || "");
setInCollege(!!data.in_college);
setExpectedGraduation(data.expected_graduation || "");
setPartTimeIncome(data.part_time_income || "");
setTuitionPaid(data.tuition_paid || "");
setCollegeLoanTotal(data.college_loan_total || "");
setExistingCollegeDebt(data.existing_college_debt || "");
setCreditHoursPerYear(data.credit_hours_per_year || "");
setProgramType(data.program_type || "");
setIsFullyOnline(!!data.is_fully_online);
setSelectedSchool(data.selected_school || "");
setSelectedProgram(data.selected_program || "");
}
}
} catch (err) {
console.error("Failed to fetch financial profile", err);
}
}
const handleChange = (e) => {
const { name, value, type, checked } = e.target;
setFormData((prev) => ({
...prev,
[name]: type === "checkbox" ? checked : value
}));
fetchFinancialProfile();
}, [userId]);
useEffect(() => {
if (selectedSchool && schoolData.length > 0) {
// Filter programs for the selected school and display them as suggestions
const programs = schoolData
.filter(s => s.INSTNM.toLowerCase() === selectedSchool.toLowerCase())
.map(s => s.CIPDESC);
// Filter unique programs and show the first 10
setProgramSuggestions([...new Set(programs)].slice(0, 10));
}
}, [selectedSchool, schoolData]);
useEffect(() => {
if (selectedProgram && selectedSchool && schoolData.length > 0) {
const types = schoolData
.filter(s => s.CIPDESC === selectedProgram && s.INSTNM.toLowerCase() === selectedSchool.toLowerCase())
.map(s => s.CREDDESC);
setAvailableProgramTypes([...new Set(types)]);
}
}, [selectedProgram, selectedSchool, schoolData]);
useEffect(() => {
if (selectedSchool && selectedProgram && programType && schoolData.length > 0) {
const match = schoolData.find(s =>
s.INSTNM.toLowerCase() === selectedSchool.toLowerCase() &&
s.CIPDESC === selectedProgram &&
s.CREDDESC === programType
);
const tuition = match ? parseFloat(match[isFullyOnline ? "Out State Graduate" : "In_state cost"] || 0) : 0;
setCalculatedTuition(tuition);
}
}, [selectedSchool, selectedProgram, programType, isFullyOnline, schoolData]);
const handleSchoolChange = (e) => {
const value = e.target.value;
setSelectedSchool(value);
const filtered = schoolData.filter(s => s.INSTNM.toLowerCase().includes(value.toLowerCase()));
const unique = [...new Set(filtered.map(s => s.INSTNM))];
setSchoolSuggestions(unique.slice(0, 10));
setSelectedProgram("");
setAvailableProgramTypes([]);
};
const handleSchoolSelect = (name) => {
setSelectedSchool(name);
setSchoolSuggestions([]);
setSelectedProgram("");
setAvailableProgramTypes([]);
setProgramSuggestions([]);
};
const handleProgramChange = (e) => {
const value = e.target.value;
setSelectedProgram(value);
if (!value) {
setProgramSuggestions([]);
return;
}
const filtered = schoolData.filter(s =>
s.INSTNM.toLowerCase() === selectedSchool.toLowerCase() &&
s.CIPDESC.toLowerCase().includes(value.toLowerCase())
);
const uniquePrograms = [...new Set(filtered.map(s => s.CIPDESC))];
setProgramSuggestions(uniquePrograms);
const filteredTypes = schoolData.filter(s =>
s.INSTNM.toLowerCase() === selectedSchool.toLowerCase() &&
s.CIPDESC === value
).map(s => s.CREDDESC);
setAvailableProgramTypes([...new Set(filteredTypes)]);
};
const handleProgramTypeSelect = (e) => {
setProgramType(e.target.value);
};
const handleSubmit = async (e) => {
e.preventDefault();
const formData = {
currentSalary,
additionalIncome,
monthlyExpenses,
monthlyDebtPayments,
retirementSavings,
retirementContribution,
emergencyFund,
inCollege,
expectedGraduation,
partTimeIncome,
tuitionPaid,
collegeLoanTotal,
existingCollegeDebt,
creditHoursPerYear,
programType,
isFullyOnline,
selectedSchool,
selectedProgram
};
try {
const res = await authFetch("/api/premium/financial-profile", {
method: "POST",
@ -81,90 +205,126 @@ export default function FinancialProfileForm() {
navigate('/milestone-tracker', {
state: { selectedCareer }
});
}
}
} catch (err) {
console.error("Error submitting financial profile:", err);
}
};
const handleInput = (setter) => (e) => setter(e.target.value);
return (
<form onSubmit={handleSubmit} className="max-w-2xl mx-auto p-4 space-y-4 bg-white shadow rounded">
<h2 className="text-xl font-semibold">Your Financial Profile</h2>
<div>
<label className="block font-medium">Current Salary</label>
<input name="currentSalary" type="number" value={formData.currentSalary} onChange={handleChange}
className="w-full border rounded p-2" placeholder="$" />
</div>
<label className="block font-medium">Current Salary</label>
<input type="number" value={currentSalary} onChange={handleInput(setCurrentSalary)} className="w-full border rounded p-2" placeholder="$" />
<div>
<label className="block font-medium">Additional Monthly Income</label>
<input name="additionalIncome" type="number" value={formData.additionalIncome} onChange={handleChange}
className="w-full border rounded p-2" placeholder="$" />
</div>
<label className="block font-medium">Additional Monthly Income</label>
<input type="number" value={additionalIncome} onChange={handleInput(setAdditionalIncome)} className="w-full border rounded p-2" placeholder="$" />
<div>
<label className="block font-medium">Monthly Living Expenses</label>
<input name="monthlyExpenses" type="number" value={formData.monthlyExpenses} onChange={handleChange}
className="w-full border rounded p-2" placeholder="$" />
</div>
<label className="block font-medium">Monthly Living Expenses</label>
<input type="number" value={monthlyExpenses} onChange={handleInput(setMonthlyExpenses)} className="w-full border rounded p-2" placeholder="$" />
<div>
<label className="block font-medium">Monthly Debt Payments (non-student)</label>
<input name="monthlyDebtPayments" type="number" value={formData.monthlyDebtPayments} onChange={handleChange}
className="w-full border rounded p-2" placeholder="$" />
</div>
<label className="block font-medium">Monthly Debt Payments</label>
<input type="number" value={monthlyDebtPayments} onChange={handleInput(setMonthlyDebtPayments)} className="w-full border rounded p-2" placeholder="$" />
<div>
<label className="block font-medium">Current Retirement Savings</label>
<input name="retirementSavings" type="number" value={formData.retirementSavings} onChange={handleChange}
className="w-full border rounded p-2" placeholder="$" />
</div>
<label className="block font-medium">Retirement Savings</label>
<input type="number" value={retirementSavings} onChange={handleInput(setRetirementSavings)} className="w-full border rounded p-2" placeholder="$" />
<div>
<label className="block font-medium">Monthly Retirement Contribution</label>
<input name="retirementContribution" type="number" value={formData.retirementContribution} onChange={handleChange}
className="w-full border rounded p-2" placeholder="$" />
</div>
<label className="block font-medium">Monthly Retirement Contribution</label>
<input type="number" value={retirementContribution} onChange={handleInput(setRetirementContribution)} className="w-full border rounded p-2" placeholder="$" />
<div>
<label className="block font-medium">Emergency Fund Balance</label>
<input name="emergencyFund" type="number" value={formData.emergencyFund} onChange={handleChange}
className="w-full border rounded p-2" placeholder="$" />
</div>
<label className="block font-medium">Emergency Fund Balance</label>
<input type="number" value={emergencyFund} onChange={handleInput(setEmergencyFund)} className="w-full border rounded p-2" placeholder="$" />
<div className="flex items-center space-x-2">
<input id="inCollege" name="inCollege" type="checkbox" checked={formData.inCollege} onChange={handleChange} />
<input id="inCollege" type="checkbox" checked={inCollege} onChange={(e) => setInCollege(e.target.checked)} />
<label htmlFor="inCollege" className="font-medium">Are you currently in college?</label>
</div>
{formData.inCollege && (
{inCollege && (
<>
<div>
<label className="block font-medium">Expected Graduation Date</label>
<input name="expectedGraduation" type="date" value={formData.expectedGraduation} onChange={handleChange}
className="w-full border rounded p-2" />
</div>
{/* Selected School input with suggestions */}
<label className="block font-medium">Selected School</label>
<input
type="text"
value={selectedSchool}
onChange={handleSchoolChange}
className="w-full border rounded p-2"
placeholder="Search for a School"
/>
{schoolSuggestions.length > 0 && (
<ul className="border rounded bg-white max-h-40 overflow-y-auto shadow-md">
{schoolSuggestions.map((suggestion, idx) => (
<li
key={idx}
onClick={() => handleSchoolSelect(suggestion)}
className="p-2 hover:bg-blue-100 cursor-pointer"
>
{suggestion}
</li>
))}
</ul>
)}
<div>
<label className="block font-medium">Part-Time Monthly Income</label>
<input name="partTimeIncome" type="number" value={formData.partTimeIncome} onChange={handleChange}
className="w-full border rounded p-2" placeholder="$" />
</div>
<div>
<label className="block font-medium">Tuition Paid So Far</label>
<input name="tuitionPaid" type="number" value={formData.tuitionPaid} onChange={handleChange}
className="w-full border rounded p-2" placeholder="$" />
</div>
<div>
<label className="block font-medium">Total College Loan Amount</label>
<input name="collegeLoanTotal" type="number" value={formData.collegeLoanTotal} onChange={handleChange}
className="w-full border rounded p-2" placeholder="$" />
</div>
</>
)}
{/* Program input with suggestions */}
<label className="block font-medium">Program</label>
<input
type="text"
value={selectedProgram}
onChange={handleProgramChange}
className="w-full border rounded p-2"
placeholder="Search for a Program"
/>
{selectedProgram.length > 0 && programSuggestions.length > 0 && (
<ul className="border rounded bg-white max-h-40 overflow-y-auto shadow-md">
{programSuggestions.map((suggestion, idx) => (
<li
key={idx}
onClick={() => {
setSelectedProgram(suggestion);
setProgramSuggestions([]);
const filteredTypes = schoolData.filter(s =>
s.INSTNM.toLowerCase() === selectedSchool.toLowerCase() &&
s.CIPDESC === suggestion
).map(s => s.CREDDESC);
setAvailableProgramTypes([...new Set(filteredTypes)]);
}}
className="p-2 hover:bg-blue-100 cursor-pointer"
>
{suggestion}
</li>
))}
</ul>
)}
{/* Program Type input */}
<label className="block font-medium">Program Type</label>
<select
value={programType}
onChange={handleProgramTypeSelect}
className="w-full border rounded p-2"
>
<option value="">Select Program Type</option>
{availableProgramTypes.map((type, idx) => (
<option key={idx} value={type}>
{type}
</option>
))}
</select>
</>
)}
<>
<label className="block font-medium">Estimated Tuition (Override if needed)</label>
<input
type="number"
value={manualTuition || calculatedTuition}
onChange={handleInput(setManualTuition)}
className="w-full border rounded p-2"
placeholder="Override tuition amount"
/>
</>
<div className="pt-4">
<button type="submit" className="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700">
@ -174,3 +334,5 @@ export default function FinancialProfileForm() {
</form>
);
}
export default FinancialProfileForm;

View File

@ -38,7 +38,7 @@ function GettingStarted() {
<div className="premium-access">
<h3>Already know your path?</h3>
<p>You can skip ahead and begin planning your milestones now.</p>
<button className="premium-button" onClick={() => navigate('/milestone-tracker', { state: { fromGettingStarted: true } })}>
<button className="premium-button" onClick={() => navigate('/financial-profile', { state: { fromGettingStarted: true } })}>
Access Milestone Tracker <span className="premium-label">(Premium)</span>
</button>
</div>

View File

@ -179,6 +179,17 @@ const filteredMilestones = raw.filter(
<input type="text" placeholder="Description" value={newMilestone.description} onChange={(e) => setNewMilestone({ ...newMilestone, description: e.target.value })} />
<input type="date" value={newMilestone.date} onChange={(e) => setNewMilestone({ ...newMilestone, date: e.target.value })} />
<input type="number" placeholder="Progress (%)" value={newMilestone.progress} onChange={(e) => setNewMilestone({ ...newMilestone, progress: parseInt(e.target.value, 10) })} />
{activeView === 'Financial' && (
<div>
<input
type="number"
placeholder="Full New Salary (e.g., 70000)"
value={newMilestone.newSalary || ''}
onChange={(e) => setNewMilestone({ ...newMilestone, newSalary: parseFloat(e.target.value) })}
/>
<p>Enter the full new salary (not just the change) after the milestone has taken place.</p>
</div>
)}
<button onClick={saveMilestone}>{editingMilestone ? 'Update' : 'Add'} Milestone</button>
</div>
)}

View File

@ -2,12 +2,22 @@
import React, { useState, useEffect } from 'react';
import { useLocation, useNavigate } from 'react-router-dom';
import { v4 as uuidv4 } from 'uuid';
import { Line } from 'react-chartjs-2';
import { Chart as ChartJS, LineElement, CategoryScale, LinearScale, PointElement, Tooltip, Legend } from 'chart.js';
import annotationPlugin from 'chartjs-plugin-annotation';
import { Filler } from 'chart.js';
import CareerSelectDropdown from './CareerSelectDropdown.js';
import CareerSearch from './CareerSearch.js';
import MilestoneTimeline from './MilestoneTimeline.js';
import AISuggestedMilestones from './AISuggestedMilestones.js';
import './MilestoneTracker.css';
import './MilestoneTimeline.css'; // Ensure this file contains styles for timeline-line and milestone-dot
import { simulateFinancialProjection } from '../utils/FinancialProjectionService.js';
ChartJS.register( LineElement, CategoryScale, LinearScale, Filler, PointElement, Tooltip, Legend, annotationPlugin );
const MilestoneTracker = ({ selectedCareer: initialCareer }) => {
const location = useLocation();
@ -19,10 +29,15 @@ const MilestoneTracker = ({ selectedCareer: initialCareer }) => {
const [showSessionExpiredModal, setShowSessionExpiredModal] = useState(false);
const [pendingCareerForModal, setPendingCareerForModal] = useState(null);
const [activeView, setActiveView] = useState("Career");
const [projectionData, setProjectionData] = useState(null);
const [financialProfile, setFinancialProfile] = useState(null); // Store the financial profile
const [loanPayoffMonth, setLoanPayoffMonth] = useState(null);
const apiURL = process.env.REACT_APP_API_URL;
const authFetch = async (url, options = {}) => {
const token = localStorage.getItem('token');
if (!token) {
@ -44,19 +59,19 @@ const MilestoneTracker = ({ selectedCareer: initialCareer }) => {
const fetchCareerPaths = async () => {
const res = await authFetch(`${apiURL}/premium/planned-path/all`);
if (!res) return;
const data = await res.json();
// Flatten nested array
const flatPaths = data.careerPath.flat();
// Handle duplicates
const uniquePaths = Array.from(
new Set(flatPaths.map(cp => cp.career_name))
).map(name => flatPaths.find(cp => cp.career_name === name));
setExistingCareerPaths(uniquePaths);
const fromPopout = location.state?.selectedCareer;
if (fromPopout) {
setSelectedCareer(fromPopout);
@ -72,8 +87,41 @@ const MilestoneTracker = ({ selectedCareer: initialCareer }) => {
}
}
};
const fetchFinancialProfile = async () => {
const res = await authFetch(`${apiURL}/premium/financial-profile`);
if (res && res.ok) {
const data = await res.json();
setFinancialProfile(data); // Set the financial profile in state
}
};
fetchCareerPaths();
}, []);
fetchFinancialProfile();
}, []);
// Calculate financial projection based on the profile
useEffect(() => {
if (financialProfile && selectedCareer) {
const { projectionData, loanPaidOffMonth } = simulateFinancialProjection({
currentSalary: financialProfile.current_salary,
monthlyExpenses: financialProfile.monthly_expenses,
studentLoanAmount: financialProfile.college_loan_total,
studentLoanAPR: 5.5, // Default rate
loanTermYears: 10, // Default term
emergencySavings: financialProfile.emergency_fund,
retirementSavings: financialProfile.retirement_savings,
monthlyRetirementContribution: financialProfile.retirement_contribution,
monthlyEmergencyContribution: 0, // Add emergency savings contribution if available
gradDate: financialProfile.expected_graduation,
fullTimeCollegeStudent: financialProfile.in_college,
partTimeIncome: financialProfile.part_time_income,
startDate: new Date(),
});
setProjectionData(projectionData);
setLoanPayoffMonth(loanPaidOffMonth); // Set the projection data
}
}, [financialProfile, selectedCareer]);
const handleCareerChange = (selected) => {
if (selected && selected.id && selected.career_name) {
@ -84,6 +132,8 @@ const MilestoneTracker = ({ selectedCareer: initialCareer }) => {
}
};
console.log("📊 projectionData sample:", projectionData?.slice(0, 5));
const handleConfirmCareerSelection = async () => {
const newId = uuidv4();
const body = { career_path_id: newId, career_name: pendingCareerForModal, start_date: new Date().toISOString().split('T')[0] };
@ -116,9 +166,92 @@ const MilestoneTracker = ({ selectedCareer: initialCareer }) => {
<MilestoneTimeline careerPathId={careerPathId} authFetch={authFetch} activeView={activeView} setActiveView={setActiveView} />
{console.log('Passing careerPathId to MilestoneTimeline:', careerPathId)}
<AISuggestedMilestones career={selectedCareer?.career_name} careerPathId={careerPathId} authFetch={authFetch} activeView={activeView}/>
<AISuggestedMilestones career={selectedCareer?.career_name} careerPathId={careerPathId} authFetch={authFetch} activeView={activeView} projectionData={projectionData}/>
<CareerSearch
{projectionData && (
<div className="bg-white p-4 mt-6 rounded shadow">
<h3 className="text-lg font-semibold mb-2">Financial Projection</h3>
<Line
data={{
labels: projectionData.map(p => p.month),
datasets: [
{
label: 'Net Savings',
data: projectionData.map(p => p.netSavings),
borderColor: 'rgba(54, 162, 235, 1)',
backgroundColor: 'rgba(54, 162, 235, 0.2)',
tension: 0.4,
fill: true
},
{
label: 'Loan Balance',
data: projectionData.map(p => p.loanBalance),
borderColor: 'rgba(255, 99, 132, 1)',
backgroundColor: 'rgba(255, 99, 132, 0.2)',
tension: 0.4,
fill: {
target: 'origin',
above: 'rgba(255,99,132,0.3)', // loan debt
below: 'transparent' // don't show below 0
}
},
{
label: 'Retirement Savings',
data: projectionData.map(p => p.totalRetirementSavings),
borderColor: 'rgba(75, 192, 192, 1)',
backgroundColor: 'rgba(75, 192, 192, 0.2)',
tension: 0.4,
fill: true
}
]
}}
options={{
responsive: true,
plugins: {
legend: { position: 'bottom' },
tooltip: { mode: 'index', intersect: false },
annotation: loanPayoffMonth
? {
annotations: {
loanPaidOffLine: {
type: 'line',
xMin: loanPayoffMonth,
xMax: loanPayoffMonth,
borderColor: 'rgba(255, 206, 86, 1)',
borderWidth: 2,
borderDash: [6, 6],
label: {
display: true,
content: 'Loan Paid Off',
position: 'end',
backgroundColor: 'rgba(255, 206, 86, 0.8)',
color: '#000',
font: {
style: 'bold',
size: 12
},
rotation: 0,
yAdjust: -10
}
}
}
}
: undefined
},
scales: {
y: {
beginAtZero: true,
ticks: {
callback: (value) => `$${value.toLocaleString()}`
}
}
}
}}
/>
</div>
)}
<CareerSearch
onSelectCareer={(careerName) => setPendingCareerForModal(careerName)}
setPendingCareerForModal={setPendingCareerForModal}
authFetch={authFetch}

View File

@ -0,0 +1,110 @@
import moment from 'moment';
// Function to simulate monthly financial projection
// src/utils/FinancialProjectionService.js
export function simulateFinancialProjection(userProfile) {
const {
currentSalary,
monthlyExpenses,
studentLoanAmount,
studentLoanAPR,
loanTermYears,
milestones = [],
emergencySavings,
retirementSavings,
monthlyRetirementContribution,
monthlyEmergencyContribution,
gradDate,
fullTimeCollegeStudent,
partTimeIncome = 0,
startDate = new Date()
} = userProfile;
const monthlyLoanPayment = calculateLoanPayment(studentLoanAmount, studentLoanAPR, loanTermYears);
let totalEmergencySavings = emergencySavings;
let totalRetirementSavings = retirementSavings;
let loanBalance = studentLoanAmount;
let monthlyIncome = currentSalary;
let projectionData = [];
const graduationDate = gradDate ? new Date(gradDate) : null;
let milestoneIndex = 0;
let loanPaidOffMonth = null;
const date = new Date(startDate);
for (let month = 0; month < 240; month++) {
// ✅ Advance date forward by one month
date.setMonth(date.getMonth() + 1);
if (loanBalance <= 0 && !loanPaidOffMonth) {
loanPaidOffMonth = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`;
}
// Check for milestone
if (
milestoneIndex < milestones.length &&
new Date(milestones[milestoneIndex].date) <= date
) {
const milestone = milestones[milestoneIndex];
if (milestone.type === 'salary') {
monthlyIncome = milestone.newSalary;
}
milestoneIndex++;
}
// If in college before graduation, adjust income
if (graduationDate && date < graduationDate) {
monthlyIncome = fullTimeCollegeStudent ? 0 : partTimeIncome;
}
let thisMonthLoanPayment = 0;
if (loanBalance > 0) {
const interestForMonth = loanBalance * (studentLoanAPR / 100 / 12);
const principalForMonth = Math.min(loanBalance, monthlyLoanPayment - interestForMonth);
loanBalance -= principalForMonth;
loanBalance = Math.max(loanBalance, 0);
thisMonthLoanPayment = monthlyLoanPayment;
}
let extraCash = monthlyLoanPayment - thisMonthLoanPayment;
totalEmergencySavings += monthlyEmergencyContribution + (extraCash * 0.3); // 30% redirect
totalRetirementSavings += monthlyRetirementContribution + (extraCash * 0.7); // 70% redirect
totalRetirementSavings *= (1 + 0.07 / 12); // compound growth
projectionData.push({
month: `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`,
salary: monthlyIncome,
monthlyIncome: monthlyIncome / 12,
expenses: monthlyExpenses,
loanPayment: monthlyLoanPayment,
retirementContribution: monthlyRetirementContribution,
emergencyContribution: monthlyEmergencyContribution,
netSavings:
monthlyIncome -
(monthlyExpenses +
thisMonthLoanPayment +
monthlyRetirementContribution +
monthlyEmergencyContribution),
totalEmergencySavings,
totalRetirementSavings,
loanBalance
});
}
return { projectionData, loanPaidOffMonth };
}
function calculateLoanPayment(principal, annualRate, years) {
const monthlyRate = annualRate / 100 / 12;
const numPayments = years * 12;
return (principal * monthlyRate) / (1 - Math.pow(1 + monthlyRate, -numPayments));
}

View File

@ -12,11 +12,14 @@ const authFetch = async (url, options = {}) => {
return null;
}
const method = options.method?.toUpperCase() || 'GET';
const shouldIncludeContentType = ['POST', 'PUT', 'PATCH'].includes(method);
const res = await fetch(url, {
...options,
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
...(shouldIncludeContentType && { 'Content-Type': 'application/json' }),
...options.headers,
},
});

Binary file not shown.