From 961d0e5fd4a4719525157f811341d4920085ed4a Mon Sep 17 00:00:00 2001 From: Josh Date: Tue, 27 May 2025 18:47:44 +0000 Subject: [PATCH] Added next-steps provided by AI to convert to milestones, fixed simulation. Where Am I Now should be finished. --- backend/server3.js | 326 ++++++++++++++-- src/components/MilestoneTimeline.js | 16 - src/components/MilestoneTracker.js | 352 +++++++++++------- .../PremiumOnboarding/CareerOnboarding.js | 11 +- .../PremiumOnboarding/ReviewPage.js | 126 ++++++- 5 files changed, 629 insertions(+), 202 deletions(-) diff --git a/backend/server3.js b/backend/server3.js index da56036..afe1172 100644 --- a/backend/server3.js +++ b/backend/server3.js @@ -146,6 +146,10 @@ app.post('/api/premium/career-profile', authenticatePremiumUser, async (req, res projected_end_date, college_enrollment_status, currently_working, + // The new field: + career_goals, + + // planned fields planned_monthly_expenses, planned_monthly_debt_payments, planned_monthly_retirement_contribution, @@ -162,6 +166,7 @@ app.post('/api/premium/career-profile', authenticatePremiumUser, async (req, res try { const newId = uuidv4(); + // 1) Insert includes career_goals const sql = ` INSERT INTO career_profiles ( id, @@ -173,6 +178,7 @@ app.post('/api/premium/career-profile', authenticatePremiumUser, async (req, res projected_end_date, college_enrollment_status, currently_working, + career_goals, -- ADD THIS planned_monthly_expenses, planned_monthly_debt_payments, planned_monthly_retirement_contribution, @@ -181,13 +187,14 @@ app.post('/api/premium/career-profile', authenticatePremiumUser, async (req, res planned_surplus_retirement_pct, planned_additional_income ) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE status = VALUES(status), start_date = VALUES(start_date), projected_end_date = VALUES(projected_end_date), college_enrollment_status = VALUES(college_enrollment_status), currently_working = VALUES(currently_working), + career_goals = VALUES(career_goals), -- ADD THIS planned_monthly_expenses = VALUES(planned_monthly_expenses), planned_monthly_debt_payments = VALUES(planned_monthly_debt_payments), 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, college_enrollment_status || null, currently_working || null, + career_goals || null, // pass career_goals here planned_monthly_expenses ?? null, planned_monthly_debt_payments ?? 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 - const [rows] = await pool.query(` - SELECT id - FROM career_profiles - WHERE id = ? - `, [newId]); + const [rows] = await pool.query( + `SELECT id + FROM career_profiles + WHERE id = ?`, + [newId] + ); return res.status(200).json({ 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 app.delete('/api/premium/career-profile/:careerProfileId', authenticatePremiumUser, async (req, res) => { 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 (1–3 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 ------------------------------------------------------------------ */ @@ -316,18 +589,16 @@ app.post('/api/premium/milestone', authenticatePremiumUser, async (req, res) => const createdMilestones = []; for (const m of body.milestones) { const { - milestone_type, title, description, date, career_profile_id, progress, status, - new_salary, is_universal } = m; - if (!milestone_type || !title || !date || !career_profile_id) { + if (!title || !date || !career_profile_id) { return res.status(400).json({ error: 'One or more milestones missing required fields', details: m @@ -340,27 +611,23 @@ app.post('/api/premium/milestone', authenticatePremiumUser, async (req, res) => id, user_id, career_profile_id, - milestone_type, title, description, date, progress, status, - new_salary, is_universal ) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) `, [ id, req.id, career_profile_id, - milestone_type, title, description || '', date, progress || 0, status || 'planned', - new_salary || null, is_universal ? 1 : 0 ]); @@ -368,13 +635,11 @@ app.post('/api/premium/milestone', authenticatePremiumUser, async (req, res) => id, user_id: req.id, career_profile_id, - milestone_type, title, description: description || '', date, progress: progress || 0, status: status || 'planned', - new_salary: new_salary || null, is_universal: is_universal ? 1 : 0, tasks: [] }); @@ -384,21 +649,19 @@ app.post('/api/premium/milestone', authenticatePremiumUser, async (req, res) => // single milestone const { - milestone_type, title, description, date, career_profile_id, progress, status, - new_salary, is_universal } = body; - if (!milestone_type || !title || !date || !career_profile_id) { + if ( !title || !date || !career_profile_id) { return res.status(400).json({ 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, user_id, career_profile_id, - milestone_type, title, description, date, progress, status, - new_salary, is_universal ) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) `, [ id, req.id, career_profile_id, - milestone_type, title, description || '', date, progress || 0, status || 'planned', - new_salary || null, is_universal ? 1 : 0 ]); @@ -436,13 +695,11 @@ app.post('/api/premium/milestone', authenticatePremiumUser, async (req, res) => id, user_id: req.id, career_profile_id, - milestone_type, title, description: description || '', date, progress: progress || 0, status: status || 'planned', - new_salary: new_salary || null, is_universal: is_universal ? 1 : 0, tasks: [] }; @@ -458,14 +715,12 @@ app.put('/api/premium/milestones/:milestoneId', authenticatePremiumUser, async ( try { const { milestoneId } = req.params; const { - milestone_type, title, description, date, career_profile_id, progress, status, - new_salary, is_universal } = req.body; @@ -481,40 +736,34 @@ app.put('/api/premium/milestones/:milestoneId', authenticatePremiumUser, async ( } const row = existing[0]; - const finalMilestoneType = milestone_type || row.milestone_type; const finalTitle = title || row.title; const finalDesc = description || row.description; const finalDate = date || row.date; const finalCareerProfileId = career_profile_id || row.career_profile_id; const finalProgress = progress != null ? progress : row.progress; 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; await pool.query(` UPDATE milestones SET - milestone_type = ?, title = ?, description = ?, date = ?, career_profile_id = ?, progress = ?, status = ?, - new_salary = ?, is_universal = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ? AND user_id = ? `, [ - finalMilestoneType, finalTitle, finalDesc, finalDate, finalCareerProfileId, finalProgress, finalStatus, - finalSalary, finalIsUniversal, milestoneId, req.id @@ -683,28 +932,24 @@ app.post('/api/premium/milestone/copy', authenticatePremiumUser, async (req, res id, user_id, career_profile_id, - milestone_type, title, description, date, progress, status, - new_salary, is_universal, origin_milestone_id ) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `, [ newMilestoneId, req.id, scenarioId, - original.milestone_type, original.title, original.description, original.date, original.progress, original.status, - original.new_salary, 1, originId ]); @@ -1190,6 +1435,7 @@ Milestone Requirements: 1. Provide exactly 3 SHORT-TERM milestones (within next 1-2 years). - Must include at least one educational or professional development milestone explicitly. - Do NOT exclusively focus on financial aspects. + 2. Provide exactly 2 LONG-TERM milestones (3+ years out). - Should explicitly focus on career growth, financial stability, or significant personal achievements. diff --git a/src/components/MilestoneTimeline.js b/src/components/MilestoneTimeline.js index 0b5a00e..1f61d64 100644 --- a/src/components/MilestoneTimeline.js +++ b/src/components/MilestoneTimeline.js @@ -93,17 +93,6 @@ export default function MilestoneTimeline({ console.warn('No milestones in response:', data); 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) { console.error('Failed to fetch milestones:', err); } @@ -150,7 +139,6 @@ export default function MilestoneTimeline({ description: m.description || '', date: m.date || '', progress: m.progress || 0, - newSalary: m.new_salary || '', impacts: fetchedImpacts.map((imp) => ({ id: imp.id, impact_type: imp.impact_type || 'ONE_TIME', @@ -188,10 +176,6 @@ export default function MilestoneTimeline({ career_profile_id: careerProfileId, progress: newMilestone.progress, status: newMilestone.progress >= 100 ? 'completed' : 'planned', - new_salary: - activeView === 'Financial' && newMilestone.newSalary - ? parseFloat(newMilestone.newSalary) - : null, is_universal: newMilestone.isUniversal || 0 }; diff --git a/src/components/MilestoneTracker.js b/src/components/MilestoneTracker.js index 7de3787..d6e5289 100644 --- a/src/components/MilestoneTracker.js +++ b/src/components/MilestoneTracker.js @@ -21,16 +21,15 @@ import { getFullStateName } from '../utils/stateUtils.js'; import { Button } from './ui/button.js'; import CareerSelectDropdown from './CareerSelectDropdown.js'; -import CareerSearch from './CareerSearch.js'; import MilestoneTimeline from './MilestoneTimeline.js'; import ScenarioEditModal from './ScenarioEditModal.js'; -// If you need AI suggestions in the future: -// import AISuggestedMilestones from './AISuggestedMilestones.js'; - import './MilestoneTracker.css'; import './MilestoneTimeline.css'; +// -------------- +// Register ChartJS Plugins +// -------------- ChartJS.register( LineElement, BarElement, @@ -43,17 +42,14 @@ ChartJS.register( annotationPlugin ); -// ---------------------- -// 1) Remove decimals from SOC code -// ---------------------- +// -------------- +// Helper Functions +// -------------- function stripSocCode(fullSoc) { if (!fullSoc) return ''; return fullSoc.split('.')[0]; } -// ---------------------- -// 2) Salary Gauge -// ---------------------- function getRelativePosition(userSal, p10, p90) { if (!p10 || !p90) return 0; if (userSal < p10) return 0; @@ -61,6 +57,7 @@ function getRelativePosition(userSal, p10, p90) { return (userSal - p10) / (p90 - p10); } +// A simple gauge for the user’s salary vs. percentiles function SalaryGauge({ userSalary, percentileRow, prefix = 'regional' }) { if (!percentileRow) return null; @@ -91,6 +88,7 @@ function SalaryGauge({ userSalary, percentileRow, prefix = 'regional' }) { marginBottom: '8px' }} > + {/* Median Marker */}
+ {/* User Salary Marker */}
{ + 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 }) { const location = useLocation(); const apiURL = process.env.REACT_APP_API_URL; + // Basic states const [userProfile, setUserProfile] = useState(null); const [financialProfile, setFinancialProfile] = useState(null); - const [masterCareerRatings, setMasterCareerRatings] = useState([]); const [existingCareerProfiles, setExistingCareerProfiles] = useState([]); const [selectedCareer, setSelectedCareer] = useState(initialCareer || null); @@ -260,15 +282,22 @@ export default function MilestoneTracker({ selectedCareer: initialCareer }) { const [salaryData, setSalaryData] = useState(null); const [economicProjections, setEconomicProjections] = useState(null); + // Milestones & Projection const [scenarioMilestones, setScenarioMilestones] = useState([]); const [projectionData, setProjectionData] = useState([]); const [loanPayoffMonth, setLoanPayoffMonth] = useState(null); + // Config const [simulationYearsInput, setSimulationYearsInput] = useState('20'); const simulationYears = parseInt(simulationYearsInput, 10) || 20; 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 { projectionData: initProjData = [], loanPayoffMonth: initLoanMonth = null @@ -276,22 +305,22 @@ export default function MilestoneTracker({ selectedCareer: initialCareer }) { // 1) Fetch user + financial useEffect(() => { - const fetchUser = async () => { + async function fetchUser() { try { const r = await authFetch('/api/user-profile'); if (r.ok) setUserProfile(await r.json()); } catch (err) { console.error('Error user-profile =>', err); } - }; - const fetchFin = async () => { + } + async function fetchFin() { try { const r = await authFetch(`${apiURL}/premium/financial-profile`); if (r.ok) setFinancialProfile(await r.json()); } catch (err) { console.error('Error financial =>', err); } - }; + } fetchUser(); fetchFin(); }, [apiURL]); @@ -313,7 +342,7 @@ export default function MilestoneTracker({ selectedCareer: initialCareer }) { // 3) fetch user’s career-profiles useEffect(() => { - const fetchProfiles = async () => { + async function fetchProfiles() { const r = await authFetch(`${apiURL}/premium/career-profile/all`); if (!r || !r.ok) return; const d = await r.json(); @@ -343,7 +372,7 @@ export default function MilestoneTracker({ selectedCareer: initialCareer }) { } } } - }; + } fetchProfiles(); }, [apiURL, location.state]); @@ -357,19 +386,19 @@ export default function MilestoneTracker({ selectedCareer: initialCareer }) { } localStorage.setItem('lastSelectedCareerProfileId', careerProfileId); - const fetchScenario = async () => { + async function fetchScenario() { const s = await authFetch(`${apiURL}/premium/career-profile/${careerProfileId}`); if (s.ok) setScenarioRow(await s.json()); - }; - const fetchCollege = async () => { + } + async function fetchCollege() { const c = await authFetch(`${apiURL}/premium/college-profile?careerProfileId=${careerProfileId}`); if (c.ok) setCollegeProfile(await c.json()); - }; + } fetchScenario(); fetchCollege(); }, [careerProfileId, apiURL]); - // 5) from scenarioRow.career_name => find the full SOC => strip + // 5) from scenarioRow => find the full SOC => strip useEffect(() => { if (!scenarioRow?.career_name || !masterCareerRatings.length) { setStrippedSocCode(null); @@ -395,12 +424,8 @@ export default function MilestoneTracker({ selectedCareer: initialCareer }) { } (async () => { try { - const qs = new URLSearchParams({ - socCode: strippedSocCode, - area: userArea - }).toString(); + const qs = new URLSearchParams({ socCode: strippedSocCode, area: userArea }).toString(); const url = `${apiURL}/salary?${qs}`; - console.log('[Salary fetch =>]', url); const r = await fetch(url); if (!r.ok) { console.error('[Salary fetch non-200 =>]', r.status); @@ -408,7 +433,6 @@ export default function MilestoneTracker({ selectedCareer: initialCareer }) { return; } const dd = await r.json(); - console.log('[Salary success =>]', dd); setSalaryData(dd); } catch (err) { console.error('[Salary fetch error]', err); @@ -426,7 +450,6 @@ export default function MilestoneTracker({ selectedCareer: initialCareer }) { (async () => { const qs = new URLSearchParams({ state: userState }).toString(); const econUrl = `${apiURL}/projections/${strippedSocCode}?${qs}`; - console.log('[Econ fetch =>]', econUrl); try { const r = await authFetch(econUrl); if (!r.ok) { @@ -435,7 +458,6 @@ export default function MilestoneTracker({ selectedCareer: initialCareer }) { return; } const econData = await r.json(); - console.log('[Econ success =>]', econData); setEconomicProjections(econData); } catch (err) { console.error('[Econ fetch error]', err); @@ -445,7 +467,7 @@ export default function MilestoneTracker({ selectedCareer: initialCareer }) { }, [strippedSocCode, userState, apiURL]); // 8) Build financial projection - const buildProjection = async () => { + async function buildProjection() { try { const milUrl = `${apiURL}/premium/milestones?careerProfileId=${careerProfileId}`; const mr = await authFetch(milUrl); @@ -457,16 +479,7 @@ export default function MilestoneTracker({ selectedCareer: initialCareer }) { const allMilestones = md.milestones || []; setScenarioMilestones(allMilestones); - function parseScenarioOverride(overrideVal, fallbackVal) { - // 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); -} - - + // fetch impacts const imPromises = allMilestones.map((m) => authFetch(`${apiURL}/premium/milestone-impacts?milestone_id=${m.id}`) .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) }; + function parseScenarioOverride(overrideVal, fallbackVal) { + if (overrideVal === null) { + return fallbackVal; + } + return parseFloatOrZero(overrideVal, fallbackVal); + } + const s = scenarioRow; const scenarioOverrides = { - monthlyExpenses: parseScenarioOverride( - s.planned_monthly_expenses, - financialBase.monthlyExpenses - ), - monthlyDebtPayments: parseScenarioOverride( - s.planned_monthly_debt_payments, - financialBase.monthlyDebtPayments - ), - monthlyRetirementContribution: parseScenarioOverride( - s.planned_monthly_retirement_contribution, - financialBase.retirementContribution - ), - monthlyEmergencyContribution: parseScenarioOverride( - s.planned_monthly_emergency_contribution, - financialBase.emergencyContribution - ), - surplusEmergencyAllocation: parseScenarioOverride( - s.planned_surplus_emergency_pct, - financialBase.extraCashEmergencyPct - ), - surplusRetirementAllocation: parseScenarioOverride( - s.planned_surplus_retirement_pct, - financialBase.extraCashRetirementPct - ), - additionalIncome: parseScenarioOverride( - s.planned_additional_income, - financialBase.additionalIncome - ), - }; - + monthlyExpenses: parseScenarioOverride( + s.planned_monthly_expenses, + financialBase.monthlyExpenses + ), + monthlyDebtPayments: parseScenarioOverride( + s.planned_monthly_debt_payments, + financialBase.monthlyDebtPayments + ), + monthlyRetirementContribution: parseScenarioOverride( + s.planned_monthly_retirement_contribution, + financialBase.retirementContribution + ), + monthlyEmergencyContribution: parseScenarioOverride( + s.planned_monthly_emergency_contribution, + financialBase.emergencyContribution + ), + surplusEmergencyAllocation: parseScenarioOverride( + s.planned_surplus_emergency_pct, + financialBase.extraCashEmergencyPct + ), + surplusRetirementAllocation: parseScenarioOverride( + s.planned_surplus_retirement_pct, + financialBase.extraCashRetirementPct + ), + additionalIncome: parseScenarioOverride( + s.planned_additional_income, + financialBase.additionalIncome + ) + }; const c = collegeProfile; const collegeData = { @@ -587,7 +606,6 @@ export default function MilestoneTracker({ selectedCareer: initialCareer }) { const { projectionData: pData, loanPaidOffMonth } = simulateFinancialProjection(mergedProfile); - // Build "cumulativeNetSavings" ourselves, plus each row has .retirementSavings and .emergencySavings let cumu = mergedProfile.emergencySavings || 0; const finalData = pData.map((mo) => { cumu += mo.netSavings || 0; @@ -599,27 +617,19 @@ export default function MilestoneTracker({ selectedCareer: initialCareer }) { } catch (err) { console.error('Error in scenario simulation =>', err); } - }; + } useEffect(() => { if (!financialProfile || !scenarioRow || !collegeProfile) return; buildProjection(); }, [financialProfile, scenarioRow, collegeProfile, careerProfileId, apiURL, simulationYears]); - // Handlers - const handleSimulationYearsChange = (e) => setSimulationYearsInput(e.target.value); - const handleSimulationYearsBlur = () => { - if (!simulationYearsInput.trim()) setSimulationYearsInput('20'); - }; - - // -- Annotations -- - // 1) Milestone lines + // Build chart datasets / annotations const milestoneAnnotationLines = {}; scenarioMilestones.forEach((m) => { if (!m.date) return; const d = new Date(m.date); if (isNaN(d)) return; - const yyyy = d.getUTCFullYear(); const mm = String(d.getUTCMonth() + 1).padStart(2, '0'); 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); - - // 3) Conditionally add the loan payoff annotation const annotationConfig = {}; if (loanPayoffMonth && hasStudentLoan) { annotationConfig.loanPaidOffLine = { @@ -665,19 +672,16 @@ export default function MilestoneTracker({ selectedCareer: initialCareer }) { } }; } - const allAnnotations = { ...milestoneAnnotationLines, ...annotationConfig }; - // Build the chart datasets: const emergencyData = { label: 'Emergency Savings', 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)', tension: 0.4, fill: true }; - const retirementData = { label: 'Retirement Savings', data: projectionData.map((p) => p.retirementSavings), @@ -686,8 +690,6 @@ export default function MilestoneTracker({ selectedCareer: initialCareer }) { tension: 0.4, fill: true }; - - // The total leftover each month (sum of any net gains so far). const totalSavingsData = { label: 'Total Savings', data: projectionData.map((p) => p.totalSavings), @@ -696,8 +698,6 @@ export default function MilestoneTracker({ selectedCareer: initialCareer }) { tension: 0.4, fill: true }; - - // We'll insert the Loan Balance dataset only if they actually have a loan const loanBalanceData = { label: 'Loan Balance', 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]; - if (hasStudentLoan) { - // Insert loan after the first two lines, or wherever you prefer - chartDatasets.push(loanBalanceData); - } + if (hasStudentLoan) chartDatasets.push(loanBalanceData); chartDatasets.push(totalSavingsData); 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 (

Where Am I Now?

@@ -760,7 +836,7 @@ export default function MilestoneTracker({ selectedCareer: initialCareer }) { {salaryData?.regional && (

- Regional Data ({userArea || 'U.S.'}) + Regional Salary Data ({userArea || 'U.S.'})

10th percentile:{' '} @@ -768,14 +844,12 @@ export default function MilestoneTracker({ selectedCareer: initialCareer }) { ? `$${salaryData.regional.regional_PCT10.toLocaleString()}` : 'N/A'}

-

Median:{' '} {salaryData.regional.regional_MEDIAN ? `$${salaryData.regional.regional_MEDIAN.toLocaleString()}` : 'N/A'}

-

90th percentile:{' '} {salaryData.regional.regional_PCT90 @@ -793,21 +867,19 @@ export default function MilestoneTracker({ selectedCareer: initialCareer }) { {salaryData?.national && (

-

National Data

+

National Salary Data

10th percentile:{' '} {salaryData.national.national_PCT10 ? `$${salaryData.national.national_PCT10.toLocaleString()}` : 'N/A'}

-

Median:{' '} {salaryData.national.national_MEDIAN ? `$${salaryData.national.national_MEDIAN.toLocaleString()}` : 'N/A'}

-

90th percentile:{' '} {salaryData.national.national_PCT90 @@ -839,15 +911,12 @@ export default function MilestoneTracker({ selectedCareer: initialCareer }) {

)} - {/* 4) Milestone Timeline */} + {/* 4) Career Goals */}
-

Your Milestones

- {}} - /> +

Your Career Goals

+

+ {scenarioRow?.career_goals || 'No career goals entered yet.'} +

{/* 5) Financial Projection */} @@ -865,9 +934,7 @@ export default function MilestoneTracker({ selectedCareer: initialCareer }) { plugins: { legend: { position: 'bottom' }, tooltip: { mode: 'index', intersect: false }, - annotation: { - annotations: allAnnotations - } + annotation: { annotations: allAnnotations } }, scales: { y: { @@ -920,6 +987,41 @@ export default function MilestoneTracker({ selectedCareer: initialCareer }) { authFetch={authFetch} /> + {/* 7) AI Next Steps */} +
+ + {aiLoading &&

Generating your next steps…

} + + {/* If we have structured recs, show checkboxes */} + {recommendations.length > 0 && ( +
+

Select the Advice You Want to Keep

+
    + {recommendations.map((m) => ( +
  • + handleToggle(m.id)} + /> +
    + {m.title} + {m.date} +

    {m.description}

    +
    +
  • + ))} +
+ {selectedIds.length > 0 && ( + + )} +
+ )} +
); } diff --git a/src/components/PremiumOnboarding/CareerOnboarding.js b/src/components/PremiumOnboarding/CareerOnboarding.js index 6e53d12..aa4a0e2 100644 --- a/src/components/PremiumOnboarding/CareerOnboarding.js +++ b/src/components/PremiumOnboarding/CareerOnboarding.js @@ -152,7 +152,16 @@ const CareerOnboarding = ({ nextStep, prevStep, data, setData }) => {
- +
+ +