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 = `
|
const sql = `
|
||||||
SELECT
|
SELECT
|
||||||
*,
|
*,
|
||||||
DATE_FORMAT(start_date, '%Y-%m-%d') AS start_date,
|
DATE_FORMAT(start_date, '%Y-%m-%d') AS start_date
|
||||||
DATE_FORMAT(projected_end_date, '%Y-%m-%d') AS projected_end_date
|
|
||||||
FROM career_profiles
|
FROM career_profiles
|
||||||
WHERE user_id = ?
|
WHERE user_id = ?
|
||||||
ORDER BY start_date DESC
|
ORDER BY start_date DESC
|
||||||
@ -214,8 +213,7 @@ app.get('/api/premium/career-profile/all', authenticatePremiumUser, async (req,
|
|||||||
const sql = `
|
const sql = `
|
||||||
SELECT
|
SELECT
|
||||||
*,
|
*,
|
||||||
DATE_FORMAT(start_date, '%Y-%m-%d') AS start_date,
|
DATE_FORMAT(start_date, '%Y-%m-%d') AS start_date
|
||||||
DATE_FORMAT(projected_end_date, '%Y-%m-%d') AS projected_end_date
|
|
||||||
FROM career_profiles
|
FROM career_profiles
|
||||||
WHERE user_id = ?
|
WHERE user_id = ?
|
||||||
ORDER BY start_date ASC
|
ORDER BY start_date ASC
|
||||||
@ -235,8 +233,7 @@ app.get('/api/premium/career-profile/:careerProfileId', authenticatePremiumUser,
|
|||||||
const sql = `
|
const sql = `
|
||||||
SELECT
|
SELECT
|
||||||
*,
|
*,
|
||||||
DATE_FORMAT(start_date, '%Y-%m-%d') AS start_date,
|
DATE_FORMAT(start_date, '%Y-%m-%d') AS start_date
|
||||||
DATE_FORMAT(projected_end_date, '%Y-%m-%d') AS projected_end_date
|
|
||||||
FROM career_profiles
|
FROM career_profiles
|
||||||
WHERE id = ?
|
WHERE id = ?
|
||||||
AND user_id = ?
|
AND user_id = ?
|
||||||
@ -262,7 +259,6 @@ app.post('/api/premium/career-profile', authenticatePremiumUser, async (req, res
|
|||||||
career_name,
|
career_name,
|
||||||
status,
|
status,
|
||||||
start_date,
|
start_date,
|
||||||
projected_end_date,
|
|
||||||
college_enrollment_status,
|
college_enrollment_status,
|
||||||
currently_working,
|
currently_working,
|
||||||
career_goals,
|
career_goals,
|
||||||
@ -295,7 +291,6 @@ app.post('/api/premium/career-profile', authenticatePremiumUser, async (req, res
|
|||||||
career_name,
|
career_name,
|
||||||
status,
|
status,
|
||||||
start_date,
|
start_date,
|
||||||
projected_end_date,
|
|
||||||
college_enrollment_status,
|
college_enrollment_status,
|
||||||
currently_working,
|
currently_working,
|
||||||
career_goals,
|
career_goals,
|
||||||
@ -309,11 +304,10 @@ app.post('/api/premium/career-profile', authenticatePremiumUser, async (req, res
|
|||||||
planned_surplus_retirement_pct,
|
planned_surplus_retirement_pct,
|
||||||
planned_additional_income
|
planned_additional_income
|
||||||
)
|
)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
ON DUPLICATE KEY UPDATE
|
ON DUPLICATE KEY UPDATE
|
||||||
status = VALUES(status),
|
status = VALUES(status),
|
||||||
start_date = VALUES(start_date),
|
start_date = VALUES(start_date),
|
||||||
projected_end_date = VALUES(projected_end_date),
|
|
||||||
college_enrollment_status = VALUES(college_enrollment_status),
|
college_enrollment_status = VALUES(college_enrollment_status),
|
||||||
currently_working = VALUES(currently_working),
|
currently_working = VALUES(currently_working),
|
||||||
career_goals = VALUES(career_goals),
|
career_goals = VALUES(career_goals),
|
||||||
@ -336,7 +330,6 @@ app.post('/api/premium/career-profile', authenticatePremiumUser, async (req, res
|
|||||||
career_name,
|
career_name,
|
||||||
status || 'planned',
|
status || 'planned',
|
||||||
start_date || null,
|
start_date || null,
|
||||||
projected_end_date || null,
|
|
||||||
college_enrollment_status || null,
|
college_enrollment_status || null,
|
||||||
currently_working || null,
|
currently_working || null,
|
||||||
career_goals || null,
|
career_goals || null,
|
||||||
@ -2453,14 +2446,14 @@ app.delete('/api/premium/milestones/:milestoneId', authenticatePremiumUser, asyn
|
|||||||
------------------------------------------------------------------ */
|
------------------------------------------------------------------ */
|
||||||
|
|
||||||
// GET /api/premium/financial-profile
|
// GET /api/premium/financial-profile
|
||||||
app.get('/api/premium/financial-profile', auth, (req, res) => {
|
app.get('/api/premium/financial-profile', authenticatePremiumUser, async (req, res) => {
|
||||||
const uid = req.userId;
|
try {
|
||||||
db.query('SELECT * FROM financial_profile WHERE user_id=?', [uid],
|
const [rows] = await pool.query(
|
||||||
(err, rows) => {
|
'SELECT * FROM financial_profiles WHERE user_id=? LIMIT 1',
|
||||||
if (err) return res.status(500).json({ error:'DB error' });
|
[req.id]
|
||||||
|
);
|
||||||
|
|
||||||
if (!rows.length) {
|
if (!rows.length) {
|
||||||
// ←———— send a benign default instead of 404
|
|
||||||
return res.json({
|
return res.json({
|
||||||
current_salary: 0,
|
current_salary: 0,
|
||||||
additional_income: 0,
|
additional_income: 0,
|
||||||
@ -2475,7 +2468,10 @@ app.get('/api/premium/financial-profile', auth, (req, res) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
res.json(rows[0]);
|
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) => {
|
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
|
AI-SUGGESTED MILESTONES
|
||||||
------------------------------------------------------------------ */
|
------------------------------------------------------------------ */
|
||||||
|
35
src/App.js
35
src/App.js
@ -25,6 +25,10 @@ import InterestInventory from './components/InterestInventory.js';
|
|||||||
import Dashboard from './components/Dashboard.js';
|
import Dashboard from './components/Dashboard.js';
|
||||||
import UserProfile from './components/UserProfile.js';
|
import UserProfile from './components/UserProfile.js';
|
||||||
import FinancialProfileForm from './components/FinancialProfileForm.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 CareerRoadmap from './components/CareerRoadmap.js';
|
||||||
import Paywall from './components/Paywall.js';
|
import Paywall from './components/Paywall.js';
|
||||||
import OnboardingContainer from './components/PremiumOnboarding/OnboardingContainer.js';
|
import OnboardingContainer from './components/PremiumOnboarding/OnboardingContainer.js';
|
||||||
@ -239,7 +243,7 @@ const uiToolHandlers = useMemo(() => {
|
|||||||
{/* Header */}
|
{/* Header */}
|
||||||
<header className="flex items-center justify-between border-b bg-white px-6 py-4 shadow-sm relative">
|
<header className="flex items-center justify-between border-b bg-white px-6 py-4 shadow-sm relative">
|
||||||
<h1 className="text-lg font-semibold">
|
<h1 className="text-lg font-semibold">
|
||||||
AptivaAI - Career Guidance Platform (beta)
|
AptivaAI - Career Guidance Platform
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
{isAuthenticated && (
|
{isAuthenticated && (
|
||||||
@ -360,7 +364,7 @@ const uiToolHandlers = useMemo(() => {
|
|||||||
)}
|
)}
|
||||||
onClick={() => navigate('/retirement')}
|
onClick={() => navigate('/retirement')}
|
||||||
>
|
>
|
||||||
Retirement Planning
|
Retirement Planning (beta)
|
||||||
{!canAccessPremium && (
|
{!canAccessPremium && (
|
||||||
<span className="text-xs ml-1 text-gray-600">
|
<span className="text-xs ml-1 text-gray-600">
|
||||||
(Premium)
|
(Premium)
|
||||||
@ -407,19 +411,31 @@ const uiToolHandlers = useMemo(() => {
|
|||||||
{canAccessPremium ? (
|
{canAccessPremium ? (
|
||||||
/* Premium users go straight to the wizard */
|
/* Premium users go straight to the wizard */
|
||||||
<Link
|
<Link
|
||||||
to="/premium-onboarding"
|
to="/profile/careers"
|
||||||
className="block px-4 py-2 hover:bg-gray-100 text-sm text-gray-700"
|
className="block px-4 py-2 hover:bg-gray-100 text-sm text-gray-700"
|
||||||
>
|
>
|
||||||
Premium Onboarding
|
Career Profiles
|
||||||
</Link>
|
</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
|
<Link
|
||||||
to="/paywall"
|
to="/profile/college"
|
||||||
className="block px-4 py-2 hover:bg-gray-100 text-sm text-gray-700"
|
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>
|
</Link>
|
||||||
|
) : (
|
||||||
|
<span className="block px-4 py-2 text-sm text-gray-400 cursor-not-allowed">
|
||||||
|
College Profiles (Premium)
|
||||||
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -533,6 +549,11 @@ const uiToolHandlers = useMemo(() => {
|
|||||||
</PremiumRoute>
|
</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
|
<Route
|
||||||
path="/financial-profile"
|
path="/financial-profile"
|
||||||
element={
|
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,9 +1,19 @@
|
|||||||
// FinancialProfileForm.js
|
// FinancialProfileForm.js
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import authFetch from '../utils/authFetch.js';
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
function FinancialProfileForm() {
|
import authFetch from '../utils/authFetch.js';
|
||||||
// We'll store the fields in local state
|
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 [currentSalary, setCurrentSalary] = useState('');
|
||||||
const [additionalIncome, setAdditionalIncome] = useState('');
|
const [additionalIncome, setAdditionalIncome] = useState('');
|
||||||
const [monthlyExpenses, setMonthlyExpenses] = useState('');
|
const [monthlyExpenses, setMonthlyExpenses] = useState('');
|
||||||
@ -11,42 +21,72 @@ function FinancialProfileForm() {
|
|||||||
const [retirementSavings, setRetirementSavings] = useState('');
|
const [retirementSavings, setRetirementSavings] = useState('');
|
||||||
const [emergencyFund, setEmergencyFund] = useState('');
|
const [emergencyFund, setEmergencyFund] = useState('');
|
||||||
const [retirementContribution, setRetirementContribution] = useState('');
|
const [retirementContribution, setRetirementContribution] = useState('');
|
||||||
const [monthlyEmergencyContribution, setMonthlyEmergencyContribution] = useState('');
|
const [emergencyContribution, setEmergencyContribution] = useState('');
|
||||||
const [extraCashEmergencyPct, setExtraCashEmergencyPct] = useState('');
|
const [extraCashEmergencyPct, setExtraCashEmergencyPct] = useState('50');
|
||||||
const [extraCashRetirementPct, setExtraCashRetirementPct] = useState('');
|
const [extraCashRetirementPct, setExtraCashRetirementPct] = useState('50');
|
||||||
|
|
||||||
|
/* wizard modal */
|
||||||
|
const [showExpensesWizard, setShowExpensesWizard] = useState(false);
|
||||||
|
const openWizard = () => setShowExpensesWizard(true);
|
||||||
|
const closeWizard = () => setShowExpensesWizard(false);
|
||||||
|
|
||||||
|
/* ───────────── preload existing row ───────── */
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// On mount, fetch the user's existing profile from the new financial_profiles table
|
(async () => {
|
||||||
async function fetchProfile() {
|
|
||||||
try {
|
try {
|
||||||
const res = await authFetch('/api/premium/financial-profile', {
|
const res = await authFetch('/api/premium/financial-profile');
|
||||||
method: 'GET'
|
if (!res.ok) return;
|
||||||
});
|
const d = await res.json();
|
||||||
if (res.ok) {
|
|
||||||
const data = await res.json();
|
setCurrentSalary (d.current_salary ?? '');
|
||||||
// data might be an empty object if no row yet
|
setAdditionalIncome (d.additional_income ?? '');
|
||||||
setCurrentSalary(data.current_salary || '');
|
setMonthlyExpenses (d.monthly_expenses ?? '');
|
||||||
setAdditionalIncome(data.additional_income || '');
|
setMonthlyDebtPayments (d.monthly_debt_payments ?? '');
|
||||||
setMonthlyExpenses(data.monthly_expenses || '');
|
setRetirementSavings (d.retirement_savings ?? '');
|
||||||
setMonthlyDebtPayments(data.monthly_debt_payments || '');
|
setEmergencyFund (d.emergency_fund ?? '');
|
||||||
setRetirementSavings(data.retirement_savings || '');
|
setRetirementContribution (d.retirement_contribution ?? '');
|
||||||
setEmergencyFund(data.emergency_fund || '');
|
setEmergencyContribution (d.emergency_contribution ?? '');
|
||||||
setRetirementContribution(data.retirement_contribution || '');
|
setExtraCashEmergencyPct (d.extra_cash_emergency_pct ?? '');
|
||||||
setMonthlyEmergencyContribution(data.monthly_emergency_contribution || '');
|
setExtraCashRetirementPct (d.extra_cash_retirement_pct ?? '');
|
||||||
setExtraCashEmergencyPct(data.extra_cash_emergency_pct || '');
|
} catch (err) { console.error(err); }
|
||||||
setExtraCashRetirementPct(data.extra_cash_retirement_pct || '');
|
})();
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error("Failed to load financial profile:", err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fetchProfile();
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Submit form updates => POST to the same endpoint
|
/* -----------------------------------------------------------
|
||||||
|
* 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
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ───────────── submit ─────────────────────── */
|
||||||
async function handleSubmit(e) {
|
async function handleSubmit(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
try {
|
|
||||||
const body = {
|
const body = {
|
||||||
current_salary: parseFloat(currentSalary) || 0,
|
current_salary: parseFloat(currentSalary) || 0,
|
||||||
additional_income: parseFloat(additionalIncome) || 0,
|
additional_income: parseFloat(additionalIncome) || 0,
|
||||||
@ -55,127 +95,125 @@ function FinancialProfileForm() {
|
|||||||
retirement_savings: parseFloat(retirementSavings) || 0,
|
retirement_savings: parseFloat(retirementSavings) || 0,
|
||||||
emergency_fund: parseFloat(emergencyFund) || 0,
|
emergency_fund: parseFloat(emergencyFund) || 0,
|
||||||
retirement_contribution: parseFloat(retirementContribution) || 0,
|
retirement_contribution: parseFloat(retirementContribution) || 0,
|
||||||
monthly_emergency_contribution: parseFloat(monthlyEmergencyContribution) || 0,
|
emergency_contribution: parseFloat(emergencyContribution) || 0,
|
||||||
extra_cash_emergency_pct: parseFloat(extraCashEmergencyPct) || 0,
|
extra_cash_emergency_pct: pct(extraCashEmergencyPct),
|
||||||
extra_cash_retirement_pct: parseFloat(extraCashRetirementPct) || 0
|
extra_cash_retirement_pct: pct(extraCashRetirementPct)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
const res = await authFetch('/api/premium/financial-profile', {
|
const res = await authFetch('/api/premium/financial-profile', {
|
||||||
method : 'POST',
|
method : 'POST',
|
||||||
headers: { 'Content-Type':'application/json' },
|
headers: { 'Content-Type':'application/json' },
|
||||||
body : JSON.stringify(body)
|
body : JSON.stringify(body)
|
||||||
});
|
});
|
||||||
|
if (!res.ok) throw new Error(await res.text());
|
||||||
if (res.ok) {
|
alert('Financial profile saved.');
|
||||||
// show success or redirect
|
nav(-1);
|
||||||
console.log("Profile updated");
|
|
||||||
} else {
|
|
||||||
console.error("Failed to update profile:", await res.text());
|
|
||||||
}
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Error submitting financial profile:", err);
|
console.error(err);
|
||||||
|
alert('Failed to save financial profile.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ───────────── view ───────────────────────── */
|
||||||
return (
|
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>
|
{/* salary / income */}
|
||||||
<input
|
<label className="block font-medium">Current Annual Salary</label>
|
||||||
type="number"
|
<input type="number" className="w-full border rounded p-2"
|
||||||
value={currentSalary}
|
name="currentSalary" value={currentSalary} onChange={handleChange} />
|
||||||
onChange={(e) => setCurrentSalary(e.target.value)}
|
|
||||||
className="w-full border rounded p-2"
|
|
||||||
placeholder="$"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<label className="block font-medium">Additional Monthly Income</label>
|
<label className="block font-medium">Additional Annual Income</label>
|
||||||
<input
|
<input type="number" className="w-full border rounded p-2"
|
||||||
type="number"
|
name="additionalIncome" value={additionalIncome} onChange={handleChange} />
|
||||||
value={additionalIncome}
|
|
||||||
onChange={(e) => setAdditionalIncome(e.target.value)}
|
|
||||||
className="w-full border rounded p-2"
|
|
||||||
placeholder="$"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<label className="block font-medium">Monthly Living Expenses</label>
|
{/* expenses with wizard */}
|
||||||
<input
|
<label className="block font-medium">Monthly Living Expenses</label>
|
||||||
type="number"
|
<div className="flex space-x-2 items-center">
|
||||||
|
<input type="number" className="w-full border rounded p-2"
|
||||||
value={monthlyExpenses}
|
value={monthlyExpenses}
|
||||||
onChange={(e) => setMonthlyExpenses(e.target.value)}
|
onChange={e=>setMonthlyExpenses(e.target.value)} />
|
||||||
className="w-full border rounded p-2"
|
<Button className="bg-blue-600 text-white px-3 py-2 rounded"
|
||||||
placeholder="$"
|
type="button" onClick={openWizard}>
|
||||||
/>
|
Need Help?
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<label className="block font-medium">Monthly Debt Payments</label>
|
{/* rest of the numeric fields */}
|
||||||
<input
|
<label className="block font-medium">Monthly Debt Payments</label>
|
||||||
type="number"
|
<input type="number" className="w-full border rounded p-2"
|
||||||
value={monthlyDebtPayments}
|
name="monthlyDebtPayments" value={monthlyDebtPayments} onChange={handleChange} />
|
||||||
onChange={(e) => setMonthlyDebtPayments(e.target.value)}
|
|
||||||
className="w-full border rounded p-2"
|
|
||||||
placeholder="$"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<label className="block font-medium">Retirement Savings</label>
|
<label className="block font-medium">Retirement Savings</label>
|
||||||
<input
|
<input type="number" className="w-full border rounded p-2"
|
||||||
type="number"
|
name="retirementSavings" value={retirementSavings} onChange={handleChange} />
|
||||||
value={retirementSavings}
|
|
||||||
onChange={(e) => setRetirementSavings(e.target.value)}
|
|
||||||
className="w-full border rounded p-2"
|
|
||||||
placeholder="$"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<label className="block font-medium">Emergency Fund</label>
|
<label className="block font-medium">Emergency Fund</label>
|
||||||
<input
|
<input type="number" className="w-full border rounded p-2"
|
||||||
type="number"
|
name="emergencyFund" value={emergencyFund} onChange={handleChange} />
|
||||||
value={emergencyFund}
|
|
||||||
onChange={(e) => setEmergencyFund(e.target.value)}
|
|
||||||
className="w-full border rounded p-2"
|
|
||||||
placeholder="$"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<label className="block font-medium">Monthly Retirement Contribution</label>
|
<label className="block font-medium">Monthly Retirement Contribution</label>
|
||||||
<input
|
<input type="number" className="w-full border rounded p-2"
|
||||||
type="number"
|
name="retirementContribution" value={retirementContribution} onChange={handleChange} />
|
||||||
value={retirementContribution}
|
|
||||||
onChange={(e) => setRetirementContribution(e.target.value)}
|
|
||||||
className="w-full border rounded p-2"
|
|
||||||
placeholder="$"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<label className="block font-medium">Monthly Emergency Contribution</label>
|
<label className="block font-medium">Monthly Emergency Contribution</label>
|
||||||
<input
|
<input type="number" className="w-full border rounded p-2"
|
||||||
type="number"
|
name="emergencyContribution"
|
||||||
value={monthlyEmergencyContribution}
|
value={emergencyContribution}
|
||||||
onChange={(e) => setMonthlyEmergencyContribution(e.target.value)}
|
onChange={handleChange} />
|
||||||
className="w-full border rounded p-2"
|
|
||||||
placeholder="$"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<label className="block font-medium">Extra Cash to Emergency (%)</label>
|
{/* allocation – kept in sync */}
|
||||||
<input
|
<h3 className="text-lg font-medium pt-2">Extra Monthly Cash Allocation (must total 100%)</h3>
|
||||||
type="number"
|
|
||||||
|
<label className="block font-medium">To Emergency Fund (%)</label>
|
||||||
|
<input type="number" className="w-full border rounded p-2"
|
||||||
|
name="extraCashEmergencyPct"
|
||||||
value={extraCashEmergencyPct}
|
value={extraCashEmergencyPct}
|
||||||
onChange={(e) => setExtraCashEmergencyPct(e.target.value)}
|
onChange={handleChange} />
|
||||||
className="w-full border rounded p-2"
|
|
||||||
placeholder="e.g. 30"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<label className="block font-medium">Extra Cash to Retirement (%)</label>
|
<label className="block font-medium">To Retirement (%)</label>
|
||||||
<input
|
<input type="number" className="w-full border rounded p-2"
|
||||||
type="number"
|
name="extraCashRetirementPct"
|
||||||
value={extraCashRetirementPct}
|
value={extraCashRetirementPct}
|
||||||
onChange={(e) => setExtraCashRetirementPct(e.target.value)}
|
onChange={handleChange} />
|
||||||
className="w-full border rounded p-2"
|
|
||||||
placeholder="e.g. 70"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<button type="submit" className="bg-blue-600 text-white px-4 py-2 rounded">
|
{/* action buttons */}
|
||||||
Save and Continue
|
<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>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="bg-blue-600 hover:bg-blue-700 text-white font-semibold py-2 px-4 rounded"
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</form>
|
</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;
|
const skipFin = !!data.skipFinancialStep;
|
||||||
// 1) Grab the location state values, if any
|
// 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 ── */
|
/* ── 3. side‑effects when route brings a new career object ── */
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!navCareerObj?.title) return;
|
if (!navCareerObj?.title) return;
|
||||||
@ -166,17 +158,6 @@ function handleSubmit() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</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">
|
<div className="space-y-2">
|
||||||
<label className="block font-medium">
|
<label className="block font-medium">
|
||||||
Are you currently enrolled in college or planning to enroll? <Req />
|
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>College enrollment Status:</strong> {careerData.college_enrollment_status || 'N/A'}</div>
|
||||||
<div><strong>Status:</strong> {careerData.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>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><strong>Career Goals:</strong> {careerData.career_goals || 'N/A'}</div>
|
||||||
</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.
|
Plan strategically and financially for retirement. AptivaAI provides you with clear financial projections, milestone tracking, and scenario analysis for a secure future.
|
||||||
</p>
|
</p>
|
||||||
<div className="grid grid-cols-1 gap-4">
|
<div className="grid grid-cols-1 gap-4">
|
||||||
<Button onClick={() => navigate('/financial-profile')}>Update Financial Profile</Button>
|
<Button onClick={() => navigate('/retirement-planner')}>Compare different retirement scenarios and get AI help with planning</Button>
|
||||||
<Button onClick={() => navigate('/retirement-planner')}>Set Retirement Milestones and get AI help with planning</Button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -159,7 +159,6 @@ export default function ScenarioEditModal({
|
|||||||
career_name : safe(s.career_name),
|
career_name : safe(s.career_name),
|
||||||
status : safe(s.status || 'planned'),
|
status : safe(s.status || 'planned'),
|
||||||
start_date : safe(s.start_date),
|
start_date : safe(s.start_date),
|
||||||
projected_end_date : safe(s.projected_end_date),
|
|
||||||
retirement_start_date: safe(s.retirement_start_date),
|
retirement_start_date: safe(s.retirement_start_date),
|
||||||
desired_retirement_income_monthly : safe(
|
desired_retirement_income_monthly : safe(
|
||||||
s.desired_retirement_income_monthly
|
s.desired_retirement_income_monthly
|
||||||
@ -498,7 +497,6 @@ async function handleSave() {
|
|||||||
currently_working : formData.currently_working || "no",
|
currently_working : formData.currently_working || "no",
|
||||||
status : s(formData.status),
|
status : s(formData.status),
|
||||||
start_date : s(formData.start_date),
|
start_date : s(formData.start_date),
|
||||||
projected_end_date : s(formData.projected_end_date),
|
|
||||||
retirement_start_date : s(formData.retirement_start_date),
|
retirement_start_date : s(formData.retirement_start_date),
|
||||||
desired_retirement_income_monthly : n(formData.desired_retirement_income_monthly),
|
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"
|
className="border border-gray-300 rounded p-2 w-full"
|
||||||
/>
|
/>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
|
|
||||||
{/* Retirement date */}
|
{/* Retirement date */}
|
||||||
|
@ -51,7 +51,6 @@ export default function ScenarioEditWizard({
|
|||||||
currently_working: scenData.currently_working,
|
currently_working: scenData.currently_working,
|
||||||
status: scenData.status,
|
status: scenData.status,
|
||||||
start_date: scenData.start_date,
|
start_date: scenData.start_date,
|
||||||
projected_end_date: scenData.projected_end_date,
|
|
||||||
planned_monthly_expenses: scenData.planned_monthly_expenses,
|
planned_monthly_expenses: scenData.planned_monthly_expenses,
|
||||||
planned_monthly_debt_payments: scenData.planned_monthly_debt_payments,
|
planned_monthly_debt_payments: scenData.planned_monthly_debt_payments,
|
||||||
planned_monthly_retirement_contribution: scenData.planned_monthly_retirement_contribution,
|
planned_monthly_retirement_contribution: scenData.planned_monthly_retirement_contribution,
|
||||||
|
Loading…
Reference in New Issue
Block a user