Fixed jwt token/user_auth.user_id vs. user_profile.id. College/financial/Premium Onboarding calls, ReviewPage info.
This commit is contained in:
parent
0c8cd4a969
commit
287737fa8b
@ -180,8 +180,8 @@ app.post('/api/register', async (req, res) => {
|
|||||||
|
|
||||||
// 2) Insert into user_auth, referencing user_profile.id
|
// 2) Insert into user_auth, referencing user_profile.id
|
||||||
const authQuery = `
|
const authQuery = `
|
||||||
INSERT INTO user_auth (id, username, hashed_password)
|
INSERT INTO user_auth (user_id, username, hashed_password)
|
||||||
VALUES (?, ?, ?)
|
VALUES (?, ?, ?)
|
||||||
`;
|
`;
|
||||||
pool.query(
|
pool.query(
|
||||||
authQuery,
|
authQuery,
|
||||||
@ -219,7 +219,7 @@ app.post('/api/register', async (req, res) => {
|
|||||||
* Body: { username, password }
|
* Body: { username, password }
|
||||||
* Returns JWT signed with user_profile.id
|
* Returns JWT signed with user_profile.id
|
||||||
*/
|
*/
|
||||||
app.post('/api/signin', (req, res) => {
|
app.post('/api/signin', async (req, res) => {
|
||||||
const { username, password } = req.body;
|
const { username, password } = req.body;
|
||||||
if (!username || !password) {
|
if (!username || !password) {
|
||||||
return res
|
return res
|
||||||
@ -227,25 +227,28 @@ app.post('/api/signin', (req, res) => {
|
|||||||
.json({ error: 'Both username and password are required' });
|
.json({ error: 'Both username and password are required' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SELECT only the columns you actually have:
|
||||||
|
// 'ua.id' is user_auth's primary key,
|
||||||
|
// 'ua.user_id' references user_profile.id,
|
||||||
|
// and we alias user_profile.id as profileId for clarity.
|
||||||
const query = `
|
const query = `
|
||||||
SELECT
|
SELECT
|
||||||
user_auth.id,
|
ua.id AS authId,
|
||||||
user_auth.hashed_password,
|
ua.user_id AS userProfileId,
|
||||||
user_profile.firstname,
|
ua.hashed_password,
|
||||||
user_profile.lastname,
|
up.firstname,
|
||||||
user_profile.email,
|
up.lastname,
|
||||||
user_profile.zipcode,
|
up.email,
|
||||||
user_profile.state,
|
up.zipcode,
|
||||||
user_profile.area,
|
up.state,
|
||||||
user_profile.is_premium,
|
up.area,
|
||||||
user_profile.is_pro_premium,
|
up.career_situation
|
||||||
user_profile.career_situation,
|
FROM user_auth ua
|
||||||
user_profile.career_priorities,
|
LEFT JOIN user_profile up
|
||||||
user_profile.career_list
|
ON ua.user_id = up.id
|
||||||
FROM user_auth
|
WHERE ua.username = ?
|
||||||
LEFT JOIN user_profile ON user_auth.id = user_profile.id
|
|
||||||
WHERE user_auth.username = ?
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
pool.query(query, [username], async (err, results) => {
|
pool.query(query, [username], async (err, results) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
console.error('Error querying user_auth:', err.message);
|
console.error('Error querying user_auth:', err.message);
|
||||||
@ -259,22 +262,26 @@ app.post('/api/signin', (req, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const row = results[0];
|
const row = results[0];
|
||||||
// Compare password
|
|
||||||
|
// Compare password with bcrypt
|
||||||
const isMatch = await bcrypt.compare(password, row.hashed_password);
|
const isMatch = await bcrypt.compare(password, row.hashed_password);
|
||||||
if (!isMatch) {
|
if (!isMatch) {
|
||||||
return res.status(401).json({ error: 'Invalid username or password' });
|
return res.status(401).json({ error: 'Invalid username or password' });
|
||||||
}
|
}
|
||||||
|
|
||||||
// The user_profile id is stored in user_auth.id
|
// IMPORTANT: Use 'row.userProfileId' (from user_profile.id) in the token
|
||||||
const token = jwt.sign({ id: row.id }, SECRET_KEY, {
|
// so your '/api/user-profile' can decode it and do SELECT * FROM user_profile WHERE id=?
|
||||||
|
const token = jwt.sign({ id: row.userProfileId }, SECRET_KEY, {
|
||||||
expiresIn: '2h',
|
expiresIn: '2h',
|
||||||
});
|
});
|
||||||
|
|
||||||
// Return the user info + token
|
// Return user info + token
|
||||||
|
// 'authId' is user_auth's PK, but typically you won't need it on the client
|
||||||
|
// 'row.userProfileId' is the actual user_profile.id
|
||||||
res.status(200).json({
|
res.status(200).json({
|
||||||
message: 'Login successful',
|
message: 'Login successful',
|
||||||
token,
|
token,
|
||||||
id: row.id, // The user_profile.id
|
id: row.userProfileId, // This is user_profile.id (important if your frontend needs it)
|
||||||
user: {
|
user: {
|
||||||
firstname: row.firstname,
|
firstname: row.firstname,
|
||||||
lastname: row.lastname,
|
lastname: row.lastname,
|
||||||
@ -282,16 +289,14 @@ app.post('/api/signin', (req, res) => {
|
|||||||
zipcode: row.zipcode,
|
zipcode: row.zipcode,
|
||||||
state: row.state,
|
state: row.state,
|
||||||
area: row.area,
|
area: row.area,
|
||||||
is_premium: row.is_premium,
|
|
||||||
is_pro_premium: row.is_pro_premium,
|
|
||||||
career_situation: row.career_situation,
|
career_situation: row.career_situation,
|
||||||
career_priorities: row.career_priorities,
|
|
||||||
career_list: row.career_list,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/* ------------------------------------------------------------------
|
/* ------------------------------------------------------------------
|
||||||
CHECK USERNAME (MySQL)
|
CHECK USERNAME (MySQL)
|
||||||
------------------------------------------------------------------ */
|
------------------------------------------------------------------ */
|
||||||
|
129
src/components/ExpensesWizard.js
Normal file
129
src/components/ExpensesWizard.js
Normal file
@ -0,0 +1,129 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { Button } from './ui/button.js';
|
||||||
|
|
||||||
|
|
||||||
|
function ExpensesWizard({ onClose, onExpensesCalculated }) {
|
||||||
|
const [housing, setHousing] = useState('');
|
||||||
|
const [utilities, setUtilities] = useState('');
|
||||||
|
const [groceries, setGroceries] = useState('');
|
||||||
|
const [transportation, setTransportation] = useState('');
|
||||||
|
const [insurance, setInsurance] = useState('');
|
||||||
|
const [misc, setMisc] = useState('');
|
||||||
|
|
||||||
|
const calculateTotal = () => {
|
||||||
|
const sum =
|
||||||
|
(parseFloat(housing) || 0) +
|
||||||
|
(parseFloat(utilities) || 0) +
|
||||||
|
(parseFloat(groceries) || 0) +
|
||||||
|
(parseFloat(transportation) || 0) +
|
||||||
|
(parseFloat(insurance) || 0) +
|
||||||
|
(parseFloat(misc) || 0);
|
||||||
|
|
||||||
|
return sum;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFinish = () => {
|
||||||
|
const total = calculateTotal();
|
||||||
|
onExpensesCalculated(total);
|
||||||
|
onClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
const totalExpenses = calculateTotal();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-4 bg-white rounded shadow-md">
|
||||||
|
<h2 className="text-xl font-semibold mb-4">Monthly Expenses Wizard</h2>
|
||||||
|
<p className="text-sm mb-4">
|
||||||
|
Enter approximate amounts for each category below. We'll sum them up to estimate
|
||||||
|
your monthly expenses.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="mb-2">
|
||||||
|
<label className="block text-sm font-medium">
|
||||||
|
Housing (Rent/Mortgage)
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
className="border p-2 rounded w-full"
|
||||||
|
value={housing}
|
||||||
|
onChange={(e) => setHousing(e.target.value)}
|
||||||
|
placeholder="e.g. 1500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mb-2">
|
||||||
|
<label className="block text-sm font-medium">Utilities</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
className="border p-2 rounded w-full"
|
||||||
|
value={utilities}
|
||||||
|
onChange={(e) => setUtilities(e.target.value)}
|
||||||
|
placeholder="Water, electricity, gas, etc. (e.g. 200)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mb-2">
|
||||||
|
<label className="block text-sm font-medium">Food</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
className="border p-2 rounded w-full"
|
||||||
|
value={groceries}
|
||||||
|
onChange={(e) => setGroceries(e.target.value)}
|
||||||
|
placeholder="Groceries, dining out, etc. (e.g. 300)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mb-2">
|
||||||
|
<label className="block text-sm font-medium">Transportation</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
className="border p-2 rounded w-full"
|
||||||
|
value={transportation}
|
||||||
|
onChange={(e) => setTransportation(e.target.value)}
|
||||||
|
placeholder="Car payment, gas, train, bus, uber fare e.g. 500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mb-2">
|
||||||
|
<label className="block text-sm font-medium">Insurance</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
className="border p-2 rounded w-full"
|
||||||
|
value={insurance}
|
||||||
|
onChange={(e) => setInsurance(e.target.value)}
|
||||||
|
placeholder="Car, house, rental, health insurance not deducted from paycheck (e.g. 200)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mb-2">
|
||||||
|
<label className="block text-sm font-medium">Miscellaneous</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
className="border p-2 rounded w-full"
|
||||||
|
value={misc}
|
||||||
|
onChange={(e) => setMisc(e.target.value)}
|
||||||
|
placeholder="Subscriptions, Phone, any recurring cost not covered elsewhere e.g. 250"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Show the user the current total */}
|
||||||
|
<p className="text-sm mb-4">
|
||||||
|
<strong>Current Total:</strong> ${totalExpenses.toFixed(2)}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="mt-4 flex justify-between">
|
||||||
|
<Button
|
||||||
|
className="bg-gray-200 text-gray-800 hover:bg-gray-300"
|
||||||
|
onClick={onClose}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button className="bg-blue-600 hover:bg-blue-700" onClick={handleFinish}>
|
||||||
|
Use This Total
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ExpensesWizard;
|
@ -475,7 +475,7 @@ function CollegeOnboarding({ nextStep, prevStep, data, setData, careerProfileId
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setShowAidWizard(true)}
|
onClick={() => setShowAidWizard(true)}
|
||||||
className="bg-gray-200 px-3 py-2 rounded"
|
className="bg-blue-600 text-center px-3 py-2 rounded"
|
||||||
>
|
>
|
||||||
Need Help?
|
Need Help?
|
||||||
</button>
|
</button>
|
||||||
|
@ -1,7 +1,10 @@
|
|||||||
// FinancialOnboarding.js
|
// FinancialOnboarding.js
|
||||||
import React from 'react';
|
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
|
||||||
|
|
||||||
const FinancialOnboarding = ({ nextStep, prevStep, data, setData, isEditMode = false }) => {
|
const FinancialOnboarding = ({ nextStep, prevStep, data, setData }) => {
|
||||||
const {
|
const {
|
||||||
currently_working = '',
|
currently_working = '',
|
||||||
current_salary = 0,
|
current_salary = 0,
|
||||||
@ -14,19 +17,37 @@ const FinancialOnboarding = ({ nextStep, prevStep, data, setData, isEditMode = f
|
|||||||
emergency_contribution = 0,
|
emergency_contribution = 0,
|
||||||
extra_cash_emergency_pct = "",
|
extra_cash_emergency_pct = "",
|
||||||
extra_cash_retirement_pct = "",
|
extra_cash_retirement_pct = "",
|
||||||
planned_monthly_expenses = '',
|
|
||||||
planned_monthly_debt_payments = '',
|
|
||||||
planned_monthly_retirement_contribution = '',
|
|
||||||
planned_monthly_emergency_contribution = '',
|
|
||||||
planned_surplus_emergency_pct = '',
|
|
||||||
planned_surplus_retirement_pct = '',
|
|
||||||
planned_additional_income = ''
|
|
||||||
} = data;
|
} = data;
|
||||||
|
|
||||||
const handleChange = (e) => {
|
const [showExpensesWizard, setShowExpensesWizard] = useState(false);
|
||||||
const { name, value } = e.target;
|
|
||||||
let val = parseFloat(value) || 0;
|
|
||||||
|
|
||||||
|
const handleNeedHelpExpenses = () => {
|
||||||
|
setShowExpensesWizard(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleExpensesCalculated = (total) => {
|
||||||
|
setData(prev => ({
|
||||||
|
...prev,
|
||||||
|
monthly_expenses: total
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
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') {
|
if (name === 'extra_cash_emergency_pct') {
|
||||||
val = Math.min(Math.max(val, 0), 100);
|
val = Math.min(Math.max(val, 0), 100);
|
||||||
setData(prevData => ({
|
setData(prevData => ({
|
||||||
@ -46,6 +67,11 @@ const FinancialOnboarding = ({ nextStep, prevStep, data, setData, isEditMode = f
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleSubmit = () => {
|
||||||
|
// Move to next step
|
||||||
|
nextStep();
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="max-w-md mx-auto p-6 space-y-6">
|
<div className="max-w-md mx-auto p-6 space-y-6">
|
||||||
<h2 className="text-2xl font-semibold">Financial Details</h2>
|
<h2 className="text-2xl font-semibold">Financial Details</h2>
|
||||||
@ -53,11 +79,13 @@ const FinancialOnboarding = ({ nextStep, prevStep, data, setData, isEditMode = f
|
|||||||
{currently_working === 'yes' && (
|
{currently_working === 'yes' && (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div>
|
<div>
|
||||||
<label className="block font-medium">Current Annual Salary</label>
|
<label className="block font-medium">Current Annual Salary
|
||||||
|
{infoIcon("Gross annual salary before taxes")}
|
||||||
|
</label>
|
||||||
<input
|
<input
|
||||||
name="current_salary"
|
name="current_salary"
|
||||||
type="number"
|
type="number"
|
||||||
placeholder="Current Annual Salary"
|
placeholder="e.g. 110000"
|
||||||
value={current_salary || ''}
|
value={current_salary || ''}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
className="w-full border rounded p-2"
|
className="w-full border rounded p-2"
|
||||||
@ -65,11 +93,13 @@ const FinancialOnboarding = ({ nextStep, prevStep, data, setData, isEditMode = f
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block font-medium">Additional Annual Income</label>
|
<label className="block font-medium">Additional Annual Income (optional)
|
||||||
|
{infoIcon("Yearly bonuses, investment returns, side jobs, etc.")}
|
||||||
|
</label>
|
||||||
<input
|
<input
|
||||||
name="additional_income"
|
name="additional_income"
|
||||||
type="number"
|
type="number"
|
||||||
placeholder="Investments, side jobs, etc. (optional)"
|
placeholder="e.g. 2400"
|
||||||
value={additional_income || ''}
|
value={additional_income || ''}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
className="w-full border rounded p-2"
|
className="w-full border rounded p-2"
|
||||||
@ -79,24 +109,34 @@ const FinancialOnboarding = ({ nextStep, prevStep, data, setData, isEditMode = f
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div>
|
<label className="block font-medium"> Monthly Expenses
|
||||||
<label className="block font-medium">Monthly Expenses</label>
|
{infoIcon("The total amount you spend on rent, utilities, groceries, etc.")}
|
||||||
|
</label>
|
||||||
|
<div className="flex space-x-2 items-center">
|
||||||
<input
|
<input
|
||||||
name="monthly_expenses"
|
|
||||||
type="number"
|
type="number"
|
||||||
placeholder="Monthly Expenses"
|
|
||||||
value={monthly_expenses || ''}
|
|
||||||
onChange={handleChange}
|
|
||||||
className="w-full border rounded p-2"
|
className="w-full border rounded p-2"
|
||||||
|
name="monthly_expenses"
|
||||||
|
value={monthly_expenses}
|
||||||
|
onChange={handleChange}
|
||||||
|
placeholder="e.g. 1500"
|
||||||
/>
|
/>
|
||||||
|
<Button
|
||||||
|
className="bg-blue-600 text-center px-3 py-2 rounded"
|
||||||
|
onClick={handleNeedHelpExpenses}
|
||||||
|
>
|
||||||
|
Need Help?
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block font-medium">Monthly Debt Payments (optional)</label>
|
<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
|
<input
|
||||||
name="monthly_debt_payments"
|
name="monthly_debt_payments"
|
||||||
type="number"
|
type="number"
|
||||||
placeholder="Monthly Debt Payments"
|
placeholder="e.g. 500"
|
||||||
value={monthly_debt_payments || ''}
|
value={monthly_debt_payments || ''}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
className="w-full border rounded p-2"
|
className="w-full border rounded p-2"
|
||||||
@ -104,11 +144,13 @@ const FinancialOnboarding = ({ nextStep, prevStep, data, setData, isEditMode = f
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block font-medium">Retirement Savings</label>
|
<label className="block font-medium">Retirement Savings (optional)
|
||||||
|
{infoIcon("Current Retirement Balance")}
|
||||||
|
</label>
|
||||||
<input
|
<input
|
||||||
name="retirement_savings"
|
name="retirement_savings"
|
||||||
type="number"
|
type="number"
|
||||||
placeholder="Retirement Savings"
|
placeholder="e.g. 50000"
|
||||||
value={retirement_savings || ''}
|
value={retirement_savings || ''}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
className="w-full border rounded p-2"
|
className="w-full border rounded p-2"
|
||||||
@ -116,11 +158,13 @@ const FinancialOnboarding = ({ nextStep, prevStep, data, setData, isEditMode = f
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block font-medium">Monthly Retirement Contribution</label>
|
<label className="block font-medium">Monthly Retirement Contribution (optional)
|
||||||
|
{infoIcon("Dollar value (not percentage) of monthly retirement contribution")}
|
||||||
|
</label>
|
||||||
<input
|
<input
|
||||||
name="retirement_contribution"
|
name="retirement_contribution"
|
||||||
type="number"
|
type="number"
|
||||||
placeholder="Monthly Retirement Contribution"
|
placeholder="e.g. 300"
|
||||||
value={retirement_contribution || ''}
|
value={retirement_contribution || ''}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
className="w-full border rounded p-2"
|
className="w-full border rounded p-2"
|
||||||
@ -128,11 +172,13 @@ const FinancialOnboarding = ({ nextStep, prevStep, data, setData, isEditMode = f
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block font-medium">Emergency Fund Savings</label>
|
<label className="block font-medium">Emergency Fund Savings (optional)
|
||||||
|
{infoIcon("Balance of your emergency fund for job loss, medical emergencies, etc.")}
|
||||||
|
</label>
|
||||||
<input
|
<input
|
||||||
name="emergency_fund"
|
name="emergency_fund"
|
||||||
type="number"
|
type="number"
|
||||||
placeholder="Emergency Fund Savings"
|
placeholder="e.g. 10000"
|
||||||
value={emergency_fund || ''}
|
value={emergency_fund || ''}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
className="w-full border rounded p-2"
|
className="w-full border rounded p-2"
|
||||||
@ -140,11 +186,13 @@ const FinancialOnboarding = ({ nextStep, prevStep, data, setData, isEditMode = f
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block font-medium">Monthly Emergency Fund Contribution</label>
|
<label className="block font-medium">Monthly Emergency Savings Contribution (optional)
|
||||||
|
{infoIcon("Dollar value (not percentage) of monthly emergency savings contribution")}
|
||||||
|
</label>
|
||||||
<input
|
<input
|
||||||
name="emergency_contribution"
|
name="emergency_contribution"
|
||||||
type="number"
|
type="number"
|
||||||
placeholder="Monthly Emergency Fund Contribution (optional)"
|
placeholder="e.g. 300"
|
||||||
value={emergency_contribution || ''}
|
value={emergency_contribution || ''}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
className="w-full border rounded p-2"
|
className="w-full border rounded p-2"
|
||||||
@ -184,94 +232,6 @@ const FinancialOnboarding = ({ nextStep, prevStep, data, setData, isEditMode = f
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Only show the planned overrides if isEditMode is true */}
|
|
||||||
{isEditMode && (
|
|
||||||
<div className="space-y-4">
|
|
||||||
<hr className="my-4" />
|
|
||||||
<h2 className="text-xl font-medium">Planned Scenario Overrides</h2>
|
|
||||||
<p className="text-gray-600">
|
|
||||||
These fields let you override your real finances for this scenario.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block font-medium">Planned Monthly Expenses</label>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
name="planned_monthly_expenses"
|
|
||||||
value={planned_monthly_expenses}
|
|
||||||
onChange={handleChange}
|
|
||||||
className="w-full border rounded p-2"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block font-medium">Planned Monthly Debt Payments</label>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
name="planned_monthly_debt_payments"
|
|
||||||
value={planned_monthly_debt_payments}
|
|
||||||
onChange={handleChange}
|
|
||||||
className="w-full border rounded p-2"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block font-medium">Planned Monthly Retirement Contribution</label>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
name="planned_monthly_retirement_contribution"
|
|
||||||
value={planned_monthly_retirement_contribution}
|
|
||||||
onChange={handleChange}
|
|
||||||
className="w-full border rounded p-2"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block font-medium">Planned Monthly Emergency Contribution</label>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
name="planned_monthly_emergency_contribution"
|
|
||||||
value={planned_monthly_emergency_contribution}
|
|
||||||
onChange={handleChange}
|
|
||||||
className="w-full border rounded p-2"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block font-medium">Planned Surplus % to Emergency</label>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
name="planned_surplus_emergency_pct"
|
|
||||||
value={planned_surplus_emergency_pct}
|
|
||||||
onChange={handleChange}
|
|
||||||
className="w-full border rounded p-2"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block font-medium">Planned Surplus % to Retirement</label>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
name="planned_surplus_retirement_pct"
|
|
||||||
value={planned_surplus_retirement_pct}
|
|
||||||
onChange={handleChange}
|
|
||||||
className="w-full border rounded p-2"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block font-medium">Planned Additional Annual Income</label>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
name="planned_additional_income"
|
|
||||||
value={planned_additional_income}
|
|
||||||
onChange={handleChange}
|
|
||||||
className="w-full border rounded p-2"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="flex justify-between pt-4">
|
<div className="flex justify-between pt-4">
|
||||||
<button
|
<button
|
||||||
onClick={prevStep}
|
onClick={prevStep}
|
||||||
@ -285,6 +245,16 @@ const FinancialOnboarding = ({ nextStep, prevStep, data, setData, isEditMode = f
|
|||||||
>
|
>
|
||||||
Next: College →
|
Next: College →
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
{showExpensesWizard && (
|
||||||
|
<Modal onClose={() => setShowExpensesWizard(false)}>
|
||||||
|
<ExpensesWizard
|
||||||
|
onClose={() => setShowExpensesWizard(false)}
|
||||||
|
onExpensesCalculated={handleExpensesCalculated}
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
)}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
// OnboardingContainer.js
|
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import PremiumWelcome from './PremiumWelcome.js';
|
import PremiumWelcome from './PremiumWelcome.js';
|
||||||
@ -12,41 +11,62 @@ import authFetch from '../../utils/authFetch.js';
|
|||||||
const OnboardingContainer = () => {
|
const OnboardingContainer = () => {
|
||||||
console.log('OnboardingContainer MOUNT');
|
console.log('OnboardingContainer MOUNT');
|
||||||
|
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
// 1. Local state for multi-step onboarding
|
||||||
const [step, setStep] = useState(0);
|
const [step, setStep] = useState(0);
|
||||||
const [careerData, setCareerData] = useState({});
|
const [careerData, setCareerData] = useState({});
|
||||||
const [financialData, setFinancialData] = useState({});
|
const [financialData, setFinancialData] = useState({});
|
||||||
const [collegeData, setCollegeData] = useState({});
|
const [collegeData, setCollegeData] = useState({});
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
const nextStep = () => setStep(step + 1);
|
// 2. On mount, check if localStorage has onboarding data
|
||||||
const prevStep = () => setStep(step - 1);
|
useEffect(() => {
|
||||||
|
const stored = localStorage.getItem('premiumOnboardingState');
|
||||||
|
if (stored) {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(stored);
|
||||||
|
// Restore step and data if they exist
|
||||||
|
if (parsed.step !== undefined) setStep(parsed.step);
|
||||||
|
if (parsed.careerData) setCareerData(parsed.careerData);
|
||||||
|
if (parsed.financialData) setFinancialData(parsed.financialData);
|
||||||
|
if (parsed.collegeData) setCollegeData(parsed.collegeData);
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('Failed to parse premiumOnboardingState:', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 3. Whenever any key pieces of state change, save to localStorage
|
||||||
|
useEffect(() => {
|
||||||
|
const stateToStore = {
|
||||||
|
step,
|
||||||
|
careerData,
|
||||||
|
financialData,
|
||||||
|
collegeData
|
||||||
|
};
|
||||||
|
localStorage.setItem('premiumOnboardingState', JSON.stringify(stateToStore));
|
||||||
|
}, [step, careerData, financialData, collegeData]);
|
||||||
|
|
||||||
|
// Move user to next or previous step
|
||||||
|
const nextStep = () => setStep((prev) => prev + 1);
|
||||||
|
const prevStep = () => setStep((prev) => prev - 1);
|
||||||
|
|
||||||
function parseFloatOrNull(value) {
|
function parseFloatOrNull(value) {
|
||||||
// If user left it blank ("" or undefined), treat it as NULL.
|
if (value == null || value === '') {
|
||||||
if (value == null || value === '') {
|
return null;
|
||||||
return null;
|
}
|
||||||
|
const parsed = parseFloat(value);
|
||||||
|
return isNaN(parsed) ? null : parsed;
|
||||||
}
|
}
|
||||||
const parsed = parseFloat(value);
|
|
||||||
// If parseFloat can't parse, also return null
|
|
||||||
return isNaN(parsed) ? null : parsed;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('Final collegeData in OnboardingContainer:', collegeData);
|
console.log('Final collegeData in OnboardingContainer:', collegeData);
|
||||||
|
|
||||||
// Final “all done” submission when user finishes the last step
|
// 4. Final “all done” submission
|
||||||
const handleFinalSubmit = async () => {
|
const handleFinalSubmit = async () => {
|
||||||
try {
|
try {
|
||||||
// Build a scenarioPayload that includes optional planned_* fields:
|
|
||||||
const scenarioPayload = {
|
const scenarioPayload = {
|
||||||
...careerData,
|
...careerData,
|
||||||
planned_monthly_expenses: parseFloatOrNull(careerData.planned_monthly_expenses),
|
};
|
||||||
planned_monthly_debt_payments: parseFloatOrNull(careerData.planned_monthly_debt_payments),
|
|
||||||
planned_monthly_retirement_contribution: parseFloatOrNull(careerData.planned_monthly_retirement_contribution),
|
|
||||||
planned_monthly_emergency_contribution: parseFloatOrNull(careerData.planned_monthly_emergency_contribution),
|
|
||||||
planned_surplus_emergency_pct: parseFloatOrNull(careerData.planned_surplus_emergency_pct),
|
|
||||||
planned_surplus_retirement_pct: parseFloatOrNull(careerData.planned_surplus_retirement_pct),
|
|
||||||
planned_additional_income: parseFloatOrNull(careerData.planned_additional_income),
|
|
||||||
};
|
|
||||||
|
|
||||||
// 1) POST career-profile (scenario)
|
// 1) POST career-profile (scenario)
|
||||||
const careerRes = await authFetch('/api/premium/career-profile', {
|
const careerRes = await authFetch('/api/premium/career-profile', {
|
||||||
@ -56,7 +76,7 @@ const OnboardingContainer = () => {
|
|||||||
});
|
});
|
||||||
if (!careerRes.ok) throw new Error('Failed to save career profile');
|
if (!careerRes.ok) throw new Error('Failed to save career profile');
|
||||||
const careerJson = await careerRes.json();
|
const careerJson = await careerRes.json();
|
||||||
const { career_profile_id } = careerJson; // <-- Renamed from career_profile_id
|
const { career_profile_id } = careerJson;
|
||||||
if (!career_profile_id) {
|
if (!career_profile_id) {
|
||||||
throw new Error('No career_profile_id returned by server');
|
throw new Error('No career_profile_id returned by server');
|
||||||
}
|
}
|
||||||
@ -69,34 +89,38 @@ const OnboardingContainer = () => {
|
|||||||
});
|
});
|
||||||
if (!financialRes.ok) throw new Error('Failed to save financial profile');
|
if (!financialRes.ok) throw new Error('Failed to save financial profile');
|
||||||
|
|
||||||
// 3) Only do college-profile if user is "currently_enrolled" or "prospective_student"
|
// 3) Possibly POST college-profile
|
||||||
if (
|
if (
|
||||||
careerData.college_enrollment_status === 'currently_enrolled' ||
|
careerData.college_enrollment_status === 'currently_enrolled' ||
|
||||||
careerData.college_enrollment_status === 'prospective_student'
|
careerData.college_enrollment_status === 'prospective_student'
|
||||||
) {
|
) {
|
||||||
const mergedCollege = {
|
const mergedCollege = {
|
||||||
...collegeData,
|
...collegeData,
|
||||||
career_profile_id,
|
career_profile_id,
|
||||||
college_enrollment_status: careerData.college_enrollment_status,
|
college_enrollment_status: careerData.college_enrollment_status,
|
||||||
};
|
};
|
||||||
const collegeRes = await authFetch('/api/premium/college-profile', {
|
const collegeRes = await authFetch('/api/premium/college-profile', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify(mergedCollege),
|
body: JSON.stringify(mergedCollege),
|
||||||
});
|
});
|
||||||
if (!collegeRes.ok) throw new Error('Failed to save college profile');
|
if (!collegeRes.ok) throw new Error('Failed to save college profile');
|
||||||
} else {
|
} else {
|
||||||
console.log('Skipping college-profile upsert because user is not enrolled/planning.');
|
console.log('Skipping college-profile upsert because user is not enrolled/planning.');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Done => navigate
|
// 4) Clear localStorage so next onboarding starts fresh (optional)
|
||||||
navigate('/milestone-tracker');
|
localStorage.removeItem('premiumOnboardingState');
|
||||||
|
|
||||||
|
// 5) Navigate away
|
||||||
|
navigate('/milestone-tracker');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
// (optionally show error to user)
|
// Optionally show error to user
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 5. Array of steps
|
||||||
const onboardingSteps = [
|
const onboardingSteps = [
|
||||||
<PremiumWelcome nextStep={nextStep} />,
|
<PremiumWelcome nextStep={nextStep} />,
|
||||||
|
|
||||||
@ -121,7 +145,6 @@ const OnboardingContainer = () => {
|
|||||||
nextStep={nextStep}
|
nextStep={nextStep}
|
||||||
data={{
|
data={{
|
||||||
...collegeData,
|
...collegeData,
|
||||||
// keep enrollment status from careerData if relevant:
|
|
||||||
college_enrollment_status: careerData.college_enrollment_status,
|
college_enrollment_status: careerData.college_enrollment_status,
|
||||||
}}
|
}}
|
||||||
setData={setCollegeData}
|
setData={setCollegeData}
|
||||||
|
@ -87,19 +87,26 @@ function ReviewPage({
|
|||||||
|
|
||||||
{/* --- COLLEGE SECTION --- */}
|
{/* --- COLLEGE SECTION --- */}
|
||||||
{inOrPlanningCollege && (
|
{inOrPlanningCollege && (
|
||||||
<div className="p-4 border rounded-md space-y-2">
|
<div className="p-4 border rounded-md space-y-2">
|
||||||
<h3 className="text-xl font-semibold">College Info</h3>
|
<h3 className="text-xl font-semibold">College Info</h3>
|
||||||
<div><strong>College Name:</strong> {collegeData.college_name || 'N/A'}</div>
|
|
||||||
<div><strong>Major:</strong> {collegeData.major || 'N/A'}</div>
|
<div><strong>College Name</strong></div>
|
||||||
{/* If you have these fields, show them if they're meaningful */}
|
<div><strong>Major</strong></div>
|
||||||
{collegeData.tuition != null && (
|
<div><strong>Program Type</strong></div>
|
||||||
<div><strong>Tuition (calculated):</strong> {formatNum(collegeData.tuition)}</div>
|
<div><strong>Tuition (calculated)</strong></div>
|
||||||
)}
|
<div><strong>Program Length (years)</strong></div>
|
||||||
{collegeData.program_length != null && (
|
<div><strong>Credit Hours Per Year</strong></div>
|
||||||
<div><strong>Program Length (years):</strong> {formatNum(collegeData.program_length)}</div>
|
<div><strong>Credit Hours Required</strong></div>
|
||||||
)}
|
<div><strong>Hours Completed</strong></div>
|
||||||
</div>
|
<div><strong>Is In State?</strong></div>
|
||||||
)}
|
<div><strong>Loan Deferral Until Graduation?</strong></div>
|
||||||
|
<div><strong>Annual Financial Aid</strong></div>
|
||||||
|
<div><strong>Existing College Debt</strong></div>
|
||||||
|
<div><strong>Extra Monthly Payment</strong></div>
|
||||||
|
<div><strong>Expected Graduation</strong></div>
|
||||||
|
<div><strong>Expected Salary</strong></div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* --- ACTION BUTTONS --- */}
|
{/* --- ACTION BUTTONS --- */}
|
||||||
<div className="flex justify-between pt-4">
|
<div className="flex justify-between pt-4">
|
||||||
|
Loading…
Reference in New Issue
Block a user