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 */}
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 && (
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 }) {
+ {scenarioRow?.career_goals || 'No career goals entered yet.'} +
Generating your next steps…
} + + {/* If we have structured recs, show checkboxes */} + {recommendations.length > 0 && ( +{m.description}
+