diff --git a/backend/server.js b/backend/server.js index 23795be..758d4c2 100755 --- a/backend/server.js +++ b/backend/server.js @@ -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) ------------------------------------------------------------------ */ diff --git a/src/components/ExpensesWizard.js b/src/components/ExpensesWizard.js new file mode 100644 index 0000000..27e6966 --- /dev/null +++ b/src/components/ExpensesWizard.js @@ -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 ( +
+

Monthly Expenses Wizard

+

+ Enter approximate amounts for each category below. We'll sum them up to estimate + your monthly expenses. +

+ +
+ + setHousing(e.target.value)} + placeholder="e.g. 1500" + /> +
+ +
+ + setUtilities(e.target.value)} + placeholder="Water, electricity, gas, etc. (e.g. 200)" + /> +
+ +
+ + setGroceries(e.target.value)} + placeholder="Groceries, dining out, etc. (e.g. 300)" + /> +
+ +
+ + setTransportation(e.target.value)} + placeholder="Car payment, gas, train, bus, uber fare e.g. 500" + /> +
+ +
+ + setInsurance(e.target.value)} + placeholder="Car, house, rental, health insurance not deducted from paycheck (e.g. 200)" + /> +
+ +
+ + setMisc(e.target.value)} + placeholder="Subscriptions, Phone, any recurring cost not covered elsewhere e.g. 250" + /> +
+ +{/* Show the user the current total */} +

+ Current Total: ${totalExpenses.toFixed(2)} +

+ +
+ + +
+
+ ); +} + +export default ExpensesWizard; diff --git a/src/components/PremiumOnboarding/CollegeOnboarding.js b/src/components/PremiumOnboarding/CollegeOnboarding.js index 93771bf..97f2b59 100644 --- a/src/components/PremiumOnboarding/CollegeOnboarding.js +++ b/src/components/PremiumOnboarding/CollegeOnboarding.js @@ -475,7 +475,7 @@ function CollegeOnboarding({ nextStep, prevStep, data, setData, careerProfileId diff --git a/src/components/PremiumOnboarding/FinancialOnboarding.js b/src/components/PremiumOnboarding/FinancialOnboarding.js index 295f8cf..5b2b0ab 100644 --- a/src/components/PremiumOnboarding/FinancialOnboarding.js +++ b/src/components/PremiumOnboarding/FinancialOnboarding.js @@ -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) => ( + + i + + ); + + + 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 (

Financial Details

@@ -53,11 +79,13 @@ const FinancialOnboarding = ({ nextStep, prevStep, data, setData, isEditMode = f {currently_working === 'yes' && (
- +
- + -
- + +
+
- +
- +
- +
- +
- +
- {/* Only show the planned overrides if isEditMode is true */} - {isEditMode && ( -
-
-

Planned Scenario Overrides

-

- These fields let you override your real finances for this scenario. -

- -
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- - -
-
- )} -
+ + {showExpensesWizard && ( + setShowExpensesWizard(false)}> + setShowExpensesWizard(false)} + onExpensesCalculated={handleExpensesCalculated} + /> + + )} +
); diff --git a/src/components/PremiumOnboarding/OnboardingContainer.js b/src/components/PremiumOnboarding/OnboardingContainer.js index 37cb6d1..8b1e061 100644 --- a/src/components/PremiumOnboarding/OnboardingContainer.js +++ b/src/components/PremiumOnboarding/OnboardingContainer.js @@ -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 = [ , @@ -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} diff --git a/src/components/PremiumOnboarding/ReviewPage.js b/src/components/PremiumOnboarding/ReviewPage.js index e7c12d1..c39ad7d 100644 --- a/src/components/PremiumOnboarding/ReviewPage.js +++ b/src/components/PremiumOnboarding/ReviewPage.js @@ -87,19 +87,26 @@ function ReviewPage({ {/* --- COLLEGE SECTION --- */} {inOrPlanningCollege && ( -
-

College Info

-
College Name: {collegeData.college_name || 'N/A'}
-
Major: {collegeData.major || 'N/A'}
- {/* If you have these fields, show them if they're meaningful */} - {collegeData.tuition != null && ( -
Tuition (calculated): {formatNum(collegeData.tuition)}
- )} - {collegeData.program_length != null && ( -
Program Length (years): {formatNum(collegeData.program_length)}
- )} -
- )} +
+

College Info

+ +
College Name
+
Major
+
Program Type
+
Tuition (calculated)
+
Program Length (years)
+
Credit Hours Per Year
+
Credit Hours Required
+
Hours Completed
+
Is In State?
+
Loan Deferral Until Graduation?
+
Annual Financial Aid
+
Existing College Debt
+
Extra Monthly Payment
+
Expected Graduation
+
Expected Salary
+
+)} {/* --- ACTION BUTTONS --- */}