Added CareerProfileForm and CollegeProfileForm, changed FinancialProfileForm for consistency, RetirementLanding button removal for FinancialProfileForm. Altered CareerOnboarding and ReviewPage
This commit is contained in:
parent
613f79f6ee
commit
465a7d686c
@ -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
|
||||
------------------------------------------------------------------ */
|
||||
|
37
src/App.js
37
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 */}
|
||||
<header className="flex items-center justify-between border-b bg-white px-6 py-4 shadow-sm relative">
|
||||
<h1 className="text-lg font-semibold">
|
||||
AptivaAI - Career Guidance Platform (beta)
|
||||
AptivaAI - Career Guidance Platform
|
||||
</h1>
|
||||
|
||||
{isAuthenticated && (
|
||||
@ -360,7 +364,7 @@ const uiToolHandlers = useMemo(() => {
|
||||
)}
|
||||
onClick={() => navigate('/retirement')}
|
||||
>
|
||||
Retirement Planning
|
||||
Retirement Planning (beta)
|
||||
{!canAccessPremium && (
|
||||
<span className="text-xs ml-1 text-gray-600">
|
||||
(Premium)
|
||||
@ -406,20 +410,32 @@ const uiToolHandlers = useMemo(() => {
|
||||
</Link>
|
||||
{canAccessPremium ? (
|
||||
/* Premium users go straight to the wizard */
|
||||
<Link
|
||||
to="/premium-onboarding"
|
||||
<Link
|
||||
to="/profile/careers"
|
||||
className="block px-4 py-2 hover:bg-gray-100 text-sm text-gray-700"
|
||||
>
|
||||
Premium Onboarding
|
||||
Career Profiles
|
||||
</Link>
|
||||
) : (
|
||||
/* Free users are nudged to upgrade */
|
||||
<span
|
||||
className="block px-4 py-2 text-sm text-gray-400 cursor-not-allowed"
|
||||
>
|
||||
Career Profiles (Premium)
|
||||
</span>
|
||||
)}
|
||||
|
||||
{/* College Profiles (go straight to list) */}
|
||||
{canAccessPremium ? (
|
||||
<Link
|
||||
to="/paywall"
|
||||
to="/profile/college"
|
||||
className="block px-4 py-2 hover:bg-gray-100 text-sm text-gray-700"
|
||||
>
|
||||
College Planning <span className="text-xs">(Premium)</span>
|
||||
College Profiles
|
||||
</Link>
|
||||
) : (
|
||||
<span className="block px-4 py-2 text-sm text-gray-400 cursor-not-allowed">
|
||||
College Profiles (Premium)
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@ -533,6 +549,11 @@ const uiToolHandlers = useMemo(() => {
|
||||
</PremiumRoute>
|
||||
}
|
||||
/>
|
||||
<Route path="/profile/careers" element={<CareerProfileList />} />
|
||||
<Route path="/profile/careers/:id/edit" element={<CareerProfileForm />} />
|
||||
|
||||
<Route path="/profile/college/" element={<CollegeProfileList />} />
|
||||
<Route path="/profile/college/:careerId/:id?" element={<CollegeProfileForm />} />
|
||||
<Route
|
||||
path="/financial-profile"
|
||||
element={
|
||||
|
232
src/components/CareerProfileForm.js
Normal file
232
src/components/CareerProfileForm.js
Normal file
@ -0,0 +1,232 @@
|
||||
// CareerProfileForm.js
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import authFetch from '../utils/authFetch.js';
|
||||
import CareerSearch from './CareerSearch.js'; // ← same component as onboarding
|
||||
|
||||
export default function CareerProfileForm() {
|
||||
const { id } = useParams(); // "new" or an existing uuid
|
||||
const nav = useNavigate();
|
||||
|
||||
/* ---------- 1. local state ---------- */
|
||||
const [form, setForm] = useState({
|
||||
scenario_title : '',
|
||||
career_name : '',
|
||||
soc_code : '',
|
||||
status : 'current',
|
||||
start_date : '',
|
||||
retirement_start_date : '',
|
||||
college_enrollment_status : '',
|
||||
career_goals : '',
|
||||
desired_retirement_income_monthly : ''
|
||||
});
|
||||
|
||||
const [careerLocked, setCareerLocked] = useState(id !== 'new'); // lock unless new
|
||||
|
||||
/* ---------- 2. helpers ---------- */
|
||||
const handleChange = e =>
|
||||
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 (
|
||||
<div className="max-w-lg mx-auto space-y-4">
|
||||
<h2 className="text-2xl font-semibold">
|
||||
{id === 'new' ? 'New' : 'Edit'} Career Profile
|
||||
</h2>
|
||||
|
||||
{/* Scenario title */}
|
||||
<label className="block">
|
||||
<span className="font-medium">Scenario Title</span>
|
||||
<input
|
||||
name="scenario_title"
|
||||
className="mt-1 w-full border rounded p-2"
|
||||
placeholder="e.g. Data‑Scientist Plan"
|
||||
value={form.scenario_title}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</label>
|
||||
|
||||
{/* Career picker (locked vs editable) */}
|
||||
<label className="block font-medium">Career *</label>
|
||||
{careerLocked ? (
|
||||
<div className="flex items-center space-x-2">
|
||||
<input
|
||||
className="flex-1 border rounded p-2 bg-gray-100"
|
||||
value={form.career_name}
|
||||
disabled
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="text-blue-600 underline text-sm"
|
||||
onClick={unlockCareer}
|
||||
>
|
||||
Change
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<CareerSearch onCareerSelected={handleCareerSelected} required />
|
||||
)}
|
||||
|
||||
{/* Status */}
|
||||
<label className="block">
|
||||
<span className="font-medium">Status</span>
|
||||
<select
|
||||
name="status"
|
||||
className="mt-1 w-full border rounded p-2"
|
||||
value={form.status}
|
||||
onChange={handleChange}
|
||||
>
|
||||
<option value="current">current</option>
|
||||
<option value="future">future</option>
|
||||
<option value="retired">retired</option>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
{/* Dates */}
|
||||
<label className="block">
|
||||
<span className="font-medium">Start Date</span>
|
||||
<input
|
||||
type="date"
|
||||
name="start_date"
|
||||
className="mt-1 w-full border rounded p-2"
|
||||
value={form.start_date}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label className="block">
|
||||
<span className="font-medium">Retirement Start Date</span>
|
||||
<input
|
||||
type="date"
|
||||
name="retirement_start_date"
|
||||
className="mt-1 w-full border rounded p-2"
|
||||
value={form.retirement_start_date}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</label>
|
||||
|
||||
{/* College status */}
|
||||
<label className="block">
|
||||
<span className="font-medium">College Enrollment Status</span>
|
||||
<select
|
||||
name="college_enrollment_status"
|
||||
className="mt-1 w-full border rounded p-2"
|
||||
value={form.college_enrollment_status}
|
||||
onChange={handleChange}
|
||||
>
|
||||
<option value="">-- select --</option>
|
||||
<option value="not_applicable">Not Applicable</option>
|
||||
<option value="prospective_student">Prospective Student</option>
|
||||
<option value="currently_enrolled">Currently Enrolled</option>
|
||||
<option value="completed">Completed</option>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
{/* Career goals */}
|
||||
<label className="block">
|
||||
<span className="font-medium">Career Goals</span>
|
||||
<textarea
|
||||
rows={3}
|
||||
name="career_goals"
|
||||
className="mt-1 w-full border rounded p-2"
|
||||
placeholder="e.g. Become a senior data‑scientist in five years…"
|
||||
value={form.career_goals}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</label>
|
||||
|
||||
{/* Desired retirement income */}
|
||||
<label className="block">
|
||||
<span className="font-medium">Desired Retirement Income / Month ($)</span>
|
||||
<input
|
||||
type="number"
|
||||
name="desired_retirement_income_monthly"
|
||||
className="mt-1 w-full border rounded p-2"
|
||||
placeholder="e.g. 6000"
|
||||
value={form.desired_retirement_income_monthly}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</label>
|
||||
|
||||
{/* Action buttons */}
|
||||
<div className="pt-4 flex justify-between">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => nav(-1)}
|
||||
className="bg-gray-200 hover:bg-gray-300 text-gray-700 font-semibold py-2 px-4 rounded"
|
||||
>
|
||||
← Back
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={save}
|
||||
className="bg-blue-600 hover:bg-blue-700 text-white font-semibold py-2 px-4 rounded"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
79
src/components/CareerProfileList.js
Normal file
79
src/components/CareerProfileList.js
Normal file
@ -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 (
|
||||
<div className="max-w-4xl mx-auto space-y-4">
|
||||
<h2 className="text-2xl font-semibold">Career Profiles</h2>
|
||||
|
||||
<button
|
||||
onClick={() => nav('/profile/careers/new/edit')}
|
||||
className="px-3 py-2 bg-blue-600 text-white rounded"
|
||||
>
|
||||
+ New Career Profile
|
||||
</button>
|
||||
|
||||
<table className="w-full border mt-4 text-sm">
|
||||
<thead className="bg-gray-100">
|
||||
<tr>
|
||||
<th className="p-2 text-left">Title</th>
|
||||
<th className="p-2 text-left">Status</th>
|
||||
<th className="p-2">Start</th>
|
||||
<th className="p-2"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{rows.map(r => (
|
||||
<tr key={r.id} className="border-t">
|
||||
<td className="p-2">{r.scenario_title || r.career_name}</td>
|
||||
<td className="p-2">{r.status}</td>
|
||||
<td className="p-2">{r.start_date}</td>
|
||||
<td className="p-2 space-x-2">
|
||||
<Link
|
||||
to={`/profile/careers/${r.id}/edit`}
|
||||
className="underline text-blue-600"
|
||||
>
|
||||
edit
|
||||
</Link>
|
||||
<button
|
||||
onClick={() => remove(r.id)}
|
||||
className="text-red-600 underline"
|
||||
>
|
||||
delete
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
{rows.length === 0 && (
|
||||
<tr>
|
||||
<td colSpan={5} className="p-4 text-center text-gray-500">
|
||||
No career profiles yet
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
}
|
522
src/components/CollegeProfileForm.js
Normal file
522
src/components/CollegeProfileForm.js
Normal file
@ -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 (
|
||||
<div className="max-w-md mx-auto p-6 space-y-4">
|
||||
<h2 className="text-2xl font-semibold">
|
||||
{id === 'new' ? 'New' : 'Edit'} College Plan
|
||||
</h2>
|
||||
|
||||
{(form.college_enrollment_status === 'currently_enrolled' ||
|
||||
form.college_enrollment_status === 'prospective_student') ? (
|
||||
/* ───────────────────────────────────────────────────────── */
|
||||
<div className="space-y-4">
|
||||
|
||||
{/* 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}) => (
|
||||
<div key={n} className="flex items-center space-x-2">
|
||||
<input type="checkbox" name={n}
|
||||
className="h-4 w-4"
|
||||
checked={!!form[n]}
|
||||
onChange={handleFieldChange}/>
|
||||
<label className="font-medium">{l}</label>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{/* 2 │ School picker */}
|
||||
<div className="space-y-1">
|
||||
<label className="block font-medium">
|
||||
School Name * (choose from list)
|
||||
</label>
|
||||
<input
|
||||
name="selected_school"
|
||||
value={form.selected_school}
|
||||
onChange={onSchoolInput}
|
||||
onBlur={() => {
|
||||
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
|
||||
/>
|
||||
<datalist id="school-suggestions">
|
||||
{schoolSug.map((s,i)=>(
|
||||
<option key={i} value={s} />
|
||||
))}
|
||||
</datalist>
|
||||
</div>
|
||||
|
||||
{/* 3 │ Program picker */}
|
||||
<div className="space-y-1">
|
||||
<label className="block font-medium">
|
||||
Major / Program * (choose from list)
|
||||
</label>
|
||||
<input
|
||||
name="selected_program"
|
||||
value={form.selected_program}
|
||||
onChange={onProgramInput}
|
||||
onBlur={() => {
|
||||
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
|
||||
/>
|
||||
<datalist id="program-suggestions">
|
||||
{progSug.map((p,i)=>(
|
||||
<option key={i} value={p} />
|
||||
))}
|
||||
</datalist>
|
||||
</div>
|
||||
|
||||
{/* 4 │ Program‑type */}
|
||||
<div className="space-y-1">
|
||||
<label className="block font-medium">Degree Type *</label>
|
||||
<select
|
||||
name="program_type"
|
||||
value={form.program_type}
|
||||
onChange={handleFieldChange}
|
||||
className="w-full border rounded p-2"
|
||||
required
|
||||
>
|
||||
<option value="">Select Program Type</option>
|
||||
{types.map((t,i)=><option key={i} value={t}>{t}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* 5 │ Academic calendar */}
|
||||
<div className="space-y-1">
|
||||
<label className="block font-medium">Academic Calendar</label>
|
||||
<select
|
||||
name="academic_calendar"
|
||||
value={form.academic_calendar || 'semester'}
|
||||
onChange={handleFieldChange}
|
||||
className="w-full border rounded p-2"
|
||||
>
|
||||
<option value="semester">Semester</option>
|
||||
<option value="quarter">Quarter</option>
|
||||
<option value="trimester">Trimester</option>
|
||||
<option value="other">Other</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* 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') && (
|
||||
<div className="space-y-1">
|
||||
<label className="block font-medium">Credit Hours Required</label>
|
||||
<input
|
||||
type="number"
|
||||
name="credit_hours_required"
|
||||
value={form.credit_hours_required}
|
||||
onChange={handleFieldChange}
|
||||
className="w-full border rounded p-2"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-1">
|
||||
<label className="block font-medium">Credit Hours Per Year</label>
|
||||
<input
|
||||
type="number"
|
||||
name="credit_hours_per_year"
|
||||
value={form.credit_hours_per_year}
|
||||
onChange={handleFieldChange}
|
||||
className="w-full border rounded p-2"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 7 │ Tuition & aid */}
|
||||
<div className="space-y-1">
|
||||
<label className="block font-medium">Yearly Tuition</label>
|
||||
<input
|
||||
type="number"
|
||||
value={
|
||||
manualTuition.trim() === '' ? autoTuition : manualTuition
|
||||
}
|
||||
onChange={handleManualTuitionChange}
|
||||
placeholder="Blank = auto‑calculated"
|
||||
className="w-full border rounded p-2"
|
||||
/>
|
||||
</div>
|
||||
<span className="font-medium">Annual Aid</span>
|
||||
<input
|
||||
type="number"
|
||||
name="annual_financial_aid"
|
||||
value={form.annual_financial_aid}
|
||||
onChange={handleFieldChange}
|
||||
className="mt-1 w-full border rounded p-2"
|
||||
/>
|
||||
|
||||
|
||||
{/* 8 │ Existing debt */}
|
||||
<div className="space-y-1">
|
||||
<label className="block font-medium">Existing College Debt</label>
|
||||
<input
|
||||
type="number"
|
||||
name="existing_college_debt"
|
||||
value={form.existing_college_debt}
|
||||
onChange={handleFieldChange}
|
||||
className="w-full border rounded p-2"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 9 │ Program‑length & hours‑completed */}
|
||||
{(form.college_enrollment_status === 'currently_enrolled' ||
|
||||
form.college_enrollment_status === 'prospective_student') && (
|
||||
<>
|
||||
<div className="space-y-1">
|
||||
<label className="block font-medium">Program Length (years)</label>
|
||||
<input
|
||||
type="number"
|
||||
name="program_length"
|
||||
value={form.program_length}
|
||||
onChange={handleFieldChange}
|
||||
className="w-full border rounded p-2"
|
||||
/>
|
||||
</div>
|
||||
{form.college_enrollment_status === 'currently_enrolled' && (
|
||||
<div className="space-y-1">
|
||||
<label className="block font-medium">Hours Completed</label>
|
||||
<input
|
||||
type="number"
|
||||
name="hours_completed"
|
||||
value={form.hours_completed}
|
||||
onChange={handleFieldChange}
|
||||
className="w-full border rounded p-2"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* 10 │ Interest, term, extra payment, salary */}
|
||||
<div className="flex space-x-4">
|
||||
<label className="block flex-1">
|
||||
<span className="font-medium">Interest %</span>
|
||||
<input
|
||||
type="number"
|
||||
name="interest_rate"
|
||||
value={form.interest_rate}
|
||||
onChange={handleFieldChange}
|
||||
className="mt-1 w-full border rounded p-2"
|
||||
/>
|
||||
</label>
|
||||
<label className="block flex-1">
|
||||
<span className="font-medium">Loan Term (years)</span>
|
||||
<input
|
||||
type="number"
|
||||
name="loan_term"
|
||||
value={form.loan_term}
|
||||
onChange={handleFieldChange}
|
||||
className="mt-1 w-full border rounded p-2"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<label className="block font-medium">Extra Monthly Payment</label>
|
||||
<input
|
||||
type="number"
|
||||
name="extra_payment"
|
||||
value={form.extra_payment}
|
||||
onChange={handleFieldChange}
|
||||
className="w-full border rounded p-2"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<label className="block font-medium">Expected Salary After Graduation</label>
|
||||
<input
|
||||
type="number"
|
||||
name="expected_salary"
|
||||
value={form.expected_salary}
|
||||
onChange={handleFieldChange}
|
||||
className="w-full border rounded p-2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<p>
|
||||
User is neither currently enrolled nor a prospective student – nothing to
|
||||
edit.
|
||||
</p>
|
||||
)}
|
||||
|
||||
{/* 11 │ Action buttons */}
|
||||
<div className="pt-4">
|
||||
<button
|
||||
onClick={() => nav(-1)}
|
||||
className="bg-gray-200 hover:bg-gray-300 text-gray-700 font-semibold py-2 px-4 rounded mr-3"
|
||||
>
|
||||
← Back
|
||||
</button>
|
||||
<button
|
||||
onClick={handleSave}
|
||||
disabled={!schoolValid || !programValid}
|
||||
className="bg-blue-600 hover:bg-blue-700 text-white font-semibold py-2 px-4 rounded"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
}
|
57
src/components/CollegeProfileList.js
Normal file
57
src/components/CollegeProfileList.js
Normal file
@ -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 (
|
||||
<div className="max-w-5xl mx-auto space-y-4">
|
||||
<h2 className="text-2xl font-semibold">College Profiles</h2>
|
||||
|
||||
<table className="w-full border text-sm">
|
||||
<thead className="bg-gray-100">
|
||||
<tr>
|
||||
<th className="p-2 text-left">Career</th>
|
||||
<th className="p-2 text-left">School</th>
|
||||
<th className="p-2 text-left">Program</th>
|
||||
<th className="p-2">Created</th>
|
||||
<th className="p-2"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{rows.map(r => (
|
||||
<tr key={r.id} className="border-t">
|
||||
<td className="p-2">{r.career_title}</td>
|
||||
<td className="p-2">{r.selected_school}</td>
|
||||
<td className="p-2">{r.selected_program}</td>
|
||||
<td className="p-2">{r.created_at?.slice(0,10)}</td>
|
||||
<td className="p-2">
|
||||
<Link
|
||||
to={`/profile/college/${r.career_profile_id}/${r.id}`}
|
||||
className="underline text-blue-600"
|
||||
>
|
||||
edit
|
||||
</Link>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
{rows.length === 0 && (
|
||||
<tr><td colSpan={5} className="p-4 text-center text-gray-500">
|
||||
No college profiles yet
|
||||
</td></tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -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 (
|
||||
<form onSubmit={handleSubmit} className="max-w-2xl mx-auto p-4 space-y-4 bg-white shadow rounded">
|
||||
<h2 className="text-xl font-semibold">Edit Your Financial Profile</h2>
|
||||
<>
|
||||
<form
|
||||
onSubmit={handleSubmit}
|
||||
className="max-w-2xl mx-auto p-6 space-y-4 bg-white shadow rounded"
|
||||
>
|
||||
<h2 className="text-xl font-semibold">Edit Your Financial Profile</h2>
|
||||
|
||||
<label className="block font-medium">Current Salary</label>
|
||||
<input
|
||||
type="number"
|
||||
value={currentSalary}
|
||||
onChange={(e) => setCurrentSalary(e.target.value)}
|
||||
className="w-full border rounded p-2"
|
||||
placeholder="$"
|
||||
/>
|
||||
{/* salary / income */}
|
||||
<label className="block font-medium">Current Annual Salary</label>
|
||||
<input type="number" className="w-full border rounded p-2"
|
||||
name="currentSalary" value={currentSalary} onChange={handleChange} />
|
||||
|
||||
<label className="block font-medium">Additional Monthly Income</label>
|
||||
<input
|
||||
type="number"
|
||||
value={additionalIncome}
|
||||
onChange={(e) => setAdditionalIncome(e.target.value)}
|
||||
className="w-full border rounded p-2"
|
||||
placeholder="$"
|
||||
/>
|
||||
<label className="block font-medium">Additional Annual Income</label>
|
||||
<input type="number" className="w-full border rounded p-2"
|
||||
name="additionalIncome" value={additionalIncome} onChange={handleChange} />
|
||||
|
||||
<label className="block font-medium">Monthly Living Expenses</label>
|
||||
<input
|
||||
type="number"
|
||||
value={monthlyExpenses}
|
||||
onChange={(e) => setMonthlyExpenses(e.target.value)}
|
||||
className="w-full border rounded p-2"
|
||||
placeholder="$"
|
||||
/>
|
||||
{/* expenses with wizard */}
|
||||
<label className="block font-medium">Monthly Living Expenses</label>
|
||||
<div className="flex space-x-2 items-center">
|
||||
<input type="number" className="w-full border rounded p-2"
|
||||
value={monthlyExpenses}
|
||||
onChange={e=>setMonthlyExpenses(e.target.value)} />
|
||||
<Button className="bg-blue-600 text-white px-3 py-2 rounded"
|
||||
type="button" onClick={openWizard}>
|
||||
Need Help?
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<label className="block font-medium">Monthly Debt Payments</label>
|
||||
<input
|
||||
type="number"
|
||||
value={monthlyDebtPayments}
|
||||
onChange={(e) => setMonthlyDebtPayments(e.target.value)}
|
||||
className="w-full border rounded p-2"
|
||||
placeholder="$"
|
||||
/>
|
||||
{/* rest of the numeric fields */}
|
||||
<label className="block font-medium">Monthly Debt Payments</label>
|
||||
<input type="number" className="w-full border rounded p-2"
|
||||
name="monthlyDebtPayments" value={monthlyDebtPayments} onChange={handleChange} />
|
||||
|
||||
<label className="block font-medium">Retirement Savings</label>
|
||||
<input
|
||||
type="number"
|
||||
value={retirementSavings}
|
||||
onChange={(e) => setRetirementSavings(e.target.value)}
|
||||
className="w-full border rounded p-2"
|
||||
placeholder="$"
|
||||
/>
|
||||
<label className="block font-medium">Retirement Savings</label>
|
||||
<input type="number" className="w-full border rounded p-2"
|
||||
name="retirementSavings" value={retirementSavings} onChange={handleChange} />
|
||||
|
||||
<label className="block font-medium">Emergency Fund</label>
|
||||
<input
|
||||
type="number"
|
||||
value={emergencyFund}
|
||||
onChange={(e) => setEmergencyFund(e.target.value)}
|
||||
className="w-full border rounded p-2"
|
||||
placeholder="$"
|
||||
/>
|
||||
<label className="block font-medium">Emergency Fund</label>
|
||||
<input type="number" className="w-full border rounded p-2"
|
||||
name="emergencyFund" value={emergencyFund} onChange={handleChange} />
|
||||
|
||||
<label className="block font-medium">Monthly Retirement Contribution</label>
|
||||
<input
|
||||
type="number"
|
||||
value={retirementContribution}
|
||||
onChange={(e) => setRetirementContribution(e.target.value)}
|
||||
className="w-full border rounded p-2"
|
||||
placeholder="$"
|
||||
/>
|
||||
<label className="block font-medium">Monthly Retirement Contribution</label>
|
||||
<input type="number" className="w-full border rounded p-2"
|
||||
name="retirementContribution" value={retirementContribution} onChange={handleChange} />
|
||||
|
||||
<label className="block font-medium">Monthly Emergency Contribution</label>
|
||||
<input
|
||||
type="number"
|
||||
value={monthlyEmergencyContribution}
|
||||
onChange={(e) => setMonthlyEmergencyContribution(e.target.value)}
|
||||
className="w-full border rounded p-2"
|
||||
placeholder="$"
|
||||
/>
|
||||
<label className="block font-medium">Monthly Emergency Contribution</label>
|
||||
<input type="number" className="w-full border rounded p-2"
|
||||
name="emergencyContribution"
|
||||
value={emergencyContribution}
|
||||
onChange={handleChange} />
|
||||
|
||||
<label className="block font-medium">Extra Cash to Emergency (%)</label>
|
||||
<input
|
||||
type="number"
|
||||
value={extraCashEmergencyPct}
|
||||
onChange={(e) => setExtraCashEmergencyPct(e.target.value)}
|
||||
className="w-full border rounded p-2"
|
||||
placeholder="e.g. 30"
|
||||
/>
|
||||
{/* allocation – kept in sync */}
|
||||
<h3 className="text-lg font-medium pt-2">Extra Monthly Cash Allocation (must total 100%)</h3>
|
||||
|
||||
<label className="block font-medium">Extra Cash to Retirement (%)</label>
|
||||
<input
|
||||
type="number"
|
||||
value={extraCashRetirementPct}
|
||||
onChange={(e) => setExtraCashRetirementPct(e.target.value)}
|
||||
className="w-full border rounded p-2"
|
||||
placeholder="e.g. 70"
|
||||
/>
|
||||
<label className="block font-medium">To Emergency Fund (%)</label>
|
||||
<input type="number" className="w-full border rounded p-2"
|
||||
name="extraCashEmergencyPct"
|
||||
value={extraCashEmergencyPct}
|
||||
onChange={handleChange} />
|
||||
|
||||
<button type="submit" className="bg-blue-600 text-white px-4 py-2 rounded">
|
||||
Save and Continue
|
||||
</button>
|
||||
</form>
|
||||
<label className="block font-medium">To Retirement (%)</label>
|
||||
<input type="number" className="w-full border rounded p-2"
|
||||
name="extraCashRetirementPct"
|
||||
value={extraCashRetirementPct}
|
||||
onChange={handleChange} />
|
||||
|
||||
{/* action buttons */}
|
||||
<div className="pt-4 flex justify-between">
|
||||
<button
|
||||
type="button"
|
||||
onClick={()=>nav(-1)}
|
||||
className="bg-gray-200 hover:bg-gray-300 text-gray-700 font-semibold py-2 px-4 rounded"
|
||||
>
|
||||
← Back
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
className="bg-blue-600 hover:bg-blue-700 text-white font-semibold py-2 px-4 rounded"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{/* wizard modal */}
|
||||
{showExpensesWizard && (
|
||||
<Modal onClose={closeWizard}>
|
||||
<ExpensesWizard
|
||||
onClose={closeWizard}
|
||||
onExpensesCalculated={total => {
|
||||
setMonthlyExpenses(total);
|
||||
closeWizard();
|
||||
}}
|
||||
/>
|
||||
</Modal>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default FinancialProfileForm;
|
||||
|
@ -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() {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="block font-medium">Projected End Date (optional):</label>
|
||||
<input
|
||||
name="projected_end_date"
|
||||
type="date"
|
||||
onChange={handleChange}
|
||||
value={data.projected_end_date || ''}
|
||||
className="w-full border rounded p-2"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="block font-medium">
|
||||
Are you currently enrolled in college or planning to enroll? <Req />
|
||||
|
@ -48,7 +48,6 @@ function ReviewPage({
|
||||
<div><strong>College enrollment Status:</strong> {careerData.college_enrollment_status || 'N/A'}</div>
|
||||
<div><strong>Status:</strong> {careerData.status || 'N/A'}</div>
|
||||
<div><strong>Start Date:</strong> {careerData.start_date || 'N/A'}</div>
|
||||
<div><strong>Projected End Date:</strong> {careerData.projected_end_date || 'N/A'}</div>
|
||||
<div><strong>Career Goals:</strong> {careerData.career_goals || 'N/A'}</div>
|
||||
</div>
|
||||
|
||||
|
@ -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.
|
||||
</p>
|
||||
<div className="grid grid-cols-1 gap-4">
|
||||
<Button onClick={() => navigate('/financial-profile')}>Update Financial Profile</Button>
|
||||
<Button onClick={() => navigate('/retirement-planner')}>Set Retirement Milestones and get AI help with planning</Button>
|
||||
<Button onClick={() => navigate('/retirement-planner')}>Compare different retirement scenarios and get AI help with planning</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -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"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Projected End Date
|
||||
</label>
|
||||
<input
|
||||
type="date"
|
||||
name="projected_end_date"
|
||||
value={formData.projected_end_date || ''}
|
||||
onChange={handleFormChange}
|
||||
className="border border-gray-300 rounded p-2 w-full"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Retirement date */}
|
||||
|
@ -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,
|
||||
|
Loading…
Reference in New Issue
Block a user