Added next-steps provided by AI to convert to milestones, fixed simulation. Where Am I Now should be finished.

This commit is contained in:
Josh 2025-05-27 18:47:44 +00:00
parent 0ea62392dd
commit 961d0e5fd4
5 changed files with 629 additions and 202 deletions

View File

@ -146,6 +146,10 @@ app.post('/api/premium/career-profile', authenticatePremiumUser, async (req, res
projected_end_date, projected_end_date,
college_enrollment_status, college_enrollment_status,
currently_working, currently_working,
// The new field:
career_goals,
// planned fields
planned_monthly_expenses, planned_monthly_expenses,
planned_monthly_debt_payments, planned_monthly_debt_payments,
planned_monthly_retirement_contribution, planned_monthly_retirement_contribution,
@ -162,6 +166,7 @@ app.post('/api/premium/career-profile', authenticatePremiumUser, async (req, res
try { try {
const newId = uuidv4(); const newId = uuidv4();
// 1) Insert includes career_goals
const sql = ` const sql = `
INSERT INTO career_profiles ( INSERT INTO career_profiles (
id, id,
@ -173,6 +178,7 @@ app.post('/api/premium/career-profile', authenticatePremiumUser, async (req, res
projected_end_date, projected_end_date,
college_enrollment_status, college_enrollment_status,
currently_working, currently_working,
career_goals, -- ADD THIS
planned_monthly_expenses, planned_monthly_expenses,
planned_monthly_debt_payments, planned_monthly_debt_payments,
planned_monthly_retirement_contribution, planned_monthly_retirement_contribution,
@ -181,13 +187,14 @@ 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), 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), -- ADD THIS
planned_monthly_expenses = VALUES(planned_monthly_expenses), planned_monthly_expenses = VALUES(planned_monthly_expenses),
planned_monthly_debt_payments = VALUES(planned_monthly_debt_payments), planned_monthly_debt_payments = VALUES(planned_monthly_debt_payments),
planned_monthly_retirement_contribution = VALUES(planned_monthly_retirement_contribution), planned_monthly_retirement_contribution = VALUES(planned_monthly_retirement_contribution),
@ -208,6 +215,7 @@ app.post('/api/premium/career-profile', authenticatePremiumUser, async (req, res
projected_end_date || null, projected_end_date || null,
college_enrollment_status || null, college_enrollment_status || null,
currently_working || null, currently_working || null,
career_goals || null, // pass career_goals here
planned_monthly_expenses ?? null, planned_monthly_expenses ?? null,
planned_monthly_debt_payments ?? null, planned_monthly_debt_payments ?? null,
planned_monthly_retirement_contribution ?? null, planned_monthly_retirement_contribution ?? null,
@ -218,11 +226,12 @@ app.post('/api/premium/career-profile', authenticatePremiumUser, async (req, res
]); ]);
// re-fetch to confirm ID // re-fetch to confirm ID
const [rows] = await pool.query(` const [rows] = await pool.query(
SELECT id `SELECT id
FROM career_profiles FROM career_profiles
WHERE id = ? WHERE id = ?`,
`, [newId]); [newId]
);
return res.status(200).json({ return res.status(200).json({
message: 'Career profile upserted.', message: 'Career profile upserted.',
@ -234,6 +243,7 @@ app.post('/api/premium/career-profile', authenticatePremiumUser, async (req, res
} }
}); });
// DELETE a career profile (scenario) by ID // DELETE a career profile (scenario) by ID
app.delete('/api/premium/career-profile/:careerProfileId', authenticatePremiumUser, async (req, res) => { app.delete('/api/premium/career-profile/:careerProfileId', authenticatePremiumUser, async (req, res) => {
const { careerProfileId } = req.params; const { careerProfileId } = req.params;
@ -302,6 +312,269 @@ app.delete('/api/premium/career-profile/:careerProfileId', authenticatePremiumUs
} }
}); });
/***************************************************
AI - NEXT STEPS ENDPOINT (with date constraints,
ignoring scenarioRow.start_date)
****************************************************/
app.post('/api/premium/ai/next-steps', authenticatePremiumUser, async (req, res) => {
try {
// 1) Gather user data from request
const {
userProfile = {},
scenarioRow = {},
financialProfile = {},
collegeProfile = {}
} = req.body;
// 2) Build a summary for ChatGPT
// (We'll ignore scenarioRow.start_date in the prompt)
const summaryText = buildUserSummary({
userProfile,
scenarioRow,
financialProfile,
collegeProfile
});
// 3) Dynamically compute "today's" date and future cutoffs
const now = new Date();
const isoToday = now.toISOString().slice(0, 10); // e.g. "2025-06-01"
// short-term = within 6 months
const shortTermLimit = new Date(now);
shortTermLimit.setMonth(shortTermLimit.getMonth() + 6);
const isoShortTermLimit = shortTermLimit.toISOString().slice(0, 10);
// long-term = 1-3 years
const oneYearFromNow = new Date(now);
oneYearFromNow.setFullYear(oneYearFromNow.getFullYear() + 1);
const isoOneYearFromNow = oneYearFromNow.toISOString().slice(0, 10);
const threeYearsFromNow = new Date(now);
threeYearsFromNow.setFullYear(threeYearsFromNow.getFullYear() + 3);
const isoThreeYearsFromNow = threeYearsFromNow.toISOString().slice(0, 10);
// 4) Construct ChatGPT messages
const messages = [
{
role: 'system',
content: `
You are an expert career & financial coach.
Today's date: ${isoToday}.
Short-term means any date up to ${isoShortTermLimit} (within 6 months).
Long-term means a date between ${isoOneYearFromNow} and ${isoThreeYearsFromNow} (1-3 years).
All milestone dates must be strictly >= ${isoToday}. Titles must be <= 5 words.
Respond ONLY in the requested JSON format.`
},
{
role: 'user',
content: `
Here is the user's current situation:
${summaryText}
Please provide exactly 3 short-term (within 6 months) and 2 long-term (13 years) milestones.
Each milestone must have:
- "title" (up to 5 words)
- "date" in YYYY-MM-DD format (>= ${isoToday})
- "description" (1-2 sentences)
Return ONLY a JSON array, no extra text:
[
{
"title": "string",
"date": "YYYY-MM-DD",
"description": "string"
},
...
]`
}
];
// 5) Call OpenAI (ignoring scenarioRow.start_date for date logic)
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
const completion = await openai.chat.completions.create({
model: 'gpt-3.5-turbo', // or 'gpt-4'
messages,
temperature: 0.7,
max_tokens: 600
});
// 6) Extract raw text
const aiAdvice = completion?.choices?.[0]?.message?.content?.trim() || 'No response';
res.json({ recommendations: aiAdvice });
} catch (err) {
console.error('Error in /api/premium/ai/next-steps =>', err);
res.status(500).json({ error: 'Failed to get AI next steps.' });
}
});
/**
* Helper that converts user data into a concise text summary.
* This can still mention scenarioRow, but we do NOT feed
* scenarioRow.start_date to ChatGPT for future date calculations.
*/
function buildUserSummary({
userProfile = {},
scenarioRow = {},
financialProfile = {},
collegeProfile = {}
}) {
// Provide a short multiline string about the user's finances, goals, etc.
// but avoid referencing scenarioRow.start_date
// e.g.:
const location = `${userProfile.state || 'Unknown State'}, ${userProfile.area || 'N/A'}`;
const careerName = scenarioRow.career_name || 'Unknown';
const careerGoals = scenarioRow.career_goals || 'No goals specified';
const status = scenarioRow.status || 'planned';
const currentlyWorking = scenarioRow.currently_working || 'no';
const currentSalary = financialProfile.current_salary || 0;
const monthlyExpenses = financialProfile.monthly_expenses || 0;
const monthlyDebt = financialProfile.monthly_debt_payments || 0;
const retirementSavings = financialProfile.retirement_savings || 0;
const emergencyFund = financialProfile.emergency_fund || 0;
// And similarly for collegeProfile if needed, ignoring start_date
return `
User Location: ${location}
Career Name: ${careerName}
Career Goals: ${careerGoals}
Career Status: ${status}
Currently Working: ${currentlyWorking}
Financial:
- Salary: \$${currentSalary}
- Monthly Expenses: \$${monthlyExpenses}
- Monthly Debt: \$${monthlyDebt}
- Retirement Savings: \$${retirementSavings}
- Emergency Fund: \$${emergencyFund}
`.trim();
}
/***************************************************
AI MILESTONE CONVERSION ENDPOINT
****************************************************/
app.post('/api/premium/milestone/convert-ai', authenticatePremiumUser, async (req, res) => {
try {
// The client passes us an array of milestones, e.g.:
// { milestones: [ { title, date, description, tasks, impacts }, ... ] }
const { milestones } = req.body;
const { careerProfileId } = req.query;
// or from body, if you prefer:
// const { careerProfileId } = req.body;
if (!careerProfileId) {
return res.status(400).json({ error: 'careerProfileId is required.' });
}
if (!Array.isArray(milestones)) {
return res.status(400).json({ error: 'Expected milestones array in body.' });
}
const newMilestones = [];
for (const m of milestones) {
// Required fields for your DB:
// title, date, career_profile_id
if (!m.title || !m.date) {
return res.status(400).json({
error: 'Missing required milestone fields (title/date).',
details: m
});
}
// create the milestone row
const id = uuidv4();
await pool.query(`
INSERT INTO milestones (
id,
user_id,
career_profile_id,
title,
description,
date,
progress,
status,
is_universal
) VALUES (?, ?, ?, ?, ?, ?, 0, 'planned', 0)
`, [
id,
req.id,
careerProfileId,
m.title,
m.description || '',
m.date
]);
// If the user also sent tasks in m.tasks:
if (Array.isArray(m.tasks)) {
for (const t of m.tasks) {
const taskId = uuidv4();
await pool.query(`
INSERT INTO tasks (
id,
milestone_id,
user_id,
title,
description,
due_date,
status
) VALUES (?, ?, ?, ?, ?, ?, 'not_started')
`, [
taskId,
id,
req.id,
t.title || 'Task',
t.description || '',
t.due_date || null
]);
}
}
// If the user also sent impacts in m.impacts:
if (Array.isArray(m.impacts)) {
for (const imp of m.impacts) {
const impactId = uuidv4();
await pool.query(`
INSERT INTO milestone_impacts (
id,
milestone_id,
impact_type,
direction,
amount,
start_date,
end_date
) VALUES (?, ?, ?, ?, ?, ?, ?)
`, [
impactId,
id,
imp.impact_type || 'none',
imp.direction || 'add',
imp.amount || 0,
imp.start_date || null,
imp.end_date || null
]);
}
}
newMilestones.push({
id,
title: m.title,
description: m.description || '',
date: m.date,
tasks: m.tasks || [],
impacts: m.impacts || []
});
}
return res.json({ createdMilestones: newMilestones });
} catch (err) {
console.error('Error converting AI milestones:', err);
return res.status(500).json({ error: 'Failed to convert AI milestones.' });
}
});
/* ------------------------------------------------------------------ /* ------------------------------------------------------------------
MILESTONE ENDPOINTS MILESTONE ENDPOINTS
------------------------------------------------------------------ */ ------------------------------------------------------------------ */
@ -316,18 +589,16 @@ app.post('/api/premium/milestone', authenticatePremiumUser, async (req, res) =>
const createdMilestones = []; const createdMilestones = [];
for (const m of body.milestones) { for (const m of body.milestones) {
const { const {
milestone_type,
title, title,
description, description,
date, date,
career_profile_id, career_profile_id,
progress, progress,
status, status,
new_salary,
is_universal is_universal
} = m; } = m;
if (!milestone_type || !title || !date || !career_profile_id) { if (!title || !date || !career_profile_id) {
return res.status(400).json({ return res.status(400).json({
error: 'One or more milestones missing required fields', error: 'One or more milestones missing required fields',
details: m details: m
@ -340,27 +611,23 @@ app.post('/api/premium/milestone', authenticatePremiumUser, async (req, res) =>
id, id,
user_id, user_id,
career_profile_id, career_profile_id,
milestone_type,
title, title,
description, description,
date, date,
progress, progress,
status, status,
new_salary,
is_universal is_universal
) )
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
`, [ `, [
id, id,
req.id, req.id,
career_profile_id, career_profile_id,
milestone_type,
title, title,
description || '', description || '',
date, date,
progress || 0, progress || 0,
status || 'planned', status || 'planned',
new_salary || null,
is_universal ? 1 : 0 is_universal ? 1 : 0
]); ]);
@ -368,13 +635,11 @@ app.post('/api/premium/milestone', authenticatePremiumUser, async (req, res) =>
id, id,
user_id: req.id, user_id: req.id,
career_profile_id, career_profile_id,
milestone_type,
title, title,
description: description || '', description: description || '',
date, date,
progress: progress || 0, progress: progress || 0,
status: status || 'planned', status: status || 'planned',
new_salary: new_salary || null,
is_universal: is_universal ? 1 : 0, is_universal: is_universal ? 1 : 0,
tasks: [] tasks: []
}); });
@ -384,21 +649,19 @@ app.post('/api/premium/milestone', authenticatePremiumUser, async (req, res) =>
// single milestone // single milestone
const { const {
milestone_type,
title, title,
description, description,
date, date,
career_profile_id, career_profile_id,
progress, progress,
status, status,
new_salary,
is_universal is_universal
} = body; } = body;
if (!milestone_type || !title || !date || !career_profile_id) { if ( !title || !date || !career_profile_id) {
return res.status(400).json({ return res.status(400).json({
error: 'Missing required fields', error: 'Missing required fields',
details: { milestone_type, title, date, career_profile_id } details: { title, date, career_profile_id }
}); });
} }
@ -408,27 +671,23 @@ app.post('/api/premium/milestone', authenticatePremiumUser, async (req, res) =>
id, id,
user_id, user_id,
career_profile_id, career_profile_id,
milestone_type,
title, title,
description, description,
date, date,
progress, progress,
status, status,
new_salary,
is_universal is_universal
) )
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
`, [ `, [
id, id,
req.id, req.id,
career_profile_id, career_profile_id,
milestone_type,
title, title,
description || '', description || '',
date, date,
progress || 0, progress || 0,
status || 'planned', status || 'planned',
new_salary || null,
is_universal ? 1 : 0 is_universal ? 1 : 0
]); ]);
@ -436,13 +695,11 @@ app.post('/api/premium/milestone', authenticatePremiumUser, async (req, res) =>
id, id,
user_id: req.id, user_id: req.id,
career_profile_id, career_profile_id,
milestone_type,
title, title,
description: description || '', description: description || '',
date, date,
progress: progress || 0, progress: progress || 0,
status: status || 'planned', status: status || 'planned',
new_salary: new_salary || null,
is_universal: is_universal ? 1 : 0, is_universal: is_universal ? 1 : 0,
tasks: [] tasks: []
}; };
@ -458,14 +715,12 @@ app.put('/api/premium/milestones/:milestoneId', authenticatePremiumUser, async (
try { try {
const { milestoneId } = req.params; const { milestoneId } = req.params;
const { const {
milestone_type,
title, title,
description, description,
date, date,
career_profile_id, career_profile_id,
progress, progress,
status, status,
new_salary,
is_universal is_universal
} = req.body; } = req.body;
@ -481,40 +736,34 @@ app.put('/api/premium/milestones/:milestoneId', authenticatePremiumUser, async (
} }
const row = existing[0]; const row = existing[0];
const finalMilestoneType = milestone_type || row.milestone_type;
const finalTitle = title || row.title; const finalTitle = title || row.title;
const finalDesc = description || row.description; const finalDesc = description || row.description;
const finalDate = date || row.date; const finalDate = date || row.date;
const finalCareerProfileId = career_profile_id || row.career_profile_id; const finalCareerProfileId = career_profile_id || row.career_profile_id;
const finalProgress = progress != null ? progress : row.progress; const finalProgress = progress != null ? progress : row.progress;
const finalStatus = status || row.status; const finalStatus = status || row.status;
const finalSalary = new_salary != null ? new_salary : row.new_salary;
const finalIsUniversal = is_universal != null ? (is_universal ? 1 : 0) : row.is_universal; const finalIsUniversal = is_universal != null ? (is_universal ? 1 : 0) : row.is_universal;
await pool.query(` await pool.query(`
UPDATE milestones UPDATE milestones
SET SET
milestone_type = ?,
title = ?, title = ?,
description = ?, description = ?,
date = ?, date = ?,
career_profile_id = ?, career_profile_id = ?,
progress = ?, progress = ?,
status = ?, status = ?,
new_salary = ?,
is_universal = ?, is_universal = ?,
updated_at = CURRENT_TIMESTAMP updated_at = CURRENT_TIMESTAMP
WHERE id = ? WHERE id = ?
AND user_id = ? AND user_id = ?
`, [ `, [
finalMilestoneType,
finalTitle, finalTitle,
finalDesc, finalDesc,
finalDate, finalDate,
finalCareerProfileId, finalCareerProfileId,
finalProgress, finalProgress,
finalStatus, finalStatus,
finalSalary,
finalIsUniversal, finalIsUniversal,
milestoneId, milestoneId,
req.id req.id
@ -683,28 +932,24 @@ app.post('/api/premium/milestone/copy', authenticatePremiumUser, async (req, res
id, id,
user_id, user_id,
career_profile_id, career_profile_id,
milestone_type,
title, title,
description, description,
date, date,
progress, progress,
status, status,
new_salary,
is_universal, is_universal,
origin_milestone_id origin_milestone_id
) )
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`, [ `, [
newMilestoneId, newMilestoneId,
req.id, req.id,
scenarioId, scenarioId,
original.milestone_type,
original.title, original.title,
original.description, original.description,
original.date, original.date,
original.progress, original.progress,
original.status, original.status,
original.new_salary,
1, 1,
originId originId
]); ]);
@ -1191,6 +1436,7 @@ Milestone Requirements:
- Must include at least one educational or professional development milestone explicitly. - Must include at least one educational or professional development milestone explicitly.
- Do NOT exclusively focus on financial aspects. - Do NOT exclusively focus on financial aspects.
2. Provide exactly 2 LONG-TERM milestones (3+ years out). 2. Provide exactly 2 LONG-TERM milestones (3+ years out).
- Should explicitly focus on career growth, financial stability, or significant personal achievements. - Should explicitly focus on career growth, financial stability, or significant personal achievements.

View File

@ -93,17 +93,6 @@ export default function MilestoneTimeline({
console.warn('No milestones in response:', data); console.warn('No milestones in response:', data);
return; return;
} }
// Separate them by type
const categorized = { Career: [], Financial: [] };
data.milestones.forEach((m) => {
if (categorized[m.milestone_type]) {
categorized[m.milestone_type].push(m);
} else {
// If there's a random type, log or store somewhere else
console.warn(`Unknown milestone type: ${m.milestone_type}`);
}
});
setMilestones(categorized);
} catch (err) { } catch (err) {
console.error('Failed to fetch milestones:', err); console.error('Failed to fetch milestones:', err);
} }
@ -150,7 +139,6 @@ export default function MilestoneTimeline({
description: m.description || '', description: m.description || '',
date: m.date || '', date: m.date || '',
progress: m.progress || 0, progress: m.progress || 0,
newSalary: m.new_salary || '',
impacts: fetchedImpacts.map((imp) => ({ impacts: fetchedImpacts.map((imp) => ({
id: imp.id, id: imp.id,
impact_type: imp.impact_type || 'ONE_TIME', impact_type: imp.impact_type || 'ONE_TIME',
@ -188,10 +176,6 @@ export default function MilestoneTimeline({
career_profile_id: careerProfileId, career_profile_id: careerProfileId,
progress: newMilestone.progress, progress: newMilestone.progress,
status: newMilestone.progress >= 100 ? 'completed' : 'planned', status: newMilestone.progress >= 100 ? 'completed' : 'planned',
new_salary:
activeView === 'Financial' && newMilestone.newSalary
? parseFloat(newMilestone.newSalary)
: null,
is_universal: newMilestone.isUniversal || 0 is_universal: newMilestone.isUniversal || 0
}; };

View File

@ -21,16 +21,15 @@ import { getFullStateName } from '../utils/stateUtils.js';
import { Button } from './ui/button.js'; import { Button } from './ui/button.js';
import CareerSelectDropdown from './CareerSelectDropdown.js'; import CareerSelectDropdown from './CareerSelectDropdown.js';
import CareerSearch from './CareerSearch.js';
import MilestoneTimeline from './MilestoneTimeline.js'; import MilestoneTimeline from './MilestoneTimeline.js';
import ScenarioEditModal from './ScenarioEditModal.js'; import ScenarioEditModal from './ScenarioEditModal.js';
// If you need AI suggestions in the future:
// import AISuggestedMilestones from './AISuggestedMilestones.js';
import './MilestoneTracker.css'; import './MilestoneTracker.css';
import './MilestoneTimeline.css'; import './MilestoneTimeline.css';
// --------------
// Register ChartJS Plugins
// --------------
ChartJS.register( ChartJS.register(
LineElement, LineElement,
BarElement, BarElement,
@ -43,17 +42,14 @@ ChartJS.register(
annotationPlugin annotationPlugin
); );
// ---------------------- // --------------
// 1) Remove decimals from SOC code // Helper Functions
// ---------------------- // --------------
function stripSocCode(fullSoc) { function stripSocCode(fullSoc) {
if (!fullSoc) return ''; if (!fullSoc) return '';
return fullSoc.split('.')[0]; return fullSoc.split('.')[0];
} }
// ----------------------
// 2) Salary Gauge
// ----------------------
function getRelativePosition(userSal, p10, p90) { function getRelativePosition(userSal, p10, p90) {
if (!p10 || !p90) return 0; if (!p10 || !p90) return 0;
if (userSal < p10) return 0; if (userSal < p10) return 0;
@ -61,6 +57,7 @@ function getRelativePosition(userSal, p10, p90) {
return (userSal - p10) / (p90 - p10); return (userSal - p10) / (p90 - p10);
} }
// A simple gauge for the users salary vs. percentiles
function SalaryGauge({ userSalary, percentileRow, prefix = 'regional' }) { function SalaryGauge({ userSalary, percentileRow, prefix = 'regional' }) {
if (!percentileRow) return null; if (!percentileRow) return null;
@ -91,6 +88,7 @@ function SalaryGauge({ userSalary, percentileRow, prefix = 'regional' }) {
marginBottom: '8px' marginBottom: '8px'
}} }}
> >
{/* Median Marker */}
<div <div
style={{ style={{
position: 'absolute', position: 'absolute',
@ -120,6 +118,7 @@ function SalaryGauge({ userSalary, percentileRow, prefix = 'regional' }) {
</div> </div>
</div> </div>
{/* User Salary Marker */}
<div <div
style={{ style={{
position: 'absolute', position: 'absolute',
@ -153,9 +152,6 @@ function SalaryGauge({ userSalary, percentileRow, prefix = 'regional' }) {
); );
} }
// ----------------------
// 3) Economic Bar
// ----------------------
function EconomicProjectionsBar({ data }) { function EconomicProjectionsBar({ data }) {
if (!data) return null; if (!data) return null;
const { const {
@ -239,16 +235,42 @@ function getYearsInCareer(startDateString) {
return Math.floor(diffYears).toString(); return Math.floor(diffYears).toString();
} }
// ---------------------- /**
// 4) MilestoneTracker * parseAiJson
// ---------------------- * If ChatGPT returns a fenced code block like:
* ```json
* [ { ... }, ... ]
* ```
* we extract that JSON. Otherwise, we parse the raw string.
*/
function parseAiJson(rawText) {
const fencedRegex = /```json\s*([\s\S]*?)\s*```/i;
const match = rawText.match(fencedRegex);
if (match) {
const jsonStr = match[1].trim();
const arr = JSON.parse(jsonStr);
// Add an "id" for each milestone
arr.forEach((m) => {
m.id = crypto.randomUUID();
});
return arr;
} else {
// fallback if no fences
const arr = JSON.parse(rawText);
arr.forEach((m) => {
m.id = crypto.randomUUID();
});
return arr;
}
}
export default function MilestoneTracker({ selectedCareer: initialCareer }) { export default function MilestoneTracker({ selectedCareer: initialCareer }) {
const location = useLocation(); const location = useLocation();
const apiURL = process.env.REACT_APP_API_URL; const apiURL = process.env.REACT_APP_API_URL;
// Basic states
const [userProfile, setUserProfile] = useState(null); const [userProfile, setUserProfile] = useState(null);
const [financialProfile, setFinancialProfile] = useState(null); const [financialProfile, setFinancialProfile] = useState(null);
const [masterCareerRatings, setMasterCareerRatings] = useState([]); const [masterCareerRatings, setMasterCareerRatings] = useState([]);
const [existingCareerProfiles, setExistingCareerProfiles] = useState([]); const [existingCareerProfiles, setExistingCareerProfiles] = useState([]);
const [selectedCareer, setSelectedCareer] = useState(initialCareer || null); const [selectedCareer, setSelectedCareer] = useState(initialCareer || null);
@ -260,15 +282,22 @@ export default function MilestoneTracker({ selectedCareer: initialCareer }) {
const [salaryData, setSalaryData] = useState(null); const [salaryData, setSalaryData] = useState(null);
const [economicProjections, setEconomicProjections] = useState(null); const [economicProjections, setEconomicProjections] = useState(null);
// Milestones & Projection
const [scenarioMilestones, setScenarioMilestones] = useState([]); const [scenarioMilestones, setScenarioMilestones] = useState([]);
const [projectionData, setProjectionData] = useState([]); const [projectionData, setProjectionData] = useState([]);
const [loanPayoffMonth, setLoanPayoffMonth] = useState(null); const [loanPayoffMonth, setLoanPayoffMonth] = useState(null);
// Config
const [simulationYearsInput, setSimulationYearsInput] = useState('20'); const [simulationYearsInput, setSimulationYearsInput] = useState('20');
const simulationYears = parseInt(simulationYearsInput, 10) || 20; const simulationYears = parseInt(simulationYearsInput, 10) || 20;
const [showEditModal, setShowEditModal] = useState(false); const [showEditModal, setShowEditModal] = useState(false);
// AI
const [aiLoading, setAiLoading] = useState(false);
const [recommendations, setRecommendations] = useState([]); // parsed array
const [selectedIds, setSelectedIds] = useState([]); // which rec IDs are checked
const { const {
projectionData: initProjData = [], projectionData: initProjData = [],
loanPayoffMonth: initLoanMonth = null loanPayoffMonth: initLoanMonth = null
@ -276,22 +305,22 @@ export default function MilestoneTracker({ selectedCareer: initialCareer }) {
// 1) Fetch user + financial // 1) Fetch user + financial
useEffect(() => { useEffect(() => {
const fetchUser = async () => { async function fetchUser() {
try { try {
const r = await authFetch('/api/user-profile'); const r = await authFetch('/api/user-profile');
if (r.ok) setUserProfile(await r.json()); if (r.ok) setUserProfile(await r.json());
} catch (err) { } catch (err) {
console.error('Error user-profile =>', err); console.error('Error user-profile =>', err);
} }
}; }
const fetchFin = async () => { async function fetchFin() {
try { try {
const r = await authFetch(`${apiURL}/premium/financial-profile`); const r = await authFetch(`${apiURL}/premium/financial-profile`);
if (r.ok) setFinancialProfile(await r.json()); if (r.ok) setFinancialProfile(await r.json());
} catch (err) { } catch (err) {
console.error('Error financial =>', err); console.error('Error financial =>', err);
} }
}; }
fetchUser(); fetchUser();
fetchFin(); fetchFin();
}, [apiURL]); }, [apiURL]);
@ -313,7 +342,7 @@ export default function MilestoneTracker({ selectedCareer: initialCareer }) {
// 3) fetch users career-profiles // 3) fetch users career-profiles
useEffect(() => { useEffect(() => {
const fetchProfiles = async () => { async function fetchProfiles() {
const r = await authFetch(`${apiURL}/premium/career-profile/all`); const r = await authFetch(`${apiURL}/premium/career-profile/all`);
if (!r || !r.ok) return; if (!r || !r.ok) return;
const d = await r.json(); const d = await r.json();
@ -343,7 +372,7 @@ export default function MilestoneTracker({ selectedCareer: initialCareer }) {
} }
} }
} }
}; }
fetchProfiles(); fetchProfiles();
}, [apiURL, location.state]); }, [apiURL, location.state]);
@ -357,19 +386,19 @@ export default function MilestoneTracker({ selectedCareer: initialCareer }) {
} }
localStorage.setItem('lastSelectedCareerProfileId', careerProfileId); localStorage.setItem('lastSelectedCareerProfileId', careerProfileId);
const fetchScenario = async () => { async function fetchScenario() {
const s = await authFetch(`${apiURL}/premium/career-profile/${careerProfileId}`); const s = await authFetch(`${apiURL}/premium/career-profile/${careerProfileId}`);
if (s.ok) setScenarioRow(await s.json()); if (s.ok) setScenarioRow(await s.json());
}; }
const fetchCollege = async () => { async function fetchCollege() {
const c = await authFetch(`${apiURL}/premium/college-profile?careerProfileId=${careerProfileId}`); const c = await authFetch(`${apiURL}/premium/college-profile?careerProfileId=${careerProfileId}`);
if (c.ok) setCollegeProfile(await c.json()); if (c.ok) setCollegeProfile(await c.json());
}; }
fetchScenario(); fetchScenario();
fetchCollege(); fetchCollege();
}, [careerProfileId, apiURL]); }, [careerProfileId, apiURL]);
// 5) from scenarioRow.career_name => find the full SOC => strip // 5) from scenarioRow => find the full SOC => strip
useEffect(() => { useEffect(() => {
if (!scenarioRow?.career_name || !masterCareerRatings.length) { if (!scenarioRow?.career_name || !masterCareerRatings.length) {
setStrippedSocCode(null); setStrippedSocCode(null);
@ -395,12 +424,8 @@ export default function MilestoneTracker({ selectedCareer: initialCareer }) {
} }
(async () => { (async () => {
try { try {
const qs = new URLSearchParams({ const qs = new URLSearchParams({ socCode: strippedSocCode, area: userArea }).toString();
socCode: strippedSocCode,
area: userArea
}).toString();
const url = `${apiURL}/salary?${qs}`; const url = `${apiURL}/salary?${qs}`;
console.log('[Salary fetch =>]', url);
const r = await fetch(url); const r = await fetch(url);
if (!r.ok) { if (!r.ok) {
console.error('[Salary fetch non-200 =>]', r.status); console.error('[Salary fetch non-200 =>]', r.status);
@ -408,7 +433,6 @@ export default function MilestoneTracker({ selectedCareer: initialCareer }) {
return; return;
} }
const dd = await r.json(); const dd = await r.json();
console.log('[Salary success =>]', dd);
setSalaryData(dd); setSalaryData(dd);
} catch (err) { } catch (err) {
console.error('[Salary fetch error]', err); console.error('[Salary fetch error]', err);
@ -426,7 +450,6 @@ export default function MilestoneTracker({ selectedCareer: initialCareer }) {
(async () => { (async () => {
const qs = new URLSearchParams({ state: userState }).toString(); const qs = new URLSearchParams({ state: userState }).toString();
const econUrl = `${apiURL}/projections/${strippedSocCode}?${qs}`; const econUrl = `${apiURL}/projections/${strippedSocCode}?${qs}`;
console.log('[Econ fetch =>]', econUrl);
try { try {
const r = await authFetch(econUrl); const r = await authFetch(econUrl);
if (!r.ok) { if (!r.ok) {
@ -435,7 +458,6 @@ export default function MilestoneTracker({ selectedCareer: initialCareer }) {
return; return;
} }
const econData = await r.json(); const econData = await r.json();
console.log('[Econ success =>]', econData);
setEconomicProjections(econData); setEconomicProjections(econData);
} catch (err) { } catch (err) {
console.error('[Econ fetch error]', err); console.error('[Econ fetch error]', err);
@ -445,7 +467,7 @@ export default function MilestoneTracker({ selectedCareer: initialCareer }) {
}, [strippedSocCode, userState, apiURL]); }, [strippedSocCode, userState, apiURL]);
// 8) Build financial projection // 8) Build financial projection
const buildProjection = async () => { async function buildProjection() {
try { try {
const milUrl = `${apiURL}/premium/milestones?careerProfileId=${careerProfileId}`; const milUrl = `${apiURL}/premium/milestones?careerProfileId=${careerProfileId}`;
const mr = await authFetch(milUrl); const mr = await authFetch(milUrl);
@ -457,16 +479,7 @@ export default function MilestoneTracker({ selectedCareer: initialCareer }) {
const allMilestones = md.milestones || []; const allMilestones = md.milestones || [];
setScenarioMilestones(allMilestones); setScenarioMilestones(allMilestones);
function parseScenarioOverride(overrideVal, fallbackVal) { // fetch impacts
// If the DB field is NULL => means user never entered anything
if (overrideVal === null) {
return fallbackVal;
}
// Otherwise user typed a number, even if it's "0"
return parseFloatOrZero(overrideVal, fallbackVal);
}
const imPromises = allMilestones.map((m) => const imPromises = allMilestones.map((m) =>
authFetch(`${apiURL}/premium/milestone-impacts?milestone_id=${m.id}`) authFetch(`${apiURL}/premium/milestone-impacts?milestone_id=${m.id}`)
.then((r) => (r.ok ? r.json() : null)) .then((r) => (r.ok ? r.json() : null))
@ -495,38 +508,44 @@ export default function MilestoneTracker({ selectedCareer: initialCareer }) {
extraCashRetirementPct: parseFloatOrZero(f.extra_cash_retirement_pct, 50) extraCashRetirementPct: parseFloatOrZero(f.extra_cash_retirement_pct, 50)
}; };
function parseScenarioOverride(overrideVal, fallbackVal) {
if (overrideVal === null) {
return fallbackVal;
}
return parseFloatOrZero(overrideVal, fallbackVal);
}
const s = scenarioRow; const s = scenarioRow;
const scenarioOverrides = { const scenarioOverrides = {
monthlyExpenses: parseScenarioOverride( monthlyExpenses: parseScenarioOverride(
s.planned_monthly_expenses, s.planned_monthly_expenses,
financialBase.monthlyExpenses financialBase.monthlyExpenses
), ),
monthlyDebtPayments: parseScenarioOverride( monthlyDebtPayments: parseScenarioOverride(
s.planned_monthly_debt_payments, s.planned_monthly_debt_payments,
financialBase.monthlyDebtPayments financialBase.monthlyDebtPayments
), ),
monthlyRetirementContribution: parseScenarioOverride( monthlyRetirementContribution: parseScenarioOverride(
s.planned_monthly_retirement_contribution, s.planned_monthly_retirement_contribution,
financialBase.retirementContribution financialBase.retirementContribution
), ),
monthlyEmergencyContribution: parseScenarioOverride( monthlyEmergencyContribution: parseScenarioOverride(
s.planned_monthly_emergency_contribution, s.planned_monthly_emergency_contribution,
financialBase.emergencyContribution financialBase.emergencyContribution
), ),
surplusEmergencyAllocation: parseScenarioOverride( surplusEmergencyAllocation: parseScenarioOverride(
s.planned_surplus_emergency_pct, s.planned_surplus_emergency_pct,
financialBase.extraCashEmergencyPct financialBase.extraCashEmergencyPct
), ),
surplusRetirementAllocation: parseScenarioOverride( surplusRetirementAllocation: parseScenarioOverride(
s.planned_surplus_retirement_pct, s.planned_surplus_retirement_pct,
financialBase.extraCashRetirementPct financialBase.extraCashRetirementPct
), ),
additionalIncome: parseScenarioOverride( additionalIncome: parseScenarioOverride(
s.planned_additional_income, s.planned_additional_income,
financialBase.additionalIncome financialBase.additionalIncome
), )
}; };
const c = collegeProfile; const c = collegeProfile;
const collegeData = { const collegeData = {
@ -587,7 +606,6 @@ export default function MilestoneTracker({ selectedCareer: initialCareer }) {
const { projectionData: pData, loanPaidOffMonth } = const { projectionData: pData, loanPaidOffMonth } =
simulateFinancialProjection(mergedProfile); simulateFinancialProjection(mergedProfile);
// Build "cumulativeNetSavings" ourselves, plus each row has .retirementSavings and .emergencySavings
let cumu = mergedProfile.emergencySavings || 0; let cumu = mergedProfile.emergencySavings || 0;
const finalData = pData.map((mo) => { const finalData = pData.map((mo) => {
cumu += mo.netSavings || 0; cumu += mo.netSavings || 0;
@ -599,27 +617,19 @@ export default function MilestoneTracker({ selectedCareer: initialCareer }) {
} catch (err) { } catch (err) {
console.error('Error in scenario simulation =>', err); console.error('Error in scenario simulation =>', err);
} }
}; }
useEffect(() => { useEffect(() => {
if (!financialProfile || !scenarioRow || !collegeProfile) return; if (!financialProfile || !scenarioRow || !collegeProfile) return;
buildProjection(); buildProjection();
}, [financialProfile, scenarioRow, collegeProfile, careerProfileId, apiURL, simulationYears]); }, [financialProfile, scenarioRow, collegeProfile, careerProfileId, apiURL, simulationYears]);
// Handlers // Build chart datasets / annotations
const handleSimulationYearsChange = (e) => setSimulationYearsInput(e.target.value);
const handleSimulationYearsBlur = () => {
if (!simulationYearsInput.trim()) setSimulationYearsInput('20');
};
// -- Annotations --
// 1) Milestone lines
const milestoneAnnotationLines = {}; const milestoneAnnotationLines = {};
scenarioMilestones.forEach((m) => { scenarioMilestones.forEach((m) => {
if (!m.date) return; if (!m.date) return;
const d = new Date(m.date); const d = new Date(m.date);
if (isNaN(d)) return; if (isNaN(d)) return;
const yyyy = d.getUTCFullYear(); const yyyy = d.getUTCFullYear();
const mm = String(d.getUTCMonth() + 1).padStart(2, '0'); const mm = String(d.getUTCMonth() + 1).padStart(2, '0');
const short = `${yyyy}-${mm}`; const short = `${yyyy}-${mm}`;
@ -640,10 +650,7 @@ export default function MilestoneTracker({ selectedCareer: initialCareer }) {
}; };
}); });
// 2) Check if there's ever a positive loan balance
const hasStudentLoan = projectionData.some((p) => p.loanBalance > 0); const hasStudentLoan = projectionData.some((p) => p.loanBalance > 0);
// 3) Conditionally add the loan payoff annotation
const annotationConfig = {}; const annotationConfig = {};
if (loanPayoffMonth && hasStudentLoan) { if (loanPayoffMonth && hasStudentLoan) {
annotationConfig.loanPaidOffLine = { annotationConfig.loanPaidOffLine = {
@ -665,19 +672,16 @@ export default function MilestoneTracker({ selectedCareer: initialCareer }) {
} }
}; };
} }
const allAnnotations = { ...milestoneAnnotationLines, ...annotationConfig }; const allAnnotations = { ...milestoneAnnotationLines, ...annotationConfig };
// Build the chart datasets:
const emergencyData = { const emergencyData = {
label: 'Emergency Savings', label: 'Emergency Savings',
data: projectionData.map((p) => p.emergencySavings), data: projectionData.map((p) => p.emergencySavings),
borderColor: 'rgba(255, 159, 64, 1)', // orange borderColor: 'rgba(255, 159, 64, 1)',
backgroundColor: 'rgba(255, 159, 64, 0.2)', backgroundColor: 'rgba(255, 159, 64, 0.2)',
tension: 0.4, tension: 0.4,
fill: true fill: true
}; };
const retirementData = { const retirementData = {
label: 'Retirement Savings', label: 'Retirement Savings',
data: projectionData.map((p) => p.retirementSavings), data: projectionData.map((p) => p.retirementSavings),
@ -686,8 +690,6 @@ export default function MilestoneTracker({ selectedCareer: initialCareer }) {
tension: 0.4, tension: 0.4,
fill: true fill: true
}; };
// The total leftover each month (sum of any net gains so far).
const totalSavingsData = { const totalSavingsData = {
label: 'Total Savings', label: 'Total Savings',
data: projectionData.map((p) => p.totalSavings), data: projectionData.map((p) => p.totalSavings),
@ -696,8 +698,6 @@ export default function MilestoneTracker({ selectedCareer: initialCareer }) {
tension: 0.4, tension: 0.4,
fill: true fill: true
}; };
// We'll insert the Loan Balance dataset only if they actually have a loan
const loanBalanceData = { const loanBalanceData = {
label: 'Loan Balance', label: 'Loan Balance',
data: projectionData.map((p) => p.loanBalance), data: projectionData.map((p) => p.loanBalance),
@ -711,20 +711,96 @@ export default function MilestoneTracker({ selectedCareer: initialCareer }) {
} }
}; };
// The final dataset array:
// 1) Emergency
// 2) Retirement
// 3) Loan (conditional)
// 4) Total
const chartDatasets = [emergencyData, retirementData]; const chartDatasets = [emergencyData, retirementData];
if (hasStudentLoan) { if (hasStudentLoan) chartDatasets.push(loanBalanceData);
// Insert loan after the first two lines, or wherever you prefer
chartDatasets.push(loanBalanceData);
}
chartDatasets.push(totalSavingsData); chartDatasets.push(totalSavingsData);
const yearsInCareer = getYearsInCareer(scenarioRow?.start_date); const yearsInCareer = getYearsInCareer(scenarioRow?.start_date);
// -- AI Handler --
async function handleAiClick() {
setAiLoading(true);
setRecommendations([]);
setSelectedIds([]);
try {
const payload = { userProfile, scenarioRow, financialProfile, collegeProfile };
const res = await authFetch('/api/premium/ai/next-steps', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
if (!res.ok) throw new Error('AI request failed');
const data = await res.json();
const rawText = data.recommendations || '';
// Parse JSON
const arr = parseAiJson(rawText);
setRecommendations(arr);
} catch (err) {
console.error('Error fetching AI recommendations:', err);
} finally {
setAiLoading(false);
}
}
// Check/uncheck a recommendation
function handleToggle(recId) {
setSelectedIds((prev) => {
if (prev.includes(recId)) {
return prev.filter((x) => x !== recId);
} else {
return [...prev, recId];
}
});
}
async function handleCreateSelectedMilestones() {
if (!careerProfileId) return;
const confirm = window.confirm('Convert selected AI suggestions into milestones?');
if (!confirm) return;
// filter out those that are checked
const selectedRecs = recommendations.filter((r) => selectedIds.includes(r.id));
if (!selectedRecs.length) return;
const newMils = selectedRecs.map((rec) => ({
title: rec.title,
description: rec.description || '',
date: new Date().toISOString().slice(0, 10), // for demonstration
career_profile_id: careerProfileId
}));
try {
const r = await authFetch('/api/premium/milestone', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ milestones: newMils })
});
if (!r.ok) throw new Error('Failed to create new milestones');
// re-run the projection to reflect newly inserted milestones
await buildProjection();
alert('Milestones successfully created! Check your timeline or projection.');
// optionally clear them
setSelectedIds([]);
} catch (err) {
console.error('Error saving new milestones:', err);
alert('Error saving AI milestones.');
}
}
function handleSimulationYearsChange(e) {
setSimulationYearsInput(e.target.value);
}
function handleSimulationYearsBlur() {
if (!simulationYearsInput.trim()) setSimulationYearsInput('20');
}
return ( return (
<div className="milestone-tracker max-w-screen-lg mx-auto px-4 py-6 space-y-6"> <div className="milestone-tracker max-w-screen-lg mx-auto px-4 py-6 space-y-6">
<h2 className="text-2xl font-bold mb-4">Where Am I Now?</h2> <h2 className="text-2xl font-bold mb-4">Where Am I Now?</h2>
@ -760,7 +836,7 @@ export default function MilestoneTracker({ selectedCareer: initialCareer }) {
{salaryData?.regional && ( {salaryData?.regional && (
<div className="bg-white p-4 rounded shadow w-full md:w-1/2"> <div className="bg-white p-4 rounded shadow w-full md:w-1/2">
<h4 className="font-medium mb-2"> <h4 className="font-medium mb-2">
Regional Data ({userArea || 'U.S.'}) Regional Salary Data ({userArea || 'U.S.'})
</h4> </h4>
<p> <p>
10th percentile:{' '} 10th percentile:{' '}
@ -768,14 +844,12 @@ export default function MilestoneTracker({ selectedCareer: initialCareer }) {
? `$${salaryData.regional.regional_PCT10.toLocaleString()}` ? `$${salaryData.regional.regional_PCT10.toLocaleString()}`
: 'N/A'} : 'N/A'}
</p> </p>
<p> <p>
Median:{' '} Median:{' '}
{salaryData.regional.regional_MEDIAN {salaryData.regional.regional_MEDIAN
? `$${salaryData.regional.regional_MEDIAN.toLocaleString()}` ? `$${salaryData.regional.regional_MEDIAN.toLocaleString()}`
: 'N/A'} : 'N/A'}
</p> </p>
<p> <p>
90th percentile:{' '} 90th percentile:{' '}
{salaryData.regional.regional_PCT90 {salaryData.regional.regional_PCT90
@ -793,21 +867,19 @@ export default function MilestoneTracker({ selectedCareer: initialCareer }) {
{salaryData?.national && ( {salaryData?.national && (
<div className="bg-white p-4 rounded shadow w-full md:w-1/2"> <div className="bg-white p-4 rounded shadow w-full md:w-1/2">
<h4 className="font-medium mb-2">National Data</h4> <h4 className="font-medium mb-2">National Salary Data</h4>
<p> <p>
10th percentile:{' '} 10th percentile:{' '}
{salaryData.national.national_PCT10 {salaryData.national.national_PCT10
? `$${salaryData.national.national_PCT10.toLocaleString()}` ? `$${salaryData.national.national_PCT10.toLocaleString()}`
: 'N/A'} : 'N/A'}
</p> </p>
<p> <p>
Median:{' '} Median:{' '}
{salaryData.national.national_MEDIAN {salaryData.national.national_MEDIAN
? `$${salaryData.national.national_MEDIAN.toLocaleString()}` ? `$${salaryData.national.national_MEDIAN.toLocaleString()}`
: 'N/A'} : 'N/A'}
</p> </p>
<p> <p>
90th percentile:{' '} 90th percentile:{' '}
{salaryData.national.national_PCT90 {salaryData.national.national_PCT90
@ -839,15 +911,12 @@ export default function MilestoneTracker({ selectedCareer: initialCareer }) {
</div> </div>
)} )}
{/* 4) Milestone Timeline */} {/* 4) Career Goals */}
<div className="bg-white p-4 rounded shadow"> <div className="bg-white p-4 rounded shadow">
<h3 className="text-lg font-semibold mb-2">Your Milestones</h3> <h3 className="text-lg font-semibold mb-2">Your Career Goals</h3>
<MilestoneTimeline <p className="text-gray-700">
careerProfileId={careerProfileId} {scenarioRow?.career_goals || 'No career goals entered yet.'}
authFetch={authFetch} </p>
activeView="Career"
onMilestoneUpdated={() => {}}
/>
</div> </div>
{/* 5) Financial Projection */} {/* 5) Financial Projection */}
@ -865,9 +934,7 @@ export default function MilestoneTracker({ selectedCareer: initialCareer }) {
plugins: { plugins: {
legend: { position: 'bottom' }, legend: { position: 'bottom' },
tooltip: { mode: 'index', intersect: false }, tooltip: { mode: 'index', intersect: false },
annotation: { annotation: { annotations: allAnnotations }
annotations: allAnnotations
}
}, },
scales: { scales: {
y: { y: {
@ -920,6 +987,41 @@ export default function MilestoneTracker({ selectedCareer: initialCareer }) {
authFetch={authFetch} authFetch={authFetch}
/> />
{/* 7) AI Next Steps */}
<div className="bg-white p-4 rounded shadow mt-4">
<Button onClick={handleAiClick}>
What Should I Do Next?
</Button>
{aiLoading && <p>Generating your next steps</p>}
{/* If we have structured recs, show checkboxes */}
{recommendations.length > 0 && (
<div className="mt-3">
<h3 className="font-semibold">Select the Advice You Want to Keep</h3>
<ul className="mt-2 space-y-2">
{recommendations.map((m) => (
<li key={m.id} className="flex items-start gap-2">
<input
type="checkbox"
checked={selectedIds.includes(m.id)}
onChange={() => handleToggle(m.id)}
/>
<div className="flex flex-col text-left">
<strong>{m.title}</strong>
<span>{m.date}</span>
<p className="text-sm">{m.description}</p>
</div>
</li>
))}
</ul>
{selectedIds.length > 0 && (
<Button className="mt-3" onClick={handleCreateSelectedMilestones}>
Create Milestones from Selected
</Button>
)}
</div>
)}
</div>
</div> </div>
); );
} }

View File

@ -152,7 +152,16 @@ const CareerOnboarding = ({ nextStep, prevStep, data, setData }) => {
<option value="prospective_student">Planning to Enroll (Prospective)</option> <option value="prospective_student">Planning to Enroll (Prospective)</option>
</select> </select>
</div> </div>
<div className="space-y-2">
<label className="block font-medium">Career Goals</label>
<textarea
name="career_goals"
value={data.career_goals || ''}
onChange={handleChange}
className="w-full border rounded p-2"
placeholder="Tell us about your goals, aspirations, or next steps..."
/>
</div>
<div className="flex justify-between pt-4"> <div className="flex justify-between pt-4">
<button <button
onClick={prevStep} onClick={prevStep}

View File

@ -1,29 +1,115 @@
// ReviewPage.js
import React from 'react'; import React from 'react';
// Hypothetical Button component from your UI library
import { Button } from '../ui/button.js'; // Adjust path if needed
function ReviewPage({ careerData, financialData, collegeData, onSubmit, onBack }) { /**
console.log("REVIEW PAGE PROPS:", { * Helper to format numeric fields for display.
careerData, * If val is null/undefined or 0 and you want to hide it,
financialData, * you can adapt the logic below.
collegeData, */
}); function formatNum(val) {
return ( // If val is null or undefined, show 'N/A'
<div> if (val == null) return 'N/A';
<h2>Review Your Info</h2> // If you'd like to hide zero, you could do:
// if (val === 0) return 'N/A';
return val;
}
<h3>Career Info</h3> function ReviewPage({
<pre>{JSON.stringify(careerData, null, 2)}</pre> careerData = {},
financialData = {},
collegeData = {},
onSubmit,
onBack
}) {
// Decide if user is in or planning to be in college
const inOrPlanningCollege = (
careerData.college_enrollment_status === 'currently_enrolled'
|| careerData.college_enrollment_status === 'prospective_student'
);
<h3>Financial Info</h3>
<pre>{JSON.stringify(financialData, null, 2)}</pre>
<h3>College Info</h3>
<pre>{JSON.stringify(collegeData, null, 2)}</pre>
<button onClick={onBack}> Back</button> return (
<button onClick={onSubmit} style={{ marginLeft: '1rem' }}> <div className="max-w-xl mx-auto p-6 space-y-6">
Submit All <h2 className="text-2xl font-semibold">Review Your Info</h2>
</button>
{/* --- CAREER SECTION --- */}
<div className="p-4 border rounded-md space-y-2">
<h3 className="text-xl font-semibold">Career Info</h3>
<div><strong>Career Name:</strong> {careerData.career_name || 'N/A'}</div>
<div><strong>Currently Working:</strong> {careerData.currently_working || 'N/A'}</div>
<div><strong>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>
{/* --- FINANCIAL SECTION --- */}
<div className="p-4 border rounded-md space-y-2">
<h3 className="text-xl font-semibold">Financial Info</h3>
{/* Current/Additional Income */}
{careerData.currently_working === 'yes' ? (
<div className="space-y-1">
<div><strong>Current Annual Salary:</strong> {formatNum(financialData.current_salary)}</div>
<div><strong>Additional Annual Income:</strong> {formatNum(financialData.additional_income)}</div>
</div>
) : (
<div><strong>Currently Not Working</strong></div>
)}
{/* Monthly Expenses / Debt */}
<div className="mt-2 space-y-1">
<div><strong>Monthly Expenses:</strong> {formatNum(financialData.monthly_expenses)}</div>
<div><strong>Monthly Debt Payments:</strong> {formatNum(financialData.monthly_debt_payments)}</div>
</div>
{/* Retirement */}
<div className="mt-2 space-y-1">
<div><strong>Retirement Savings:</strong> {formatNum(financialData.retirement_savings)}</div>
<div><strong>Monthly Retirement Contribution:</strong> {formatNum(financialData.retirement_contribution)}</div>
</div>
{/* Emergency Fund */}
<div className="mt-2 space-y-1">
<div><strong>Emergency Fund Savings:</strong> {formatNum(financialData.emergency_fund)}</div>
<div><strong>Monthly Emergency Contribution:</strong> {formatNum(financialData.emergency_contribution)}</div>
</div>
{/* Extra Monthly Cash Allocation */}
<div className="mt-2 space-y-1">
<div><strong>Extra Monthly Cash to Emergency (%):</strong> {formatNum(financialData.extra_cash_emergency_pct)}</div>
<div><strong>Extra Monthly Cash to Retirement (%):</strong> {formatNum(financialData.extra_cash_retirement_pct)}</div>
</div>
</div>
{/* --- COLLEGE SECTION --- */}
{inOrPlanningCollege && (
<div className="p-4 border rounded-md space-y-2">
<h3 className="text-xl font-semibold">College Info</h3>
<div><strong>College Name:</strong> {collegeData.college_name || 'N/A'}</div>
<div><strong>Major:</strong> {collegeData.major || 'N/A'}</div>
{/* If you have these fields, show them if they're meaningful */}
{collegeData.tuition != null && (
<div><strong>Tuition (calculated):</strong> {formatNum(collegeData.tuition)}</div>
)}
{collegeData.program_length != null && (
<div><strong>Program Length (years):</strong> {formatNum(collegeData.program_length)}</div>
)}
</div>
)}
{/* --- ACTION BUTTONS --- */}
<div className="flex justify-between pt-4">
<Button variant="secondary" onClick={onBack}>
Back
</Button>
<Button variant="primary" onClick={onSubmit}>
Submit All
</Button>
</div>
</div> </div>
); );
} }