diff --git a/backend/server3.js b/backend/server3.js
index a62a27d..29da2e6 100644
--- a/backend/server3.js
+++ b/backend/server3.js
@@ -193,8 +193,7 @@ app.get('/api/premium/career-profile/latest', authenticatePremiumUser, async (re
const sql = `
SELECT
*,
- DATE_FORMAT(start_date, '%Y-%m-%d') AS start_date,
- DATE_FORMAT(projected_end_date, '%Y-%m-%d') AS projected_end_date
+ DATE_FORMAT(start_date, '%Y-%m-%d') AS start_date
FROM career_profiles
WHERE user_id = ?
ORDER BY start_date DESC
@@ -214,8 +213,7 @@ app.get('/api/premium/career-profile/all', authenticatePremiumUser, async (req,
const sql = `
SELECT
*,
- DATE_FORMAT(start_date, '%Y-%m-%d') AS start_date,
- DATE_FORMAT(projected_end_date, '%Y-%m-%d') AS projected_end_date
+ DATE_FORMAT(start_date, '%Y-%m-%d') AS start_date
FROM career_profiles
WHERE user_id = ?
ORDER BY start_date ASC
@@ -235,8 +233,7 @@ app.get('/api/premium/career-profile/:careerProfileId', authenticatePremiumUser,
const sql = `
SELECT
*,
- DATE_FORMAT(start_date, '%Y-%m-%d') AS start_date,
- DATE_FORMAT(projected_end_date, '%Y-%m-%d') AS projected_end_date
+ DATE_FORMAT(start_date, '%Y-%m-%d') AS start_date
FROM career_profiles
WHERE id = ?
AND user_id = ?
@@ -262,7 +259,6 @@ app.post('/api/premium/career-profile', authenticatePremiumUser, async (req, res
career_name,
status,
start_date,
- projected_end_date,
college_enrollment_status,
currently_working,
career_goals,
@@ -295,7 +291,6 @@ app.post('/api/premium/career-profile', authenticatePremiumUser, async (req, res
career_name,
status,
start_date,
- projected_end_date,
college_enrollment_status,
currently_working,
career_goals,
@@ -309,11 +304,10 @@ app.post('/api/premium/career-profile', authenticatePremiumUser, async (req, res
planned_surplus_retirement_pct,
planned_additional_income
)
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON DUPLICATE KEY UPDATE
status = VALUES(status),
start_date = VALUES(start_date),
- projected_end_date = VALUES(projected_end_date),
college_enrollment_status = VALUES(college_enrollment_status),
currently_working = VALUES(currently_working),
career_goals = VALUES(career_goals),
@@ -336,7 +330,6 @@ app.post('/api/premium/career-profile', authenticatePremiumUser, async (req, res
career_name,
status || 'planned',
start_date || null,
- projected_end_date || null,
college_enrollment_status || null,
currently_working || null,
career_goals || null,
@@ -2453,29 +2446,32 @@ app.delete('/api/premium/milestones/:milestoneId', authenticatePremiumUser, asyn
------------------------------------------------------------------ */
// GET /api/premium/financial-profile
-app.get('/api/premium/financial-profile', auth, (req, res) => {
- const uid = req.userId;
- db.query('SELECT * FROM financial_profile WHERE user_id=?', [uid],
- (err, rows) => {
- if (err) return res.status(500).json({ error:'DB error' });
+app.get('/api/premium/financial-profile', authenticatePremiumUser, async (req, res) => {
+ try {
+ const [rows] = await pool.query(
+ 'SELECT * FROM financial_profiles WHERE user_id=? LIMIT 1',
+ [req.id]
+ );
- if (!rows.length) {
- // ←———— send a benign default instead of 404
- return res.json({
- current_salary: 0,
- additional_income: 0,
- monthly_expenses: 0,
- monthly_debt_payments: 0,
- retirement_savings: 0,
- emergency_fund: 0,
- retirement_contribution: 0,
- emergency_contribution: 0,
- extra_cash_emergency_pct: 50,
- extra_cash_retirement_pct: 50
- });
- }
- res.json(rows[0]);
- });
+ if (!rows.length) {
+ return res.json({
+ current_salary: 0,
+ additional_income: 0,
+ monthly_expenses: 0,
+ monthly_debt_payments: 0,
+ retirement_savings: 0,
+ emergency_fund: 0,
+ retirement_contribution: 0,
+ emergency_contribution: 0,
+ extra_cash_emergency_pct: 50,
+ extra_cash_retirement_pct: 50
+ });
+ }
+ res.json(rows[0]);
+ } catch (err) {
+ console.error('financial‑profile GET error:', err);
+ res.status(500).json({ error: 'DB error' });
+ }
});
app.post('/api/premium/financial-profile', authenticatePremiumUser, async (req, res) => {
@@ -2725,6 +2721,21 @@ app.get('/api/premium/college-profile', authenticatePremiumUser, async (req, res
}
});
+// GET every college profile for the logged‑in user
+app.get('/api/premium/college-profile/all', authenticatePremiumUser, async (req,res)=>{
+ const sql = `
+ SELECT cp.*,
+ DATE_FORMAT(cp.created_at,'%Y-%m-%d') AS created_at,
+ IFNULL(cpr.scenario_title, cpr.career_name) AS career_title
+ FROM college_profiles cp
+ JOIN career_profiles cpr ON cpr.id = cp.career_profile_id
+ WHERE cp.user_id = ?
+ ORDER BY cp.created_at DESC
+ `;
+ const [rows] = await pool.query(sql,[req.id]);
+ res.json({ collegeProfiles: rows });
+});
+
/* ------------------------------------------------------------------
AI-SUGGESTED MILESTONES
------------------------------------------------------------------ */
diff --git a/src/App.js b/src/App.js
index 84ddf58..6ee56d9 100644
--- a/src/App.js
+++ b/src/App.js
@@ -25,6 +25,10 @@ import InterestInventory from './components/InterestInventory.js';
import Dashboard from './components/Dashboard.js';
import UserProfile from './components/UserProfile.js';
import FinancialProfileForm from './components/FinancialProfileForm.js';
+import CareerProfileList from './components/CareerProfileList.js';
+import CareerProfileForm from './components/CareerProfileForm.js';
+import CollegeProfileList from './components/CollegeProfileList.js';
+import CollegeProfileForm from './components/CollegeProfileForm.js';
import CareerRoadmap from './components/CareerRoadmap.js';
import Paywall from './components/Paywall.js';
import OnboardingContainer from './components/PremiumOnboarding/OnboardingContainer.js';
@@ -239,7 +243,7 @@ const uiToolHandlers = useMemo(() => {
{/* Header */}
- AptivaAI - Career Guidance Platform (beta)
+ AptivaAI - Career Guidance Platform
{isAuthenticated && (
@@ -360,7 +364,7 @@ const uiToolHandlers = useMemo(() => {
)}
onClick={() => navigate('/retirement')}
>
- Retirement Planning
+ Retirement Planning (beta)
{!canAccessPremium && (
(Premium)
@@ -406,20 +410,32 @@ const uiToolHandlers = useMemo(() => {
{canAccessPremium ? (
/* Premium users go straight to the wizard */
-
- Premium Onboarding
+ Career Profiles
) : (
- /* Free users are nudged to upgrade */
+
+ Career Profiles (Premium)
+
+ )}
+
+ {/* College Profiles (go straight to list) */}
+ {canAccessPremium ? (
- College Planning (Premium)
+ College Profiles
+ ) : (
+
+ College Profiles (Premium)
+
)}
@@ -533,6 +549,11 @@ const uiToolHandlers = useMemo(() => {
}
/>
+ } />
+ } />
+
+ } />
+ } />
+ setForm(prev => ({ ...prev, [e.target.name]: e.target.value }));
+
+ const handleCareerSelected = obj => {
+ // obj = { title, soc_code, … }
+ setForm(prev => ({
+ ...prev,
+ career_name : obj.title,
+ soc_code : obj.soc_code
+ }));
+ setCareerLocked(true);
+ };
+
+ const unlockCareer = () => {
+ // allow user to re‑pick
+ setCareerLocked(false);
+ setForm(prev => ({ ...prev, career_name: '', soc_code: '' }));
+ };
+
+ /* ---------- 3. load an existing row (edit mode) ---------- */
+ useEffect(() => {
+ if (id === 'new') return;
+ (async () => {
+ const res = await authFetch(`/api/premium/career-profile/${id}`);
+ if (!res.ok) return;
+ const d = await res.json();
+ setForm(prev => ({
+ ...prev,
+ scenario_title : d.scenario_title ?? '',
+ career_name : d.career_name ?? '',
+ soc_code : d.soc_code ?? '',
+ status : d.status ?? 'current',
+ start_date : d.start_date ?? '',
+ retirement_start_date : d.retirement_start_date ?? '',
+ college_enrollment_status : d.college_enrollment_status ?? '',
+ career_goals : d.career_goals ?? '',
+ desired_retirement_income_monthly :
+ d.desired_retirement_income_monthly ?? ''
+ }));
+ })();
+ }, [id]);
+
+ /* ---------- 4. save ---------- */
+ async function save() {
+ if (!form.soc_code) {
+ alert('Please pick a valid career from the list first.');
+ return;
+ }
+ try {
+ const res = await authFetch('/api/premium/career-profile', {
+ method : 'POST',
+ headers : { 'Content-Type': 'application/json' },
+ body : JSON.stringify({
+ ...form,
+ id: id === 'new' ? undefined : id // upsert
+ })
+ });
+ if (!res.ok) throw new Error(await res.text());
+ nav(-1);
+ } catch (err) {
+ console.error(err);
+ alert(err.message);
+ }
+ }
+
+ /* ---------- 5. render ---------- */
+ return (
+
+
+ {id === 'new' ? 'New' : 'Edit'} Career Profile
+
+
+ {/* Scenario title */}
+
+ Scenario Title
+
+
+
+ {/* Career picker (locked vs editable) */}
+
Career *
+ {careerLocked ? (
+
+
+
+ Change
+
+
+ ) : (
+
+ )}
+
+ {/* Status */}
+
+ Status
+
+ current
+ future
+ retired
+
+
+
+ {/* Dates */}
+
+ Start Date
+
+
+
+
+ Retirement Start Date
+
+
+
+ {/* College status */}
+
+ College Enrollment Status
+
+ -- select --
+ Not Applicable
+ Prospective Student
+ Currently Enrolled
+ Completed
+
+
+
+ {/* Career goals */}
+
+ Career Goals
+
+
+
+ {/* Desired retirement income */}
+
+ Desired Retirement Income / Month ($)
+
+
+
+ {/* Action buttons */}
+
+ nav(-1)}
+ className="bg-gray-200 hover:bg-gray-300 text-gray-700 font-semibold py-2 px-4 rounded"
+ >
+ ← Back
+
+
+ Save
+
+
+
+ );
+}
diff --git a/src/components/CareerProfileList.js b/src/components/CareerProfileList.js
new file mode 100644
index 0000000..1647c34
--- /dev/null
+++ b/src/components/CareerProfileList.js
@@ -0,0 +1,79 @@
+import React, { useEffect, useState } from 'react';
+import { Link, useNavigate, useLocation } from 'react-router-dom';
+
+export default function CareerProfileList() {
+ const [rows, setRows] = useState([]);
+ const nav = useNavigate();
+ const token = localStorage.getItem('token');
+
+ useEffect(() => {
+ fetch('/api/premium/career-profile/all', {
+ headers: { Authorization: `Bearer ${token}` }
+ })
+ .then(r => r.json())
+ .then(d => setRows(d.careerProfiles || []));
+ }, [token]);
+
+ async function remove(id) {
+ if (!window.confirm('Delete this career profile?')) return;
+ await fetch(`/api/premium/career-profile/${id}`, {
+ method : 'DELETE',
+ headers: { Authorization: `Bearer ${token}` }
+ });
+ setRows(rows.filter(r => r.id !== id));
+ }
+
+ return (
+
+
Career Profiles
+
+
nav('/profile/careers/new/edit')}
+ className="px-3 py-2 bg-blue-600 text-white rounded"
+ >
+ + New Career Profile
+
+
+
+
+
+ Title
+ Status
+ Start
+
+
+
+
+ {rows.map(r => (
+
+ {r.scenario_title || r.career_name}
+ {r.status}
+ {r.start_date}
+
+
+ edit
+
+ remove(r.id)}
+ className="text-red-600 underline"
+ >
+ delete
+
+
+
+ ))}
+ {rows.length === 0 && (
+
+
+ No career profiles yet
+
+
+ )}
+
+
+
+ );
+}
diff --git a/src/components/CollegeProfileForm.js b/src/components/CollegeProfileForm.js
new file mode 100644
index 0000000..f0e840a
--- /dev/null
+++ b/src/components/CollegeProfileForm.js
@@ -0,0 +1,522 @@
+import React, { useEffect, useState } from 'react';
+import { useNavigate, useParams } from 'react-router-dom';
+import authFetch from '../utils/authFetch.js';
+
+
+/** -----------------------------------------------------------
+ * Ensure numerics are sent as numbers and booleans as 0 / 1
+ * – mirrors the logic you use in OnboardingContainer
+ * ---------------------------------------------------------- */
+const parseFloatOrNull = v => {
+ if (v === '' || v == null) return null;
+ const n = parseFloat(v);
+ return Number.isFinite(n) ? n : null;
+};
+
+function normalisePayload(draft) {
+ const bools = [
+ 'is_in_state','is_in_district','is_online',
+ 'loan_deferral_until_graduation'
+ ];
+ const nums = [
+ 'annual_financial_aid','existing_college_debt','interest_rate','loan_term',
+ 'extra_payment','expected_salary','credit_hours_per_year','hours_completed',
+ 'credit_hours_required','program_length','tuition','tuition_paid'
+ ];
+ const dates = ['enrollment_date', 'expected_graduation'];
+
+ const out = { ...draft };
+ bools.forEach(k => { out[k] = draft[k] ? 1 : 0; });
+ nums .forEach(k => { out[k] = parseFloatOrNull(draft[k]) ?? 0; });
+ dates.forEach(k => { out[k] = toMySqlDate(draft[k]); });
+
+ delete out.created_at;
+ delete out.updated_at;
+
+ return out;
+}
+
+const toMySqlDate = iso => {
+ if (!iso) return null;
+ return iso.replace('T', ' ').slice(0, 19);
+};
+
+export default function CollegeProfileForm() {
+ const { careerId, id } = useParams(); // id optional
+ const nav = useNavigate();
+ const token = localStorage.getItem('token');
+ const [cipRows, setCipRows] = useState([]);
+ const [schoolSug, setSchoolSug] = useState([]);
+ const [progSug, setProgSug] = useState([]);
+ const [types, setTypes] = useState([]);
+ const [ipeds, setIpeds] = useState([]);
+ const [schoolValid, setSchoolValid] = useState(true);
+ const [programValid, setProgramValid] = useState(true);
+
+ const schoolData = cipRows;
+
+ const [form, setForm] = useState({
+ career_profile_id : careerId,
+ selected_school : '',
+ selected_program : '',
+ program_type : '',
+ annual_financial_aid : 0,
+ tuition : 0,
+ interest_rate : 5.5,
+ loan_term : 10
+ });
+
+ const [manualTuition, setManualTuition] = useState(
+ form.tuition ? String(form.tuition) : ''
+ );
+
+ const [autoTuition, setAutoTuition] = useState(0);
+
+ // ---------- handlers (inside component) ----------
+const handleFieldChange = (e) => {
+ const { name, value, type, checked } = e.target;
+ setForm((prev) => {
+ const draft = { ...prev };
+ if (type === 'checkbox') {
+ draft[name] = checked;
+ } else if (
+ [
+ 'interest_rate','loan_term','extra_payment','expected_salary',
+ 'annual_financial_aid','existing_college_debt','credit_hours_per_year',
+ 'hours_completed','credit_hours_required','tuition','tuition_paid',
+ 'program_length'
+ ].includes(name)
+ ) {
+ draft[name] = value === '' ? '' : parseFloat(value);
+ } else {
+ draft[name] = value;
+ }
+ return draft;
+ });
+};
+
+const onSchoolInput = (e) => {
+ handleFieldChange(e);
+ const v = e.target.value.toLowerCase();
+ const suggestions = cipRows
+ .filter((r) => r.INSTNM.toLowerCase().includes(v))
+ .map((r) => r.INSTNM);
+ setSchoolSug([...new Set(suggestions)].slice(0, 10));
+};
+
+const onProgramInput = (e) => {
+ handleFieldChange(e);
+ if (!form.selected_school) return;
+ const v = e.target.value.toLowerCase();
+ const sug = cipRows
+ .filter(
+ (r) =>
+ r.INSTNM.toLowerCase() === form.selected_school.toLowerCase() &&
+ r.CIPDESC.toLowerCase().includes(v)
+ )
+ .map((r) => r.CIPDESC);
+ setProgSug([...new Set(sug)].slice(0, 10));
+};
+
+
+ useEffect(() => {
+ if (id && id !== 'new') {
+ fetch(`/api/premium/college-profile?careerProfileId=${careerId}`, {
+ headers: { Authorization: `Bearer ${token}` }
+ })
+ .then(r => r.json())
+ .then(setForm);
+ }
+ }, [careerId, id, token]);
+
+ async function handleSave(){
+ try{
+ const body = normalisePayload({ ...form, tuition: chosenTuition, career_profile_id: careerId });
+ const res = await authFetch('/api/premium/college-profile',{
+ method:'POST',
+ headers:{'Content-Type':'application/json'},
+ body:JSON.stringify(body)
+ });
+ if(!res.ok) throw new Error(await res.text());
+ alert('Saved!');
+ setForm(p => ({ ...p, tuition: chosenTuition }));
+ setManualTuition(String(chosenTuition));
+ nav(-1);
+ }catch(err){ console.error(err); alert(err.message);}
+}
+
+/* LOAD iPEDS ----------------------------- */
+useEffect(() => {
+ fetch('/ic2023_ay.csv')
+ .then(r => r.text())
+ .then(text => {
+ const rows = text.split('\n').map(l => l.split(','));
+ const headers = rows[0];
+ const parsed = rows.slice(1).map(r =>
+ Object.fromEntries(r.map((v,i)=>[headers[i], v]))
+ );
+ setIpeds(parsed); // you already declared setIpeds
+ })
+ .catch(err => console.error('iPEDS load failed', err));
+}, []);
+
+ useEffect(() => { fetch('/cip_institution_mapping_new.json')
+ .then(r=>r.text()).then(t => setCipRows(
+ t.split('\n').map(l=>{try{return JSON.parse(l)}catch{ return null }})
+ .filter(Boolean)
+ ));
+ fetch('/ic2023_ay.csv')
+ .then(r=>r.text()).then(csv=>{/* identical to CollegeOnboarding */});
+},[]);
+
+useEffect(()=>{
+ if(!form.selected_school || !form.selected_program) { setTypes([]); return; }
+ const t = cipRows.filter(r =>
+ r.INSTNM.toLowerCase()===form.selected_school.toLowerCase() &&
+ r.CIPDESC===form.selected_program)
+ .map(r=>r.CREDDESC);
+ setTypes([...new Set(t)]);
+},[form.selected_school, form.selected_program, cipRows]);
+
+
+useEffect(() => {
+ if (!ipeds.length) return;
+ if (!form.selected_school ||
+ !form.program_type ||
+ !form.credit_hours_per_year) return;
+
+ /* 1 ─ locate UNITID */
+ const sch = cipRows.find(
+ r => r.INSTNM.toLowerCase() === form.selected_school.toLowerCase()
+ );
+ if (!sch) return;
+ const unitId = sch.UNITID;
+ const row = ipeds.find(r => r.UNITID === unitId);
+ if (!row) return;
+
+ /* 2 ─ decide in‑state / district buckets */
+ const grad = [
+ "Master's Degree","Doctoral Degree",
+ "Graduate/Professional Certificate","First Professional Degree"
+ ].includes(form.program_type);
+
+ const pick = (codeInDist, codeInState, codeOut) => {
+ if (form.is_in_district) return row[codeInDist];
+ else if (form.is_in_state) return row[codeInState];
+ else return row[codeOut];
+ };
+
+ const partTime = grad
+ ? pick('HRCHG5','HRCHG6','HRCHG7')
+ : pick('HRCHG1','HRCHG2','HRCHG3');
+
+ const fullTime = grad
+ ? pick('TUITION5','TUITION6','TUITION7')
+ : pick('TUITION1','TUITION2','TUITION3');
+
+ const chpy = parseFloat(form.credit_hours_per_year) || 0;
+ const est = chpy && chpy < 24
+ ? parseFloat(partTime || 0) * chpy
+ : parseFloat(fullTime || 0);
+
+ setAutoTuition(Math.round(est));
+}, [
+ ipeds,
+ cipRows,
+ form.selected_school,
+ form.program_type,
+ form.credit_hours_per_year,
+ form.is_in_state,
+ form.is_in_district
+]);
+
+const handleManualTuitionChange = e => setManualTuition(e.target.value);
+const chosenTuition = manualTuition.trim() === ''
+ ? autoTuition
+ : parseFloat(manualTuition);
+
+return (
+
+
+ {id === 'new' ? 'New' : 'Edit'} College Plan
+
+
+ {(form.college_enrollment_status === 'currently_enrolled' ||
+ form.college_enrollment_status === 'prospective_student') ? (
+ /* ───────────────────────────────────────────────────────── */
+
+
+ {/* 1 │ Location / modality check‑boxes */}
+ {[
+ { n:'is_in_district', l:'In District?' },
+ { n:'is_in_state', l:'In‑State Tuition?' },
+ { n:'is_online', l:'Program is Fully Online' },
+ { n:'loan_deferral_until_graduation',
+ l:'Defer Loan Payments until Graduation?' }
+ ].map(({n,l}) => (
+
+
+ {l}
+
+ ))}
+
+ {/* 2 │ School picker */}
+
+
+ School Name * (choose from list)
+
+ {
+ const ok = cipRows.some(
+ r => r.INSTNM.toLowerCase() === form.selected_school.toLowerCase()
+ );
+ setSchoolValid(ok);
+ if (!ok) alert('Please pick a school from the list.');
+ }}
+ list="school-suggestions"
+ placeholder="Start typing and choose…"
+ className={`w-full border rounded p-2 ${schoolValid ? '' : 'border-red-500'}`}
+ required
+ />
+
+ {schoolSug.map((s,i)=>(
+
+ ))}
+
+
+
+ {/* 3 │ Program picker */}
+
+
+ Major / Program * (choose from list)
+
+ {
+ const ok =
+ form.selected_school && // need a school first
+ cipRows.some(
+ r =>
+ r.INSTNM.toLowerCase() === form.selected_school.toLowerCase() &&
+ r.CIPDESC.toLowerCase() === form.selected_program.toLowerCase()
+ );
+ setProgramValid(ok);
+ if (!ok) alert('Please pick a program from the list.');
+ }}
+ list="program-suggestions"
+ placeholder="Start typing and choose…"
+ className={`w-full border rounded p-2 ${programValid ? '' : 'border-red-500'}`}
+ required
+ />
+
+ {progSug.map((p,i)=>(
+
+ ))}
+
+
+
+ {/* 4 │ Program‑type */}
+
+ Degree Type *
+
+ Select Program Type
+ {types.map((t,i)=>{t} )}
+
+
+
+ {/* 5 │ Academic calendar */}
+
+ Academic Calendar
+
+ Semester
+ Quarter
+ Trimester
+ Other
+
+
+
+ {/* 6 │ Credit‑hour fields (conditionally rendered) */}
+ {(form.program_type === 'Graduate/Professional Certificate' ||
+ form.program_type === 'First Professional Degree' ||
+ form.program_type === 'Doctoral Degree' ||
+ form.program_type === 'Undergraduate Certificate or Diploma') && (
+
+ Credit Hours Required
+
+
+ )}
+
+
+ Credit Hours Per Year
+
+
+
+ {/* 7 │ Tuition & aid */}
+
+ Yearly Tuition
+
+
+
Annual Aid
+
+
+
+ {/* 8 │ Existing debt */}
+
+ Existing College Debt
+
+
+
+ {/* 9 │ Program‑length & hours‑completed */}
+ {(form.college_enrollment_status === 'currently_enrolled' ||
+ form.college_enrollment_status === 'prospective_student') && (
+ <>
+
+ Program Length (years)
+
+
+ {form.college_enrollment_status === 'currently_enrolled' && (
+
+ Hours Completed
+
+
+ )}
+ >
+ )}
+
+ {/* 10 │ Interest, term, extra payment, salary */}
+
+
+ Interest %
+
+
+
+ Loan Term (years)
+
+
+
+
+
+ Extra Monthly Payment
+
+
+
+
+ Expected Salary After Graduation
+
+
+
+ ) : (
+
+ User is neither currently enrolled nor a prospective student – nothing to
+ edit.
+
+ )}
+
+ {/* 11 │ Action buttons */}
+
+ nav(-1)}
+ className="bg-gray-200 hover:bg-gray-300 text-gray-700 font-semibold py-2 px-4 rounded mr-3"
+ >
+ ← Back
+
+
+ Save
+
+
+
+
+);
+
+}
diff --git a/src/components/CollegeProfileList.js b/src/components/CollegeProfileList.js
new file mode 100644
index 0000000..aabc834
--- /dev/null
+++ b/src/components/CollegeProfileList.js
@@ -0,0 +1,57 @@
+import React, { useEffect, useState } from 'react';
+import { Link, useNavigate } from 'react-router-dom';
+
+export default function CollegeProfileList() {
+ const [rows, setRows] = useState([]);
+ const nav = useNavigate();
+ const token = localStorage.getItem('token');
+
+ useEffect(() => {
+ fetch('/api/premium/college-profile/all', {
+ headers: { Authorization: `Bearer ${token}` }
+ })
+ .then(r => r.json())
+ .then(d => setRows(d.collegeProfiles || []));
+ }, [token]);
+
+ return (
+
+
College Profiles
+
+
+
+
+ Career
+ School
+ Program
+ Created
+
+
+
+
+ {rows.map(r => (
+
+ {r.career_title}
+ {r.selected_school}
+ {r.selected_program}
+ {r.created_at?.slice(0,10)}
+
+
+ edit
+
+
+
+ ))}
+ {rows.length === 0 && (
+
+ No college profiles yet
+
+ )}
+
+
+
+ );
+}
diff --git a/src/components/FinancialProfileForm.js b/src/components/FinancialProfileForm.js
index 34b6903..258a905 100644
--- a/src/components/FinancialProfileForm.js
+++ b/src/components/FinancialProfileForm.js
@@ -1,181 +1,219 @@
// FinancialProfileForm.js
-import React, { useState, useEffect } from 'react';
-import authFetch from '../utils/authFetch.js';
+import React, { useEffect, useState } from 'react';
+import { useNavigate } from 'react-router-dom';
-function FinancialProfileForm() {
- // We'll store the fields in local state
- const [currentSalary, setCurrentSalary] = useState('');
- const [additionalIncome, setAdditionalIncome] = useState('');
- const [monthlyExpenses, setMonthlyExpenses] = useState('');
- const [monthlyDebtPayments, setMonthlyDebtPayments] = useState('');
- const [retirementSavings, setRetirementSavings] = useState('');
- const [emergencyFund, setEmergencyFund] = useState('');
- const [retirementContribution, setRetirementContribution] = useState('');
- const [monthlyEmergencyContribution, setMonthlyEmergencyContribution] = useState('');
- const [extraCashEmergencyPct, setExtraCashEmergencyPct] = useState('');
- const [extraCashRetirementPct, setExtraCashRetirementPct] = useState('');
+import authFetch from '../utils/authFetch.js';
+import Modal from './ui/modal.js';
+import ExpensesWizard from './ExpensesWizard.js'; // same wizard you use in onboarding
+import { Button } from './ui/button.js'; // Tailwind‑based button (optional)
+/* helper – clamp 0‑100 */
+const pct = v => Math.min(Math.max(parseFloat(v) || 0, 0), 100);
+
+export default function FinancialProfileForm() {
+ const nav = useNavigate();
+
+ /* ─────────────── local state ─────────────── */
+ const [currentSalary, setCurrentSalary] = useState('');
+ const [additionalIncome, setAdditionalIncome] = useState('');
+ const [monthlyExpenses, setMonthlyExpenses] = useState('');
+ const [monthlyDebtPayments, setMonthlyDebtPayments] = useState('');
+ const [retirementSavings, setRetirementSavings] = useState('');
+ const [emergencyFund, setEmergencyFund] = useState('');
+ const [retirementContribution, setRetirementContribution] = useState('');
+ const [emergencyContribution, setEmergencyContribution] = useState('');
+ const [extraCashEmergencyPct, setExtraCashEmergencyPct] = useState('50');
+ const [extraCashRetirementPct, setExtraCashRetirementPct] = useState('50');
+
+ /* wizard modal */
+ const [showExpensesWizard, setShowExpensesWizard] = useState(false);
+ const openWizard = () => setShowExpensesWizard(true);
+ const closeWizard = () => setShowExpensesWizard(false);
+
+ /* ───────────── preload existing row ───────── */
useEffect(() => {
- // On mount, fetch the user's existing profile from the new financial_profiles table
- async function fetchProfile() {
+ (async () => {
try {
- const res = await authFetch('/api/premium/financial-profile', {
- method: 'GET'
- });
- if (res.ok) {
- const data = await res.json();
- // data might be an empty object if no row yet
- setCurrentSalary(data.current_salary || '');
- setAdditionalIncome(data.additional_income || '');
- setMonthlyExpenses(data.monthly_expenses || '');
- setMonthlyDebtPayments(data.monthly_debt_payments || '');
- setRetirementSavings(data.retirement_savings || '');
- setEmergencyFund(data.emergency_fund || '');
- setRetirementContribution(data.retirement_contribution || '');
- setMonthlyEmergencyContribution(data.monthly_emergency_contribution || '');
- setExtraCashEmergencyPct(data.extra_cash_emergency_pct || '');
- setExtraCashRetirementPct(data.extra_cash_retirement_pct || '');
- }
- } catch (err) {
- console.error("Failed to load financial profile:", err);
- }
- }
- fetchProfile();
+ const res = await authFetch('/api/premium/financial-profile');
+ if (!res.ok) return;
+ const d = await res.json();
+
+ setCurrentSalary (d.current_salary ?? '');
+ setAdditionalIncome (d.additional_income ?? '');
+ setMonthlyExpenses (d.monthly_expenses ?? '');
+ setMonthlyDebtPayments (d.monthly_debt_payments ?? '');
+ setRetirementSavings (d.retirement_savings ?? '');
+ setEmergencyFund (d.emergency_fund ?? '');
+ setRetirementContribution (d.retirement_contribution ?? '');
+ setEmergencyContribution (d.emergency_contribution ?? '');
+ setExtraCashEmergencyPct (d.extra_cash_emergency_pct ?? '');
+ setExtraCashRetirementPct (d.extra_cash_retirement_pct ?? '');
+ } catch (err) { console.error(err); }
+ })();
}, []);
- // Submit form updates => POST to the same endpoint
- async function handleSubmit(e) {
- e.preventDefault();
- try {
- const body = {
- current_salary: parseFloat(currentSalary) || 0,
- additional_income: parseFloat(additionalIncome) || 0,
- monthly_expenses: parseFloat(monthlyExpenses) || 0,
- monthly_debt_payments: parseFloat(monthlyDebtPayments) || 0,
- retirement_savings: parseFloat(retirementSavings) || 0,
- emergency_fund: parseFloat(emergencyFund) || 0,
- retirement_contribution: parseFloat(retirementContribution) || 0,
- monthly_emergency_contribution: parseFloat(monthlyEmergencyContribution) || 0,
- extra_cash_emergency_pct: parseFloat(extraCashEmergencyPct) || 0,
- extra_cash_retirement_pct: parseFloat(extraCashRetirementPct) || 0
- };
+ /* -----------------------------------------------------------
+ * keep the two % inputs complementary (must add to 100)
+ * --------------------------------------------------------- */
+ function handleChange(e) {
+ const { name, value } = e.target;
+ const pct = Math.max(0, Math.min(100, Number(value) || 0)); // clamp 0‑100
- const res = await authFetch('/api/premium/financial-profile', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify(body)
- });
-
- if (res.ok) {
- // show success or redirect
- console.log("Profile updated");
- } else {
- console.error("Failed to update profile:", await res.text());
+ if (name === 'extraCashEmergencyPct') {
+ setExtraCashEmergencyPct(String(pct));
+ setExtraCashRetirementPct(String(100 - pct));
+ } else if (name === 'extraCashRetirementPct') {
+ setExtraCashRetirementPct(String(pct));
+ setExtraCashEmergencyPct(String(100 - pct));
+ } else {
+ // all other numeric fields:
+ // allow empty string so users can clear then re‑type
+ const update = valSetter => valSetter(value === '' ? '' : Number(value));
+ switch (name) {
+ case 'currentSalary': update(setCurrentSalary); break;
+ case 'additionalIncome': update(setAdditionalIncome); break;
+ case 'monthlyExpenses': update(setMonthlyExpenses); break;
+ case 'monthlyDebtPayments': update(setMonthlyDebtPayments); break;
+ case 'retirementSavings': update(setRetirementSavings); break;
+ case 'emergencyFund': update(setEmergencyFund); break;
+ case 'retirementContribution': update(setRetirementContribution); break;
+ case 'emergencyContribution': update(setEmergencyContribution); break;
+ default: break;
}
- } catch (err) {
- console.error("Error submitting financial profile:", err);
}
}
+ /* ───────────── submit ─────────────────────── */
+ async function handleSubmit(e) {
+ e.preventDefault();
+
+ const body = {
+ current_salary: parseFloat(currentSalary) || 0,
+ additional_income: parseFloat(additionalIncome) || 0,
+ monthly_expenses: parseFloat(monthlyExpenses) || 0,
+ monthly_debt_payments: parseFloat(monthlyDebtPayments) || 0,
+ retirement_savings: parseFloat(retirementSavings) || 0,
+ emergency_fund: parseFloat(emergencyFund) || 0,
+ retirement_contribution: parseFloat(retirementContribution) || 0,
+ emergency_contribution: parseFloat(emergencyContribution) || 0,
+ extra_cash_emergency_pct: pct(extraCashEmergencyPct),
+ extra_cash_retirement_pct: pct(extraCashRetirementPct)
+ };
+
+ try {
+ const res = await authFetch('/api/premium/financial-profile', {
+ method : 'POST',
+ headers: { 'Content-Type':'application/json' },
+ body : JSON.stringify(body)
+ });
+ if (!res.ok) throw new Error(await res.text());
+ alert('Financial profile saved.');
+ nav(-1);
+ } catch (err) {
+ console.error(err);
+ alert('Failed to save financial profile.');
+ }
+ }
+
+ /* ───────────── view ───────────────────────── */
return (
-
+ To Retirement (%)
+
+
+ {/* action buttons */}
+
+ nav(-1)}
+ className="bg-gray-200 hover:bg-gray-300 text-gray-700 font-semibold py-2 px-4 rounded"
+ >
+ ← Back
+
+
+
+ Save
+
+
+
+
+ {/* wizard modal */}
+ {showExpensesWizard && (
+
+ {
+ setMonthlyExpenses(total);
+ closeWizard();
+ }}
+ />
+
+ )}
+ >
);
}
-
-export default FinancialProfileForm;
diff --git a/src/components/PremiumOnboarding/CareerOnboarding.js b/src/components/PremiumOnboarding/CareerOnboarding.js
index 96cfcc9..a3f927c 100644
--- a/src/components/PremiumOnboarding/CareerOnboarding.js
+++ b/src/components/PremiumOnboarding/CareerOnboarding.js
@@ -33,14 +33,6 @@ const CareerOnboarding = ({ nextStep, prevStep, data, setData, finishNow }) => {
const skipFin = !!data.skipFinancialStep;
// 1) Grab the location state values, if any
- const {
- socCode,
- cipCodes,
- careerTitle, // <--- we passed this from handleSelectForEducation
- userZip,
- userState,
- } = location.state || {};
-
/* ── 3. side‑effects when route brings a new career object ── */
useEffect(() => {
if (!navCareerObj?.title) return;
@@ -166,17 +158,6 @@ function handleSubmit() {
/>
-
- Projected End Date (optional):
-
-
-
Are you currently enrolled in college or planning to enroll?
diff --git a/src/components/PremiumOnboarding/ReviewPage.js b/src/components/PremiumOnboarding/ReviewPage.js
index c7648ba..218a4f3 100644
--- a/src/components/PremiumOnboarding/ReviewPage.js
+++ b/src/components/PremiumOnboarding/ReviewPage.js
@@ -48,7 +48,6 @@ function ReviewPage({
College enrollment Status: {careerData.college_enrollment_status || 'N/A'}
Status: {careerData.status || 'N/A'}
Start Date: {careerData.start_date || 'N/A'}
- Projected End Date: {careerData.projected_end_date || 'N/A'}
Career Goals: {careerData.career_goals || 'N/A'}
diff --git a/src/components/RetirementLanding.js b/src/components/RetirementLanding.js
index 0857445..d554ef8 100644
--- a/src/components/RetirementLanding.js
+++ b/src/components/RetirementLanding.js
@@ -15,8 +15,7 @@ function RetirementLanding() {
Plan strategically and financially for retirement. AptivaAI provides you with clear financial projections, milestone tracking, and scenario analysis for a secure future.
- navigate('/financial-profile')}>Update Financial Profile
- navigate('/retirement-planner')}>Set Retirement Milestones and get AI help with planning
+ navigate('/retirement-planner')}>Compare different retirement scenarios and get AI help with planning
diff --git a/src/components/ScenarioEditModal.js b/src/components/ScenarioEditModal.js
index 5b20cb4..b749296 100644
--- a/src/components/ScenarioEditModal.js
+++ b/src/components/ScenarioEditModal.js
@@ -159,7 +159,6 @@ export default function ScenarioEditModal({
career_name : safe(s.career_name),
status : safe(s.status || 'planned'),
start_date : safe(s.start_date),
- projected_end_date : safe(s.projected_end_date),
retirement_start_date: safe(s.retirement_start_date),
desired_retirement_income_monthly : safe(
s.desired_retirement_income_monthly
@@ -498,7 +497,6 @@ async function handleSave() {
currently_working : formData.currently_working || "no",
status : s(formData.status),
start_date : s(formData.start_date),
- projected_end_date : s(formData.projected_end_date),
retirement_start_date : s(formData.retirement_start_date),
desired_retirement_income_monthly : n(formData.desired_retirement_income_monthly),
@@ -687,18 +685,6 @@ if (formData.retirement_start_date) {
className="border border-gray-300 rounded p-2 w-full"
/>
-
-
- Projected End Date
-
-
-
{/* Retirement date */}
diff --git a/src/components/ScenarioEditWizard.js b/src/components/ScenarioEditWizard.js
index 26ff516..a9e0f96 100644
--- a/src/components/ScenarioEditWizard.js
+++ b/src/components/ScenarioEditWizard.js
@@ -51,7 +51,6 @@ export default function ScenarioEditWizard({
currently_working: scenData.currently_working,
status: scenData.status,
start_date: scenData.start_date,
- projected_end_date: scenData.projected_end_date,
planned_monthly_expenses: scenData.planned_monthly_expenses,
planned_monthly_debt_payments: scenData.planned_monthly_debt_payments,
planned_monthly_retirement_contribution: scenData.planned_monthly_retirement_contribution,