Fixed jwt token/user_auth.user_id vs. user_profile.id. College/financial/Premium Onboarding calls, ReviewPage info.
This commit is contained in:
parent
4cb0662119
commit
2602c7f851
@ -180,8 +180,8 @@ app.post('/api/register', async (req, res) => {
|
||||
|
||||
// 2) Insert into user_auth, referencing user_profile.id
|
||||
const authQuery = `
|
||||
INSERT INTO user_auth (id, username, hashed_password)
|
||||
VALUES (?, ?, ?)
|
||||
INSERT INTO user_auth (user_id, username, hashed_password)
|
||||
VALUES (?, ?, ?)
|
||||
`;
|
||||
pool.query(
|
||||
authQuery,
|
||||
@ -219,7 +219,7 @@ app.post('/api/register', async (req, res) => {
|
||||
* Body: { username, password }
|
||||
* 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;
|
||||
if (!username || !password) {
|
||||
return res
|
||||
@ -227,25 +227,28 @@ app.post('/api/signin', (req, res) => {
|
||||
.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 = `
|
||||
SELECT
|
||||
user_auth.id,
|
||||
user_auth.hashed_password,
|
||||
user_profile.firstname,
|
||||
user_profile.lastname,
|
||||
user_profile.email,
|
||||
user_profile.zipcode,
|
||||
user_profile.state,
|
||||
user_profile.area,
|
||||
user_profile.is_premium,
|
||||
user_profile.is_pro_premium,
|
||||
user_profile.career_situation,
|
||||
user_profile.career_priorities,
|
||||
user_profile.career_list
|
||||
FROM user_auth
|
||||
LEFT JOIN user_profile ON user_auth.id = user_profile.id
|
||||
WHERE user_auth.username = ?
|
||||
ua.id AS authId,
|
||||
ua.user_id AS userProfileId,
|
||||
ua.hashed_password,
|
||||
up.firstname,
|
||||
up.lastname,
|
||||
up.email,
|
||||
up.zipcode,
|
||||
up.state,
|
||||
up.area,
|
||||
up.career_situation
|
||||
FROM user_auth ua
|
||||
LEFT JOIN user_profile up
|
||||
ON ua.user_id = up.id
|
||||
WHERE ua.username = ?
|
||||
`;
|
||||
|
||||
pool.query(query, [username], async (err, results) => {
|
||||
if (err) {
|
||||
console.error('Error querying user_auth:', err.message);
|
||||
@ -259,22 +262,26 @@ app.post('/api/signin', (req, res) => {
|
||||
}
|
||||
|
||||
const row = results[0];
|
||||
// Compare password
|
||||
|
||||
// Compare password with bcrypt
|
||||
const isMatch = await bcrypt.compare(password, row.hashed_password);
|
||||
if (!isMatch) {
|
||||
return res.status(401).json({ error: 'Invalid username or password' });
|
||||
}
|
||||
|
||||
// The user_profile id is stored in user_auth.id
|
||||
const token = jwt.sign({ id: row.id }, SECRET_KEY, {
|
||||
// IMPORTANT: Use 'row.userProfileId' (from user_profile.id) in the token
|
||||
// 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',
|
||||
});
|
||||
|
||||
// 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({
|
||||
message: 'Login successful',
|
||||
token,
|
||||
id: row.id, // The user_profile.id
|
||||
id: row.userProfileId, // This is user_profile.id (important if your frontend needs it)
|
||||
user: {
|
||||
firstname: row.firstname,
|
||||
lastname: row.lastname,
|
||||
@ -282,16 +289,14 @@ app.post('/api/signin', (req, res) => {
|
||||
zipcode: row.zipcode,
|
||||
state: row.state,
|
||||
area: row.area,
|
||||
is_premium: row.is_premium,
|
||||
is_pro_premium: row.is_pro_premium,
|
||||
career_situation: row.career_situation,
|
||||
career_priorities: row.career_priorities,
|
||||
career_list: row.career_list,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
||||
/* ------------------------------------------------------------------
|
||||
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
|
||||
type="button"
|
||||
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?
|
||||
</button>
|
||||
|
@ -1,7 +1,10 @@
|
||||
// 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 {
|
||||
currently_working = '',
|
||||
current_salary = 0,
|
||||
@ -14,19 +17,37 @@ const FinancialOnboarding = ({ nextStep, prevStep, data, setData, isEditMode = f
|
||||
emergency_contribution = 0,
|
||||
extra_cash_emergency_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;
|
||||
|
||||
const handleChange = (e) => {
|
||||
const { name, value } = e.target;
|
||||
let val = parseFloat(value) || 0;
|
||||
const [showExpensesWizard, setShowExpensesWizard] = useState(false);
|
||||
|
||||
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') {
|
||||
val = Math.min(Math.max(val, 0), 100);
|
||||
setData(prevData => ({
|
||||
@ -46,6 +67,11 @@ const FinancialOnboarding = ({ nextStep, prevStep, data, setData, isEditMode = f
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = () => {
|
||||
// Move to next step
|
||||
nextStep();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="max-w-md mx-auto p-6 space-y-6">
|
||||
<h2 className="text-2xl font-semibold">Financial Details</h2>
|
||||
@ -53,11 +79,13 @@ const FinancialOnboarding = ({ nextStep, prevStep, data, setData, isEditMode = f
|
||||
{currently_working === 'yes' && (
|
||||
<div className="space-y-4">
|
||||
<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
|
||||
name="current_salary"
|
||||
type="number"
|
||||
placeholder="Current Annual Salary"
|
||||
placeholder="e.g. 110000"
|
||||
value={current_salary || ''}
|
||||
onChange={handleChange}
|
||||
className="w-full border rounded p-2"
|
||||
@ -65,11 +93,13 @@ const FinancialOnboarding = ({ nextStep, prevStep, data, setData, isEditMode = f
|
||||
</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
|
||||
name="additional_income"
|
||||
type="number"
|
||||
placeholder="Investments, side jobs, etc. (optional)"
|
||||
placeholder="e.g. 2400"
|
||||
value={additional_income || ''}
|
||||
onChange={handleChange}
|
||||
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>
|
||||
<label className="block font-medium">Monthly Expenses</label>
|
||||
<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
|
||||
name="monthly_expenses"
|
||||
type="number"
|
||||
placeholder="Monthly Expenses"
|
||||
value={monthly_expenses || ''}
|
||||
onChange={handleChange}
|
||||
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>
|
||||
<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
|
||||
name="monthly_debt_payments"
|
||||
type="number"
|
||||
placeholder="Monthly Debt Payments"
|
||||
placeholder="e.g. 500"
|
||||
value={monthly_debt_payments || ''}
|
||||
onChange={handleChange}
|
||||
className="w-full border rounded p-2"
|
||||
@ -104,11 +144,13 @@ const FinancialOnboarding = ({ nextStep, prevStep, data, setData, isEditMode = f
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block font-medium">Retirement Savings</label>
|
||||
<label className="block font-medium">Retirement Savings (optional)
|
||||
{infoIcon("Current Retirement Balance")}
|
||||
</label>
|
||||
<input
|
||||
name="retirement_savings"
|
||||
type="number"
|
||||
placeholder="Retirement Savings"
|
||||
placeholder="e.g. 50000"
|
||||
value={retirement_savings || ''}
|
||||
onChange={handleChange}
|
||||
className="w-full border rounded p-2"
|
||||
@ -116,11 +158,13 @@ const FinancialOnboarding = ({ nextStep, prevStep, data, setData, isEditMode = f
|
||||
</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
|
||||
name="retirement_contribution"
|
||||
type="number"
|
||||
placeholder="Monthly Retirement Contribution"
|
||||
placeholder="e.g. 300"
|
||||
value={retirement_contribution || ''}
|
||||
onChange={handleChange}
|
||||
className="w-full border rounded p-2"
|
||||
@ -128,11 +172,13 @@ const FinancialOnboarding = ({ nextStep, prevStep, data, setData, isEditMode = f
|
||||
</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
|
||||
name="emergency_fund"
|
||||
type="number"
|
||||
placeholder="Emergency Fund Savings"
|
||||
placeholder="e.g. 10000"
|
||||
value={emergency_fund || ''}
|
||||
onChange={handleChange}
|
||||
className="w-full border rounded p-2"
|
||||
@ -140,11 +186,13 @@ const FinancialOnboarding = ({ nextStep, prevStep, data, setData, isEditMode = f
|
||||
</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
|
||||
name="emergency_contribution"
|
||||
type="number"
|
||||
placeholder="Monthly Emergency Fund Contribution (optional)"
|
||||
placeholder="e.g. 300"
|
||||
value={emergency_contribution || ''}
|
||||
onChange={handleChange}
|
||||
className="w-full border rounded p-2"
|
||||
@ -184,94 +232,6 @@ const FinancialOnboarding = ({ nextStep, prevStep, data, setData, isEditMode = f
|
||||
</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">
|
||||
<button
|
||||
onClick={prevStep}
|
||||
@ -285,6 +245,16 @@ const FinancialOnboarding = ({ nextStep, prevStep, data, setData, isEditMode = f
|
||||
>
|
||||
Next: College →
|
||||
</button>
|
||||
|
||||
{showExpensesWizard && (
|
||||
<Modal onClose={() => setShowExpensesWizard(false)}>
|
||||
<ExpensesWizard
|
||||
onClose={() => setShowExpensesWizard(false)}
|
||||
onExpensesCalculated={handleExpensesCalculated}
|
||||
/>
|
||||
</Modal>
|
||||
)}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -1,4 +1,3 @@
|
||||
// OnboardingContainer.js
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import PremiumWelcome from './PremiumWelcome.js';
|
||||
@ -12,41 +11,62 @@ import authFetch from '../../utils/authFetch.js';
|
||||
const OnboardingContainer = () => {
|
||||
console.log('OnboardingContainer MOUNT');
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
// 1. Local state for multi-step onboarding
|
||||
const [step, setStep] = useState(0);
|
||||
const [careerData, setCareerData] = useState({});
|
||||
const [financialData, setFinancialData] = useState({});
|
||||
const [collegeData, setCollegeData] = useState({});
|
||||
const navigate = useNavigate();
|
||||
|
||||
const nextStep = () => setStep(step + 1);
|
||||
const prevStep = () => setStep(step - 1);
|
||||
// 2. On mount, check if localStorage has onboarding data
|
||||
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) {
|
||||
// If user left it blank ("" or undefined), treat it as NULL.
|
||||
if (value == null || value === '') {
|
||||
return null;
|
||||
if (value == null || value === '') {
|
||||
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);
|
||||
|
||||
// Final “all done” submission when user finishes the last step
|
||||
// 4. Final “all done” submission
|
||||
const handleFinalSubmit = async () => {
|
||||
try {
|
||||
// Build a scenarioPayload that includes optional planned_* fields:
|
||||
const scenarioPayload = {
|
||||
...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)
|
||||
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');
|
||||
const careerJson = await careerRes.json();
|
||||
const { career_profile_id } = careerJson; // <-- Renamed from career_profile_id
|
||||
const { career_profile_id } = careerJson;
|
||||
if (!career_profile_id) {
|
||||
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');
|
||||
|
||||
// 3) Only do college-profile if user is "currently_enrolled" or "prospective_student"
|
||||
if (
|
||||
careerData.college_enrollment_status === 'currently_enrolled' ||
|
||||
careerData.college_enrollment_status === 'prospective_student'
|
||||
) {
|
||||
const mergedCollege = {
|
||||
...collegeData,
|
||||
career_profile_id,
|
||||
college_enrollment_status: careerData.college_enrollment_status,
|
||||
};
|
||||
const collegeRes = await authFetch('/api/premium/college-profile', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(mergedCollege),
|
||||
});
|
||||
if (!collegeRes.ok) throw new Error('Failed to save college profile');
|
||||
} else {
|
||||
console.log('Skipping college-profile upsert because user is not enrolled/planning.');
|
||||
}
|
||||
// 3) Possibly POST college-profile
|
||||
if (
|
||||
careerData.college_enrollment_status === 'currently_enrolled' ||
|
||||
careerData.college_enrollment_status === 'prospective_student'
|
||||
) {
|
||||
const mergedCollege = {
|
||||
...collegeData,
|
||||
career_profile_id,
|
||||
college_enrollment_status: careerData.college_enrollment_status,
|
||||
};
|
||||
const collegeRes = await authFetch('/api/premium/college-profile', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(mergedCollege),
|
||||
});
|
||||
if (!collegeRes.ok) throw new Error('Failed to save college profile');
|
||||
} else {
|
||||
console.log('Skipping college-profile upsert because user is not enrolled/planning.');
|
||||
}
|
||||
|
||||
// Done => navigate
|
||||
navigate('/milestone-tracker');
|
||||
// 4) Clear localStorage so next onboarding starts fresh (optional)
|
||||
localStorage.removeItem('premiumOnboardingState');
|
||||
|
||||
// 5) Navigate away
|
||||
navigate('/milestone-tracker');
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
// (optionally show error to user)
|
||||
// Optionally show error to user
|
||||
}
|
||||
};
|
||||
|
||||
// 5. Array of steps
|
||||
const onboardingSteps = [
|
||||
<PremiumWelcome nextStep={nextStep} />,
|
||||
|
||||
@ -121,7 +145,6 @@ const OnboardingContainer = () => {
|
||||
nextStep={nextStep}
|
||||
data={{
|
||||
...collegeData,
|
||||
// keep enrollment status from careerData if relevant:
|
||||
college_enrollment_status: careerData.college_enrollment_status,
|
||||
}}
|
||||
setData={setCollegeData}
|
||||
|
@ -87,19 +87,26 @@ function ReviewPage({
|
||||
|
||||
{/* --- COLLEGE SECTION --- */}
|
||||
{inOrPlanningCollege && (
|
||||
<div className="p-4 border rounded-md space-y-2">
|
||||
<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>
|
||||
{/* If you have these fields, show them if they're meaningful */}
|
||||
{collegeData.tuition != null && (
|
||||
<div><strong>Tuition (calculated):</strong> {formatNum(collegeData.tuition)}</div>
|
||||
)}
|
||||
{collegeData.program_length != null && (
|
||||
<div><strong>Program Length (years):</strong> {formatNum(collegeData.program_length)}</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div className="p-4 border rounded-md space-y-2">
|
||||
<h3 className="text-xl font-semibold">College Info</h3>
|
||||
|
||||
<div><strong>College Name</strong></div>
|
||||
<div><strong>Major</strong></div>
|
||||
<div><strong>Program Type</strong></div>
|
||||
<div><strong>Tuition (calculated)</strong></div>
|
||||
<div><strong>Program Length (years)</strong></div>
|
||||
<div><strong>Credit Hours Per Year</strong></div>
|
||||
<div><strong>Credit Hours Required</strong></div>
|
||||
<div><strong>Hours Completed</strong></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 --- */}
|
||||
<div className="flex justify-between pt-4">
|
||||
|
Loading…
Reference in New Issue
Block a user