diff --git a/backend/server.js b/backend/server.js index d9ab4cc..23795be 100755 --- a/backend/server.js +++ b/backend/server.js @@ -180,7 +180,7 @@ app.post('/api/register', async (req, res) => { // 2) Insert into user_auth, referencing user_profile.id const authQuery = ` - INSERT INTO user_auth (user_id, username, hashed_password) + INSERT INTO user_auth (id, username, hashed_password) VALUES (?, ?, ?) `; pool.query( @@ -229,7 +229,7 @@ app.post('/api/signin', (req, res) => { const query = ` SELECT - user_auth.user_id, + user_auth.id, user_auth.hashed_password, user_profile.firstname, user_profile.lastname, @@ -243,7 +243,7 @@ app.post('/api/signin', (req, res) => { user_profile.career_priorities, user_profile.career_list FROM user_auth - LEFT JOIN user_profile ON user_auth.user_id = user_profile.id + LEFT JOIN user_profile ON user_auth.id = user_profile.id WHERE user_auth.username = ? `; pool.query(query, [username], async (err, results) => { @@ -265,8 +265,8 @@ app.post('/api/signin', (req, res) => { return res.status(401).json({ error: 'Invalid username or password' }); } - // The user_profile id is stored in user_auth.user_id - const token = jwt.sign({ id: row.user_id }, SECRET_KEY, { + // The user_profile id is stored in user_auth.id + const token = jwt.sign({ id: row.id }, SECRET_KEY, { expiresIn: '2h', }); @@ -274,7 +274,7 @@ app.post('/api/signin', (req, res) => { res.status(200).json({ message: 'Login successful', token, - userId: row.user_id, // The user_profile.id + id: row.id, // The user_profile.id user: { firstname: row.firstname, lastname: row.lastname, diff --git a/backend/server3.js b/backend/server3.js index b664559..41c8fe9 100644 --- a/backend/server3.js +++ b/backend/server3.js @@ -1,22 +1,22 @@ -// server3.js -import express from 'express'; +// +// server3.js - MySQL Version +// +import express from 'express'; import cors from 'cors'; import helmet from 'helmet'; import dotenv from 'dotenv'; -import { open } from 'sqlite'; -import sqlite3 from 'sqlite3'; -import jwt from 'jsonwebtoken'; -import { v4 as uuidv4 } from 'uuid'; import path from 'path'; import fs from 'fs/promises'; import multer from 'multer'; import mammoth from 'mammoth'; import { fileURLToPath } from 'url'; +import jwt from 'jsonwebtoken'; +import { v4 as uuidv4 } from 'uuid'; import pkg from 'pdfjs-dist'; - +import mysql from 'mysql2/promise'; // <-- MySQL instead of SQLite import OpenAI from 'openai'; -// --- Basic file init --- +// Basic file init const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); @@ -25,41 +25,39 @@ const env = process.env.NODE_ENV?.trim() || 'development'; const envPath = path.resolve(rootPath, `.env.${env}`); dotenv.config({ path: envPath }); // Load .env file - const app = express(); const PORT = process.env.PREMIUM_PORT || 5002; - const { getDocument } = pkg; -let db; -const initDB = async () => { - try { - db = await open({ - filename: '/home/jcoakley/aptiva-dev1-app/user_profile.db', - driver: sqlite3.Database - }); - console.log('Connected to user_profile.db for Premium Services.'); - } catch (error) { - console.error('Error connecting to premium database:', error); - } -}; -initDB(); +// 1) Create a MySQL pool using your environment variables +const pool = mysql.createPool({ + host: process.env.DB_HOST || 'localhost', + user: process.env.DB_USER || 'root', + password: process.env.DB_PASSWORD || '', + database: process.env.DB_NAME || 'user_profile_db', + waitForConnections: true, + connectionLimit: 10, + queueLimit: 0 +}); +// 2) Basic middlewares app.use(helmet()); app.use(express.json({ limit: '5mb' })); - const allowedOrigins = ['https://dev1.aptivaai.com']; app.use(cors({ origin: allowedOrigins, credentials: true })); +// 3) Authentication middleware const authenticatePremiumUser = (req, res, next) => { const token = req.headers.authorization?.split(' ')[1]; - if (!token) return res.status(401).json({ error: 'Premium authorization required' }); + if (!token) { + return res.status(401).json({ error: 'Premium authorization required' }); + } try { const SECRET_KEY = process.env.SECRET_KEY || 'supersecurekey'; - const { userId } = jwt.verify(token, SECRET_KEY); - req.userId = userId; + const { id } = jwt.verify(token, SECRET_KEY); + req.id = id; // store user ID in request next(); } catch (error) { return res.status(403).json({ error: 'Invalid or expired token' }); @@ -73,14 +71,15 @@ const authenticatePremiumUser = (req, res, next) => { // GET the latest selected career profile app.get('/api/premium/career-profile/latest', authenticatePremiumUser, async (req, res) => { try { - const row = await db.get(` + const [rows] = await pool.query(` SELECT * - FROM career_paths + FROM career_profiles WHERE user_id = ? ORDER BY start_date DESC LIMIT 1 - `, [req.userId]); - res.json(row || {}); + `, [req.id]); + + res.json(rows[0] || {}); } catch (error) { console.error('Error fetching latest career profile:', error); res.status(500).json({ error: 'Failed to fetch latest career profile' }); @@ -90,13 +89,14 @@ app.get('/api/premium/career-profile/latest', authenticatePremiumUser, async (re // GET all career profiles for the user app.get('/api/premium/career-profile/all', authenticatePremiumUser, async (req, res) => { try { - const rows = await db.all(` + const [rows] = await pool.query(` SELECT * - FROM career_paths + FROM career_profiles WHERE user_id = ? ORDER BY start_date ASC - `, [req.userId]); - res.json({ careerPaths: rows }); + `, [req.id]); + + res.json({ careerProfiles: rows }); } catch (error) { console.error('Error fetching career profiles:', error); res.status(500).json({ error: 'Failed to fetch career profiles' }); @@ -104,21 +104,20 @@ app.get('/api/premium/career-profile/all', authenticatePremiumUser, async (req, }); // GET a single career profile (scenario) by ID -app.get('/api/premium/career-profile/:careerPathId', authenticatePremiumUser, async (req, res) => { - const { careerPathId } = req.params; +app.get('/api/premium/career-profile/:careerProfileId', authenticatePremiumUser, async (req, res) => { + const { careerProfileId } = req.params; try { - const row = await db.get(` + const [rows] = await pool.query(` SELECT * - FROM career_paths + FROM career_profiles WHERE id = ? AND user_id = ? - `, [careerPathId, req.userId]); + `, [careerProfileId, req.id]); - if (!row) { - return res.status(404).json({ error: 'Career path (scenario) not found or not yours.' }); + if (!rows[0]) { + return res.status(404).json({ error: 'Career profile not found or not yours.' }); } - - res.json(row); + res.json(rows[0]); } catch (error) { console.error('Error fetching single career profile:', error); res.status(500).json({ error: 'Failed to fetch career profile by ID.' }); @@ -135,7 +134,6 @@ app.post('/api/premium/career-profile', authenticatePremiumUser, async (req, res projected_end_date, college_enrollment_status, currently_working, - planned_monthly_expenses, planned_monthly_debt_payments, planned_monthly_retirement_contribution, @@ -150,11 +148,10 @@ app.post('/api/premium/career-profile', authenticatePremiumUser, async (req, res } try { - const newCareerPathId = uuidv4(); - const now = new Date().toISOString(); + const newId = uuidv4(); - await db.run(` - INSERT INTO career_paths ( + const sql = ` + INSERT INTO career_profiles ( id, user_id, scenario_title, @@ -170,69 +167,54 @@ app.post('/api/premium/career-profile', authenticatePremiumUser, async (req, res planned_monthly_emergency_contribution, planned_surplus_emergency_pct, planned_surplus_retirement_pct, - planned_additional_income, - created_at, - updated_at + planned_additional_income ) - VALUES ( - ?, ?, ?, ?, ?, - ?, ?, ?, ?, - ?, ?, ?, ?, - ?, ?, ?, - ?, ? - ) - ON CONFLICT(user_id, career_name) - DO UPDATE SET - status = excluded.status, - start_date = excluded.start_date, - projected_end_date = excluded.projected_end_date, - college_enrollment_status = excluded.college_enrollment_status, - currently_working = excluded.currently_working, + 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), + 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), + planned_monthly_emergency_contribution = VALUES(planned_monthly_emergency_contribution), + planned_surplus_emergency_pct = VALUES(planned_surplus_emergency_pct), + planned_surplus_retirement_pct = VALUES(planned_surplus_retirement_pct), + planned_additional_income = VALUES(planned_additional_income), + updated_at = CURRENT_TIMESTAMP + `; - planned_monthly_expenses = excluded.planned_monthly_expenses, - planned_monthly_debt_payments = excluded.planned_monthly_debt_payments, - planned_monthly_retirement_contribution = excluded.planned_monthly_retirement_contribution, - planned_monthly_emergency_contribution = excluded.planned_monthly_emergency_contribution, - planned_surplus_emergency_pct = excluded.planned_surplus_emergency_pct, - planned_surplus_retirement_pct = excluded.planned_surplus_retirement_pct, - planned_additional_income = excluded.planned_additional_income, - - updated_at = ? - `, [ - newCareerPathId, - req.userId, + await pool.query(sql, [ + newId, + req.id, scenario_title || null, career_name, status || 'planned', - start_date || now, + start_date || null, projected_end_date || null, college_enrollment_status || null, currently_working || null, - planned_monthly_expenses ?? null, planned_monthly_debt_payments ?? null, planned_monthly_retirement_contribution ?? null, planned_monthly_emergency_contribution ?? null, planned_surplus_emergency_pct ?? null, planned_surplus_retirement_pct ?? null, - planned_additional_income ?? null, - - now, - now, - - now + planned_additional_income ?? null ]); - const result = await db.get(` + // re-fetch to confirm ID + const [rows] = await pool.query(` SELECT id - FROM career_paths - WHERE user_id = ? - AND career_name = ? - `, [req.userId, career_name]); + FROM career_profiles + WHERE id = ? + `, [newId]); - res.status(200).json({ + return res.status(200).json({ message: 'Career profile upserted.', - career_path_id: result?.id + career_profile_id: rows[0]?.id || newId }); } catch (error) { console.error('Error upserting career profile:', error); @@ -240,77 +222,76 @@ app.post('/api/premium/career-profile', authenticatePremiumUser, async (req, res } }); -// DELETE a career path (scenario) by ID (and associated data) -app.delete('/api/premium/career-profile/:careerPathId', authenticatePremiumUser, async (req, res) => { - const { careerPathId } = req.params; - +// DELETE a career profile (scenario) by ID +app.delete('/api/premium/career-profile/:careerProfileId', authenticatePremiumUser, async (req, res) => { + const { careerProfileId } = req.params; try { - // 1) Confirm that this career_path belongs to the user - const existing = await db.get(` + // confirm ownership + const [rows] = await pool.query(` SELECT id - FROM career_paths + FROM career_profiles WHERE id = ? AND user_id = ? - `, [careerPathId, req.userId]); + `, [careerProfileId, req.id]); - if (!existing) { - return res.status(404).json({ error: 'Career path not found or not yours.' }); + if (!rows[0]) { + return res.status(404).json({ error: 'Career profile not found or not yours.' }); } - // 2) Delete the college_profile for this scenario - await db.run(` + // delete college_profiles + await pool.query(` DELETE FROM college_profiles WHERE user_id = ? - AND career_path_id = ? - `, [req.userId, careerPathId]); + AND career_profile_id = ? + `, [req.id, careerProfileId]); - // 3) Delete scenario’s milestones (and tasks/impacts) - const scenarioMilestones = await db.all(` + // delete scenario’s milestones + tasks + impacts + const [mils] = await pool.query(` SELECT id FROM milestones WHERE user_id = ? - AND career_path_id = ? - `, [req.userId, careerPathId]); - const milestoneIds = scenarioMilestones.map((m) => m.id); + AND career_profile_id = ? + `, [req.id, careerProfileId]); + const milestoneIds = mils.map(m => m.id); if (milestoneIds.length > 0) { const placeholders = milestoneIds.map(() => '?').join(','); - // Delete tasks - await db.run(` + // tasks + await pool.query(` DELETE FROM tasks WHERE milestone_id IN (${placeholders}) `, milestoneIds); - // Delete impacts - await db.run(` + // impacts + await pool.query(` DELETE FROM milestone_impacts WHERE milestone_id IN (${placeholders}) `, milestoneIds); - // Finally delete the milestones themselves - await db.run(` + // milestones + await pool.query(` DELETE FROM milestones WHERE id IN (${placeholders}) `, milestoneIds); } - // 4) Delete the career_path row - await db.run(` - DELETE FROM career_paths - WHERE user_id = ? - AND id = ? - `, [req.userId, careerPathId]); + // delete the career_profiles row + await pool.query(` + DELETE FROM career_profiles + WHERE id = ? + AND user_id = ? + `, [careerProfileId, req.id]); - res.json({ message: 'Career path and related data successfully deleted.' }); + res.json({ message: 'Career profile and related data successfully deleted.' }); } catch (error) { - console.error('Error deleting career path:', error); - res.status(500).json({ error: 'Failed to delete career path.' }); + console.error('Error deleting career profile:', error); + res.status(500).json({ error: 'Failed to delete career profile.' }); } }); /* ------------------------------------------------------------------ - Milestone ENDPOINTS + MILESTONE ENDPOINTS ------------------------------------------------------------------ */ // CREATE one or more milestones @@ -318,8 +299,8 @@ app.post('/api/premium/milestone', authenticatePremiumUser, async (req, res) => try { const body = req.body; - // CASE 1: If client sent { milestones: [ ... ] }, do a bulk insert if (Array.isArray(body.milestones)) { + // Bulk insert const createdMilestones = []; for (const m of body.milestones) { const { @@ -327,14 +308,14 @@ app.post('/api/premium/milestone', authenticatePremiumUser, async (req, res) => title, description, date, - career_path_id, + career_profile_id, progress, status, new_salary, is_universal } = m; - if (!milestone_type || !title || !date || !career_path_id) { + if (!milestone_type || !title || !date || !career_profile_id) { return res.status(400).json({ error: 'One or more milestones missing required fields', details: m @@ -342,13 +323,11 @@ app.post('/api/premium/milestone', authenticatePremiumUser, async (req, res) => } const id = uuidv4(); - const now = new Date().toISOString(); - - await db.run(` + await pool.query(` INSERT INTO milestones ( id, user_id, - career_path_id, + career_profile_id, milestone_type, title, description, @@ -356,14 +335,13 @@ app.post('/api/premium/milestone', authenticatePremiumUser, async (req, res) => progress, status, new_salary, - is_universal, - created_at, - updated_at - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + is_universal + ) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `, [ id, - req.userId, - career_path_id, + req.id, + career_profile_id, milestone_type, title, description || '', @@ -371,15 +349,13 @@ app.post('/api/premium/milestone', authenticatePremiumUser, async (req, res) => progress || 0, status || 'planned', new_salary || null, - is_universal ? 1 : 0, - now, - now + is_universal ? 1 : 0 ]); createdMilestones.push({ id, - user_id: req.userId, - career_path_id, + user_id: req.id, + career_profile_id, milestone_type, title, description: description || '', @@ -394,34 +370,32 @@ app.post('/api/premium/milestone', authenticatePremiumUser, async (req, res) => return res.status(201).json(createdMilestones); } - // CASE 2: Single milestone creation + // single milestone const { milestone_type, title, description, date, - career_path_id, + career_profile_id, progress, status, new_salary, is_universal } = body; - if (!milestone_type || !title || !date || !career_path_id) { + if (!milestone_type || !title || !date || !career_profile_id) { return res.status(400).json({ error: 'Missing required fields', - details: { milestone_type, title, date, career_path_id } + details: { milestone_type, title, date, career_profile_id } }); } const id = uuidv4(); - const now = new Date().toISOString(); - - await db.run(` + await pool.query(` INSERT INTO milestones ( id, user_id, - career_path_id, + career_profile_id, milestone_type, title, description, @@ -429,14 +403,13 @@ app.post('/api/premium/milestone', authenticatePremiumUser, async (req, res) => progress, status, new_salary, - is_universal, - created_at, - updated_at - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + is_universal + ) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `, [ id, - req.userId, - career_path_id, + req.id, + career_profile_id, milestone_type, title, description || '', @@ -444,15 +417,13 @@ app.post('/api/premium/milestone', authenticatePremiumUser, async (req, res) => progress || 0, status || 'planned', new_salary || null, - is_universal ? 1 : 0, - now, - now + is_universal ? 1 : 0 ]); const newMilestone = { id, - user_id: req.userId, - career_path_id, + user_id: req.id, + career_profile_id, milestone_type, title, description: description || '', @@ -463,8 +434,7 @@ app.post('/api/premium/milestone', authenticatePremiumUser, async (req, res) => is_universal: is_universal ? 1 : 0, tasks: [] }; - - res.status(201).json(newMilestone); + return res.status(201).json(newMilestone); } catch (err) { console.error('Error creating milestone(s):', err); res.status(500).json({ error: 'Failed to create milestone(s).' }); @@ -480,51 +450,48 @@ app.put('/api/premium/milestones/:milestoneId', authenticatePremiumUser, async ( title, description, date, - career_path_id, + career_profile_id, progress, status, new_salary, is_universal } = req.body; - // Check if milestone exists and belongs to user - const existing = await db.get(` + const [existing] = await pool.query(` SELECT * FROM milestones WHERE id = ? AND user_id = ? - `, [milestoneId, req.userId]); + `, [milestoneId, req.id]); - if (!existing) { + if (!existing[0]) { return res.status(404).json({ error: 'Milestone not found or not yours.' }); } + const row = existing[0]; - const now = new Date().toISOString(); + 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; - const finalMilestoneType = milestone_type || existing.milestone_type; - const finalTitle = title || existing.title; - const finalDesc = description || existing.description; - const finalDate = date || existing.date; - const finalCareerPath = career_path_id || existing.career_path_id; - const finalProgress = progress != null ? progress : existing.progress; - const finalStatus = status || existing.status; - const finalSalary = new_salary != null ? new_salary : existing.new_salary; - const finalIsUniversal = - is_universal != null ? (is_universal ? 1 : 0) : existing.is_universal; - - await db.run(` + await pool.query(` UPDATE milestones SET milestone_type = ?, title = ?, description = ?, date = ?, - career_path_id = ?, + career_profile_id = ?, progress = ?, status = ?, new_salary = ?, is_universal = ?, - updated_at = ? + updated_at = CURRENT_TIMESTAMP WHERE id = ? AND user_id = ? `, [ @@ -532,62 +499,62 @@ app.put('/api/premium/milestones/:milestoneId', authenticatePremiumUser, async ( finalTitle, finalDesc, finalDate, - finalCareerPath, + finalCareerProfileId, finalProgress, finalStatus, finalSalary, finalIsUniversal, - now, milestoneId, - req.userId + req.id ]); // Return the updated milestone with tasks - const updatedMilestoneRow = await db.get(` + const [[updatedMilestoneRow]] = await pool.query(` SELECT * FROM milestones WHERE id = ? `, [milestoneId]); - const tasks = await db.all(` + + const [tasks] = await pool.query(` SELECT * FROM tasks WHERE milestone_id = ? `, [milestoneId]); - const updatedMilestone = { + res.json({ ...updatedMilestoneRow, tasks: tasks || [] - }; - - res.json(updatedMilestone); + }); } catch (err) { console.error('Error updating milestone:', err); res.status(500).json({ error: 'Failed to update milestone.' }); } }); -// GET all milestones for a given careerPathId +// GET all milestones for a given careerProfileId app.get('/api/premium/milestones', authenticatePremiumUser, async (req, res) => { - const { careerPathId } = req.query; + const { careerProfileId } = req.query; try { - // universal milestones - if (careerPathId === 'universal') { - const universalRows = await db.all(` + if (careerProfileId === 'universal') { + // universal + const [universalRows] = await pool.query(` SELECT * FROM milestones WHERE user_id = ? AND is_universal = 1 - `, [req.userId]); + `, [req.id]); const milestoneIds = universalRows.map(m => m.id); let tasksByMilestone = {}; if (milestoneIds.length > 0) { - const tasks = await db.all(` + const placeholders = milestoneIds.map(() => '?').join(','); + const [taskRows] = await pool.query(` SELECT * FROM tasks - WHERE milestone_id IN (${milestoneIds.map(() => '?').join(',')}) + WHERE milestone_id IN (${placeholders}) `, milestoneIds); - tasksByMilestone = tasks.reduce((acc, t) => { + + tasksByMilestone = taskRows.reduce((acc, t) => { if (!acc[t.milestone_id]) acc[t.milestone_id] = []; acc[t.milestone_id].push(t); return acc; @@ -601,24 +568,25 @@ app.get('/api/premium/milestones', authenticatePremiumUser, async (req, res) => return res.json({ milestones: uniMils }); } - // else fetch by careerPathId - const milestones = await db.all(` + // else by careerProfileId + const [milestones] = await pool.query(` SELECT * FROM milestones WHERE user_id = ? - AND career_path_id = ? - `, [req.userId, careerPathId]); + AND career_profile_id = ? + `, [req.id, careerProfileId]); const milestoneIds = milestones.map(m => m.id); let tasksByMilestone = {}; if (milestoneIds.length > 0) { - const tasks = await db.all(` + const placeholders = milestoneIds.map(() => '?').join(','); + const [taskRows] = await pool.query(` SELECT * FROM tasks - WHERE milestone_id IN (${milestoneIds.map(() => '?').join(',')}) + WHERE milestone_id IN (${placeholders}) `, milestoneIds); - tasksByMilestone = tasks.reduce((acc, t) => { + tasksByMilestone = taskRows.reduce((acc, t) => { if (!acc[t.milestone_id]) acc[t.milestone_id] = []; acc[t.milestone_id].push(t); return acc; @@ -629,7 +597,6 @@ app.get('/api/premium/milestones', authenticatePremiumUser, async (req, res) => ...m, tasks: tasksByMilestone[m.id] || [] })); - res.json({ milestones: milestonesWithTasks }); } catch (err) { console.error('Error fetching milestones with tasks:', err); @@ -645,62 +612,65 @@ app.post('/api/premium/milestone/copy', authenticatePremiumUser, async (req, res return res.status(400).json({ error: 'Missing milestoneId or scenarioIds.' }); } - const original = await db.get(` + // check ownership + const [origRows] = await pool.query(` SELECT * FROM milestones WHERE id = ? AND user_id = ? - `, [milestoneId, req.userId]); - if (!original) { + `, [milestoneId, req.id]); + + if (!origRows[0]) { return res.status(404).json({ error: 'Milestone not found or not owned by user.' }); } + const original = origRows[0]; + // if not universal => set universal = 1 if (original.is_universal !== 1) { - await db.run(` + await pool.query(` UPDATE milestones SET is_universal = 1 WHERE id = ? AND user_id = ? - `, [ milestoneId, req.userId ]); + `, [milestoneId, req.id]); original.is_universal = 1; } let originId = original.origin_milestone_id || original.id; if (!original.origin_milestone_id) { - await db.run(` + await pool.query(` UPDATE milestones SET origin_milestone_id = ? WHERE id = ? AND user_id = ? - `, [ originId, milestoneId, req.userId ]); + `, [originId, milestoneId, req.id]); } - const tasks = await db.all(` + // fetch tasks + const [taskRows] = await pool.query(` SELECT * FROM tasks WHERE milestone_id = ? `, [milestoneId]); - const impacts = await db.all(` + // fetch impacts + const [impactRows] = await pool.query(` SELECT * FROM milestone_impacts WHERE milestone_id = ? `, [milestoneId]); - const now = new Date().toISOString(); const copiesCreated = []; for (let scenarioId of scenarioIds) { - if (scenarioId === original.career_path_id) continue; // skip if same scenario + if (scenarioId === original.career_profile_id) continue; const newMilestoneId = uuidv4(); - const isUniversal = 1; - - await db.run(` + await pool.query(` INSERT INTO milestones ( id, user_id, - career_path_id, + career_profile_id, milestone_type, title, description, @@ -709,14 +679,12 @@ app.post('/api/premium/milestone/copy', authenticatePremiumUser, async (req, res status, new_salary, is_universal, - origin_milestone_id, - created_at, - updated_at + origin_milestone_id ) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `, [ newMilestoneId, - req.userId, + req.id, scenarioId, original.milestone_type, original.title, @@ -725,16 +693,14 @@ app.post('/api/premium/milestone/copy', authenticatePremiumUser, async (req, res original.progress, original.status, original.new_salary, - isUniversal, - originId, - now, - now + 1, + originId ]); // copy tasks - for (let t of tasks) { + for (let t of taskRows) { const newTaskId = uuidv4(); - await db.run(` + await pool.query(` INSERT INTO tasks ( id, milestone_id, @@ -742,27 +708,23 @@ app.post('/api/premium/milestone/copy', authenticatePremiumUser, async (req, res title, description, due_date, - status, - created_at, - updated_at + status ) - VALUES (?, ?, ?, ?, ?, ?, 'not_started', ?, ?) + VALUES (?, ?, ?, ?, ?, ?, 'not_started') `, [ newTaskId, newMilestoneId, - req.userId, + req.id, t.title, t.description, - t.due_date || null, - now, - now + t.due_date || null ]); } // copy impacts - for (let imp of impacts) { + for (let imp of impactRows) { const newImpactId = uuidv4(); - await db.run(` + await pool.query(` INSERT INTO milestone_impacts ( id, milestone_id, @@ -770,11 +732,9 @@ app.post('/api/premium/milestone/copy', authenticatePremiumUser, async (req, res direction, amount, start_date, - end_date, - created_at, - updated_at + end_date ) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) + VALUES (?, ?, ?, ?, ?, ?, ?) `, [ newImpactId, newMilestoneId, @@ -782,9 +742,7 @@ app.post('/api/premium/milestone/copy', authenticatePremiumUser, async (req, res imp.direction, imp.amount, imp.start_date || null, - imp.end_date || null, - now, - now + imp.end_date || null ]); } @@ -804,54 +762,51 @@ app.post('/api/premium/milestone/copy', authenticatePremiumUser, async (req, res // DELETE milestone from ALL scenarios app.delete('/api/premium/milestones/:milestoneId/all', authenticatePremiumUser, async (req, res) => { - const { milestoneId } = req.params; - try { - // 1) Fetch the milestone - const existing = await db.get(` + const { milestoneId } = req.params; + const [existingRows] = await pool.query(` SELECT id, user_id, origin_milestone_id FROM milestones WHERE id = ? AND user_id = ? - `, [milestoneId, req.userId]); + `, [milestoneId, req.id]); - if (!existing) { + if (!existingRows[0]) { return res.status(404).json({ error: 'Milestone not found or not owned by user.' }); } - - // We'll remove all milestones (the original + copies) referencing the same originId + const existing = existingRows[0]; const originId = existing.origin_milestone_id || existing.id; - // Find all those milestone IDs - const allMilsToDelete = await db.all(` + // find all + const [allMils] = await pool.query(` SELECT id FROM milestones WHERE user_id = ? AND (id = ? OR origin_milestone_id = ?) - `, [req.userId, originId, originId]); + `, [req.id, originId, originId]); - const milIDs = allMilsToDelete.map(m => m.id); + const milIDs = allMils.map(m => m.id); if (milIDs.length > 0) { const placeholders = milIDs.map(() => '?').join(','); - // Delete tasks for those milestones - await db.run(` + // tasks + await pool.query(` DELETE FROM tasks WHERE milestone_id IN (${placeholders}) `, milIDs); - // Delete impacts for those milestones - await db.run(` + // impacts + await pool.query(` DELETE FROM milestone_impacts WHERE milestone_id IN (${placeholders}) `, milIDs); - // Finally remove the milestones themselves - await db.run(` + // remove milestones + await pool.query(` DELETE FROM milestones WHERE user_id = ? AND (id = ? OR origin_milestone_id = ?) - `, [req.userId, originId, originId]); + `, [req.id, originId, originId]); } res.json({ message: 'Deleted from all scenarios' }); @@ -863,39 +818,34 @@ app.delete('/api/premium/milestones/:milestoneId/all', authenticatePremiumUser, // DELETE milestone from THIS scenario only app.delete('/api/premium/milestones/:milestoneId', authenticatePremiumUser, async (req, res) => { - const { milestoneId } = req.params; - try { - // 1) check user ownership - const existing = await db.get(` + const { milestoneId } = req.params; + + const [rows] = await pool.query(` SELECT id, user_id FROM milestones WHERE id = ? AND user_id = ? - `, [milestoneId, req.userId]); - - if (!existing) { + `, [milestoneId, req.id]); + if (!rows[0]) { return res.status(404).json({ error: 'Milestone not found or not owned by user.' }); } - // 2) Delete tasks associated with this milestone - await db.run(` + await pool.query(` DELETE FROM tasks WHERE milestone_id = ? `, [milestoneId]); - // 3) Delete milestone impacts - await db.run(` + await pool.query(` DELETE FROM milestone_impacts WHERE milestone_id = ? `, [milestoneId]); - // 4) Finally remove the milestone - await db.run(` + await pool.query(` DELETE FROM milestones WHERE id = ? AND user_id = ? - `, [milestoneId, req.userId]); + `, [milestoneId, req.id]); res.json({ message: 'Milestone deleted from this scenario.' }); } catch (err) { @@ -907,15 +857,15 @@ app.delete('/api/premium/milestones/:milestoneId', authenticatePremiumUser, asyn /* ------------------------------------------------------------------ FINANCIAL PROFILES ------------------------------------------------------------------ */ + app.get('/api/premium/financial-profile', authenticatePremiumUser, async (req, res) => { try { - const row = await db.get(` + const [rows] = await pool.query(` SELECT * FROM financial_profiles WHERE user_id = ? - `, [req.userId]); - - res.json(row || {}); + `, [req.id]); + res.json(rows[0] || {}); } catch (error) { console.error('Error fetching financial profile:', error); res.status(500).json({ error: 'Failed to fetch financial profile' }); @@ -937,14 +887,16 @@ app.post('/api/premium/financial-profile', authenticatePremiumUser, async (req, } = req.body; try { - const existing = await db.get(` + // see if profile exists + const [existingRows] = await pool.query(` SELECT user_id FROM financial_profiles WHERE user_id = ? - `, [req.userId]); + `, [req.id]); - if (!existing) { - await db.run(` + if (!existingRows[0]) { + // insert => let MySQL do created_at + await pool.query(` INSERT INTO financial_profiles ( user_id, current_salary, @@ -956,12 +908,11 @@ app.post('/api/premium/financial-profile', authenticatePremiumUser, async (req, retirement_contribution, emergency_contribution, extra_cash_emergency_pct, - extra_cash_retirement_pct, - created_at, - updated_at - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP) + extra_cash_retirement_pct + ) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `, [ - req.userId, + req.id, current_salary || 0, additional_income || 0, monthly_expenses || 0, @@ -974,7 +925,8 @@ app.post('/api/premium/financial-profile', authenticatePremiumUser, async (req, extra_cash_retirement_pct || 0 ]); } else { - await db.run(` + // update => updated_at = CURRENT_TIMESTAMP + await pool.query(` UPDATE financial_profiles SET current_salary = ?, @@ -1000,7 +952,7 @@ app.post('/api/premium/financial-profile', authenticatePremiumUser, async (req, emergency_contribution || 0, extra_cash_emergency_pct || 0, extra_cash_retirement_pct || 0, - req.userId + req.id ]); } @@ -1014,9 +966,10 @@ app.post('/api/premium/financial-profile', authenticatePremiumUser, async (req, /* ------------------------------------------------------------------ COLLEGE PROFILES ------------------------------------------------------------------ */ + app.post('/api/premium/college-profile', authenticatePremiumUser, async (req, res) => { const { - career_path_id, + career_profile_id, selected_school, selected_program, program_type, @@ -1042,14 +995,13 @@ app.post('/api/premium/college-profile', authenticatePremiumUser, async (req, re } = req.body; try { - const user_id = req.userId; const newId = uuidv4(); - await db.run(` + const sql = ` INSERT INTO college_profiles ( id, user_id, - career_path_id, + career_profile_id, selected_school, selected_program, program_type, @@ -1071,92 +1023,67 @@ app.post('/api/premium/college-profile', authenticatePremiumUser, async (req, re expected_salary, academic_calendar, tuition, - tuition_paid, - created_at, - updated_at + tuition_paid ) VALUES ( - :id, - :user_id, - :career_path_id, - :selected_school, - :selected_program, - :program_type, - :is_in_state, - :is_in_district, - :college_enrollment_status, - :annual_financial_aid, - :is_online, - :credit_hours_per_year, - :hours_completed, - :program_length, - :credit_hours_required, - :expected_graduation, - :existing_college_debt, - :interest_rate, - :loan_term, - :loan_deferral_until_graduation, - :extra_payment, - :expected_salary, - :academic_calendar, - :tuition, - :tuition_paid, - CURRENT_TIMESTAMP, - CURRENT_TIMESTAMP + ?, ?, ?, ?, ?, ?, + ?, ?, ?, ?, ?, + ?, ?, ?, ?, ?, + ?, ?, ?, ?, ?, + ?, ?, ?, ? ) - ON CONFLICT(user_id, career_path_id, selected_school, selected_program, program_type) - DO UPDATE SET - is_in_state = excluded.is_in_state, - is_in_district = excluded.is_in_district, - college_enrollment_status = excluded.college_enrollment_status, - annual_financial_aid = excluded.annual_financial_aid, - is_online = excluded.is_online, - credit_hours_per_year = excluded.credit_hours_per_year, - hours_completed = excluded.hours_completed, - program_length = excluded.program_length, - credit_hours_required = excluded.credit_hours_required, - expected_graduation = excluded.expected_graduation, - existing_college_debt = excluded.existing_college_debt, - interest_rate = excluded.interest_rate, - loan_term = excluded.loan_term, - loan_deferral_until_graduation = excluded.loan_deferral_until_graduation, - extra_payment = excluded.extra_payment, - expected_salary = excluded.expected_salary, - academic_calendar = excluded.academic_calendar, - tuition = excluded.tuition, - tuition_paid = excluded.tuition_paid, + ON DUPLICATE KEY UPDATE + is_in_state = VALUES(is_in_state), + is_in_district = VALUES(is_in_district), + college_enrollment_status = VALUES(college_enrollment_status), + annual_financial_aid = VALUES(annual_financial_aid), + is_online = VALUES(is_online), + credit_hours_per_year = VALUES(credit_hours_per_year), + hours_completed = VALUES(hours_completed), + program_length = VALUES(program_length), + credit_hours_required = VALUES(credit_hours_required), + expected_graduation = VALUES(expected_graduation), + existing_college_debt = VALUES(existing_college_debt), + interest_rate = VALUES(interest_rate), + loan_term = VALUES(loan_term), + loan_deferral_until_graduation = VALUES(loan_deferral_until_graduation), + extra_payment = VALUES(extra_payment), + expected_salary = VALUES(expected_salary), + academic_calendar = VALUES(academic_calendar), + tuition = VALUES(tuition), + tuition_paid = VALUES(tuition_paid), updated_at = CURRENT_TIMESTAMP - `, { - ':id': newId, - ':user_id': user_id, - ':career_path_id': career_path_id, - ':selected_school': selected_school, - ':selected_program': selected_program, - ':program_type': program_type || null, - ':is_in_state': is_in_state ? 1 : 0, - ':is_in_district': is_in_district ? 1 : 0, - ':college_enrollment_status': college_enrollment_status || null, - ':annual_financial_aid': annual_financial_aid || 0, - ':is_online': is_online ? 1 : 0, - ':credit_hours_per_year': credit_hours_per_year || 0, - ':hours_completed': hours_completed || 0, - ':program_length': program_length || 0, - ':credit_hours_required': credit_hours_required || 0, - ':expected_graduation': expected_graduation || null, - ':existing_college_debt': existing_college_debt || 0, - ':interest_rate': interest_rate || 0, - ':loan_term': loan_term || 10, - ':loan_deferral_until_graduation': loan_deferral_until_graduation ? 1 : 0, - ':extra_payment': extra_payment || 0, - ':expected_salary': expected_salary || 0, - ':academic_calendar': academic_calendar || 'semester', - ':tuition': tuition || 0, - ':tuition_paid': tuition_paid || 0 - }); + `; - res.status(201).json({ - message: 'College profile upsert done.' - }); + await pool.query(sql, [ + newId, + req.id, + career_profile_id, + selected_school, + selected_program, + program_type || null, + is_in_state ? 1 : 0, + is_in_district ? 1 : 0, + college_enrollment_status || null, + annual_financial_aid || 0, + is_online ? 1 : 0, + credit_hours_per_year || 0, + hours_completed || 0, + program_length || 0, + credit_hours_required || 0, + expected_graduation || null, + existing_college_debt || 0, + interest_rate || 0, + loan_term || 10, + loan_deferral_until_graduation ? 1 : 0, + extra_payment || 0, + expected_salary || 0, + academic_calendar || 'semester', + tuition || 0, + tuition_paid || 0 + ]); + + res.status(201).json({ message: 'College profile upsert done.' }); } catch (error) { console.error('Error saving college profile:', error); res.status(500).json({ error: 'Failed to save college profile.' }); @@ -1164,48 +1091,74 @@ app.post('/api/premium/college-profile', authenticatePremiumUser, async (req, re }); app.get('/api/premium/college-profile', authenticatePremiumUser, async (req, res) => { - const { careerPathId } = req.query; + const { careerProfileId } = req.query; try { - const row = await db.get(` + const [rows] = await pool.query(` SELECT * FROM college_profiles WHERE user_id = ? - AND career_path_id = ? + AND career_profile_id = ? ORDER BY created_at DESC LIMIT 1 - `, [req.userId, careerPathId]); - res.json(row || {}); + `, [req.id, careerProfileId]); + + res.json(rows[0] || {}); } catch (error) { console.error('Error fetching college profile:', error); res.status(500).json({ error: 'Failed to fetch college profile.' }); } }); +/* ------------------------------------------------------------------ + AI-SUGGESTED MILESTONES + ------------------------------------------------------------------ */ app.post('/api/premium/milestone/ai-suggestions', authenticatePremiumUser, async (req, res) => { - const { career, projectionData, existingMilestones, careerPathId, regenerate } = req.body; + const { career, projectionData, existingMilestones, careerProfileId, regenerate } = req.body; - if (!career || !careerPathId || !projectionData || projectionData.length === 0) { - return res.status(400).json({ error: 'career, careerPathId, and valid projectionData are required.' }); + if (!career || !careerProfileId || !projectionData || projectionData.length === 0) { + return res.status(400).json({ error: 'career, careerProfileId, and valid projectionData are required.' }); } - if (!regenerate) { - const existingSuggestion = await db.get(` - SELECT suggested_milestones FROM ai_suggested_milestones - WHERE user_id = ? AND career_path_id = ? - `, [req.userId, careerPathId]); + // Possibly define "careerGoals" or "previousSuggestionsContext" + const careerGoals = ''; // placeholder + const previousSuggestionsContext = ''; // placeholder - if (existingSuggestion) { - return res.json({ suggestedMilestones: JSON.parse(existingSuggestion.suggested_milestones) }); + // If not regenerating, see if we have an existing suggestion + if (!regenerate) { + const [rows] = await pool.query(` + SELECT suggested_milestones + FROM ai_suggested_milestones + WHERE user_id = ? + AND career_profile_id = ? + `, [req.id, careerProfileId]); + + if (rows[0]) { + return res.json({ suggestedMilestones: JSON.parse(rows[0].suggested_milestones) }); } } - // Explicitly regenerate (delete existing cached suggestions if any) - await db.run(` - DELETE FROM ai_suggested_milestones WHERE user_id = ? AND career_path_id = ? - `, [req.userId, careerPathId]); + // delete existing suggestions if any + await pool.query(` + DELETE FROM ai_suggested_milestones + WHERE user_id = ? + AND career_profile_id = ? + `, [req.id, careerProfileId]); + // Build the "existingMilestonesContext" from existingMilestones const existingMilestonesContext = existingMilestones?.map(m => `- ${m.title} (${m.date})`).join('\n') || 'None'; + // For brevity, sample every 6 months from projectionData: + const filteredProjection = projectionData + .filter((_, i) => i % 6 === 0) + .map(m => ` +- Month: ${m.month} + Salary: ${m.salary} + Loan Balance: ${m.loanBalance} + Emergency Savings: ${m.totalEmergencySavings} + Retirement Savings: ${m.totalRetirementSavings}`) + .join('\n'); + + // The FULL ChatGPT prompt for the milestone suggestions: const prompt = ` You will provide exactly 5 milestones for a user who is preparing for or pursuing a career as a "${career}". @@ -1219,12 +1172,7 @@ Immediately Previous Suggestions (MUST explicitly avoid these): ${previousSuggestionsContext} Financial Projection Snapshot (every 6 months, for brevity): -${projectionData.filter((_, i) => i % 6 === 0).map(m => ` -- Month: ${m.month} - Salary: ${m.salary} - Loan Balance: ${m.loanBalance} - Emergency Savings: ${m.totalEmergencySavings} - Retirement Savings: ${m.totalRetirementSavings}`).join('\n')} +${filteredProjection} Milestone Requirements: 1. Provide exactly 3 SHORT-TERM milestones (within next 1-2 years). @@ -1254,25 +1202,29 @@ IMPORTANT: - No additional commentary or text beyond the JSON array. `; - - try { + const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY }); const completion = await openai.chat.completions.create({ model: 'gpt-4-turbo', messages: [{ role: 'user', content: prompt }], - temperature: 0.2, + temperature: 0.2 }); let content = completion?.choices?.[0]?.message?.content?.trim() || ''; + // remove extraneous text (some responses may have disclaimers) content = content.replace(/^[^{[]+/, '').replace(/[^}\]]+$/, ''); - const suggestedMilestones = JSON.parse(content); const newId = uuidv4(); - await db.run(` - INSERT INTO ai_suggested_milestones (id, user_id, career_path_id, suggested_milestones) + await pool.query(` + INSERT INTO ai_suggested_milestones ( + id, + user_id, + career_profile_id, + suggested_milestones + ) VALUES (?, ?, ?, ?) - `, [newId, req.userId, careerPathId, JSON.stringify(suggestedMilestones)]); + `, [newId, req.id, careerProfileId, JSON.stringify(suggestedMilestones)]); res.json({ suggestedMilestones }); } catch (error) { @@ -1281,13 +1233,11 @@ IMPORTANT: } }); - - /* ------------------------------------------------------------------ FINANCIAL PROJECTIONS ------------------------------------------------------------------ */ -app.post('/api/premium/financial-projection/:careerPathId', authenticatePremiumUser, async (req, res) => { - const { careerPathId } = req.params; +app.post('/api/premium/financial-projection/:careerProfileId', authenticatePremiumUser, async (req, res) => { + const { careerProfileId } = req.params; const { projectionData, loanPaidOffMonth, @@ -1298,17 +1248,23 @@ app.post('/api/premium/financial-projection/:careerPathId', authenticatePremiumU try { const projectionId = uuidv4(); - await db.run(` + // let MySQL handle created_at / updated_at + await pool.query(` INSERT INTO financial_projections ( - id, user_id, career_path_id, projection_data, - loan_paid_off_month, final_emergency_savings, - final_retirement_savings, final_loan_balance, - created_at, updated_at - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP) + id, + user_id, + career_profile_id, + projection_data, + loan_paid_off_month, + final_emergency_savings, + final_retirement_savings, + final_loan_balance + ) + VALUES (?, ?, ?, ?, ?, ?, ?, ?) `, [ projectionId, - req.userId, - careerPathId, + req.id, + careerProfileId, JSON.stringify(projectionData), loanPaidOffMonth || null, finalEmergencySavings || 0, @@ -1323,26 +1279,30 @@ app.post('/api/premium/financial-projection/:careerPathId', authenticatePremiumU } }); -app.get('/api/premium/financial-projection/:careerPathId', authenticatePremiumUser, async (req, res) => { - const { careerPathId } = req.params; +app.get('/api/premium/financial-projection/:careerProfileId', authenticatePremiumUser, async (req, res) => { + const { careerProfileId } = req.params; try { - const row = await db.get(` - SELECT projection_data, loan_paid_off_month, - final_emergency_savings, final_retirement_savings, final_loan_balance + const [rows] = await pool.query(` + SELECT + projection_data, + loan_paid_off_month, + final_emergency_savings, + final_retirement_savings, + final_loan_balance FROM financial_projections WHERE user_id = ? - AND career_path_id = ? + AND career_profile_id = ? ORDER BY created_at DESC LIMIT 1 - `, [req.userId, careerPathId]); + `, [req.id, careerProfileId]); - if (!row) { + if (!rows[0]) { return res.status(404).json({ error: 'Projection not found.' }); } - const parsedProjectionData = JSON.parse(row.projection_data); + const row = rows[0]; res.status(200).json({ - projectionData: parsedProjectionData, + projectionData: JSON.parse(row.projection_data), loanPaidOffMonth: row.loan_paid_off_month, finalEmergencySavings: row.final_emergency_savings, finalRetirementSavings: row.final_retirement_savings, @@ -1358,7 +1318,7 @@ app.get('/api/premium/financial-projection/:careerPathId', authenticatePremiumUs TASK ENDPOINTS ------------------------------------------------------------------ */ -// CREATE a new task (already existed, repeated here for clarity) +// CREATE a new task app.post('/api/premium/tasks', authenticatePremiumUser, async (req, res) => { try { const { milestone_id, title, description, due_date } = req.body; @@ -1369,20 +1329,19 @@ app.post('/api/premium/tasks', authenticatePremiumUser, async (req, res) => { }); } - // Confirm milestone is owned by this user - const milestone = await db.get(` - SELECT user_id + // confirm milestone is owned by user + const [milRows] = await pool.query(` + SELECT id, user_id FROM milestones WHERE id = ? `, [milestone_id]); - if (!milestone || milestone.user_id !== req.userId) { + + if (!milRows[0] || milRows[0].user_id !== req.id) { return res.status(403).json({ error: 'Milestone not found or not yours.' }); } const taskId = uuidv4(); - const now = new Date().toISOString(); - - await db.run(` + await pool.query(` INSERT INTO tasks ( id, milestone_id, @@ -1390,25 +1349,22 @@ app.post('/api/premium/tasks', authenticatePremiumUser, async (req, res) => { title, description, due_date, - status, - created_at, - updated_at - ) VALUES (?, ?, ?, ?, ?, ?, 'not_started', ?, ?) + status + ) + VALUES (?, ?, ?, ?, ?, ?, 'not_started') `, [ taskId, milestone_id, - req.userId, + req.id, title, description || '', - due_date || null, - now, - now + due_date || null ]); const newTask = { id: taskId, milestone_id, - user_id: req.userId, + user_id: req.id, title, description: description || '', due_date: due_date || null, @@ -1427,43 +1383,37 @@ app.put('/api/premium/tasks/:taskId', authenticatePremiumUser, async (req, res) const { taskId } = req.params; const { title, description, due_date, status } = req.body; - // Check ownership - const existing = await db.get(` - SELECT user_id + const [rows] = await pool.query(` + SELECT id, user_id FROM tasks WHERE id = ? `, [taskId]); - - if (!existing || existing.user_id !== req.userId) { + if (!rows[0] || rows[0].user_id !== req.id) { return res.status(404).json({ error: 'Task not found or not owned by you.' }); } - const now = new Date().toISOString(); - await db.run(` + await pool.query(` UPDATE tasks SET title = COALESCE(?, title), description = COALESCE(?, description), due_date = COALESCE(?, due_date), status = COALESCE(?, status), - updated_at = ? + updated_at = CURRENT_TIMESTAMP WHERE id = ? `, [ title || null, description || null, due_date || null, status || null, - now, taskId ]); - // Return the updated task - const updatedTask = await db.get(` + const [[updatedTask]] = await pool.query(` SELECT * FROM tasks WHERE id = ? `, [taskId]); - res.json(updatedTask); } catch (err) { console.error('Error updating task:', err); @@ -1476,18 +1426,16 @@ app.delete('/api/premium/tasks/:taskId', authenticatePremiumUser, async (req, re try { const { taskId } = req.params; - // Verify ownership - const existing = await db.get(` - SELECT user_id + const [rows] = await pool.query(` + SELECT id, user_id FROM tasks WHERE id = ? `, [taskId]); - - if (!existing || existing.user_id !== req.userId) { + if (!rows[0] || rows[0].user_id !== req.id) { return res.status(404).json({ error: 'Task not found or not owned by you.' }); } - await db.run(` + await pool.query(` DELETE FROM tasks WHERE id = ? `, [taskId]); @@ -1510,18 +1458,18 @@ app.get('/api/premium/milestone-impacts', authenticatePremiumUser, async (req, r return res.status(400).json({ error: 'milestone_id is required.' }); } - // Verify the milestone belongs to this user - const milestoneRow = await db.get(` - SELECT user_id + // verify user owns the milestone + const [mRows] = await pool.query(` + SELECT id, user_id FROM milestones WHERE id = ? `, [milestone_id]); - if (!milestoneRow || milestoneRow.user_id !== req.userId) { - return res.status(404).json({ error: 'Milestone not found or not owned by this user.' }); + if (!mRows[0] || mRows[0].user_id !== req.id) { + return res.status(404).json({ error: 'Milestone not found or not yours.' }); } - const impacts = await db.all(` - SELECT + const [impacts] = await pool.query(` + SELECT id, milestone_id, impact_type, @@ -1551,33 +1499,25 @@ app.post('/api/premium/milestone-impacts', authenticatePremiumUser, async (req, direction = 'subtract', amount = 0, start_date = null, - end_date = null, - created_at, - updated_at + end_date = null } = req.body; if (!milestone_id || !impact_type) { - return res.status(400).json({ - error: 'milestone_id and impact_type are required.' - }); + return res.status(400).json({ error: 'milestone_id and impact_type are required.' }); } - // Confirm user owns the milestone - const milestoneRow = await db.get(` - SELECT user_id + // confirm user owns the milestone + const [mRows] = await pool.query(` + SELECT id, user_id FROM milestones WHERE id = ? `, [milestone_id]); - if (!milestoneRow || milestoneRow.user_id !== req.userId) { + if (!mRows[0] || mRows[0].user_id !== req.id) { return res.status(403).json({ error: 'Milestone not found or not owned by this user.' }); } const newUUID = uuidv4(); - const now = new Date().toISOString(); - const finalCreated = created_at || now; - const finalUpdated = updated_at || now; - - await db.run(` + await pool.query(` INSERT INTO milestone_impacts ( id, milestone_id, @@ -1585,11 +1525,9 @@ app.post('/api/premium/milestone-impacts', authenticatePremiumUser, async (req, direction, amount, start_date, - end_date, - created_at, - updated_at + end_date ) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) + VALUES (?, ?, ?, ?, ?, ?, ?) `, [ newUUID, milestone_id, @@ -1597,22 +1535,11 @@ app.post('/api/premium/milestone-impacts', authenticatePremiumUser, async (req, direction, amount, start_date, - end_date, - finalCreated, - finalUpdated + end_date ]); - const insertedRow = await db.get(` - SELECT - id, - milestone_id, - impact_type, - direction, - amount, - start_date, - end_date, - created_at, - updated_at + const [[insertedRow]] = await pool.query(` + SELECT * FROM milestone_impacts WHERE id = ? `, [newUUID]); @@ -1638,18 +1565,17 @@ app.put('/api/premium/milestone-impacts/:impactId', authenticatePremiumUser, asy } = req.body; // check ownership - const existing = await db.get(` - SELECT mi.id, m.user_id + const [rows] = await pool.query(` + SELECT mi.id AS impact_id, m.user_id FROM milestone_impacts mi JOIN milestones m ON mi.milestone_id = m.id WHERE mi.id = ? `, [impactId]); - if (!existing || existing.user_id !== req.userId) { - return res.status(404).json({ error: 'Impact not found or not owned by user.' }); + if (!rows[0] || rows[0].user_id !== req.id) { + return res.status(404).json({ error: 'Impact not found or not yours.' }); } - const now = new Date().toISOString(); - await db.run(` + await pool.query(` UPDATE milestone_impacts SET milestone_id = ?, @@ -1658,7 +1584,7 @@ app.put('/api/premium/milestone-impacts/:impactId', authenticatePremiumUser, asy amount = ?, start_date = ?, end_date = ?, - updated_at = ? + updated_at = CURRENT_TIMESTAMP WHERE id = ? `, [ milestone_id, @@ -1667,21 +1593,11 @@ app.put('/api/premium/milestone-impacts/:impactId', authenticatePremiumUser, asy amount, start_date, end_date, - now, impactId ]); - const updatedRow = await db.get(` - SELECT - id, - milestone_id, - impact_type, - direction, - amount, - start_date, - end_date, - created_at, - updated_at + const [[updatedRow]] = await pool.query(` + SELECT * FROM milestone_impacts WHERE id = ? `, [impactId]); @@ -1699,18 +1615,18 @@ app.delete('/api/premium/milestone-impacts/:impactId', authenticatePremiumUser, const { impactId } = req.params; // check ownership - const existing = await db.get(` - SELECT mi.id, m.user_id + const [rows] = await pool.query(` + SELECT mi.id AS impact_id, m.user_id FROM milestone_impacts mi JOIN milestones m ON mi.milestone_id = m.id WHERE mi.id = ? `, [impactId]); - if (!existing || existing.user_id !== req.userId) { + if (!rows[0] || rows[0].user_id !== req.id) { return res.status(404).json({ error: 'Impact not found or not owned by user.' }); } - await db.run(` + await pool.query(` DELETE FROM milestone_impacts WHERE id = ? `, [impactId]); @@ -1726,17 +1642,12 @@ app.delete('/api/premium/milestone-impacts/:impactId', authenticatePremiumUser, RESUME OPTIMIZATION ENDPOINT ------------------------------------------------------------------ */ - -const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY }); - // Setup file upload via multer const upload = multer({ dest: 'uploads/' }); -// Basic usage gating config -const MAX_MONTHLY_REWRITES_PREMIUM = 2; - -// Helper: build GPT prompt -const buildResumePrompt = (resumeText, jobTitle, jobDescription) => ` +function buildResumePrompt(resumeText, jobTitle, jobDescription) { + // Full ChatGPT prompt for resume optimization: + return ` You are an expert resume writer specialized in precisely tailoring existing resumes for optimal ATS compatibility and explicit alignment with provided job descriptions. STRICT GUIDELINES: @@ -1759,12 +1670,13 @@ ${resumeText} Precisely Tailored, ATS-Optimized Resume: `; +} async function extractTextFromPDF(filePath) { const fileBuffer = await fs.readFile(filePath); - const uint8Array = new Uint8Array(fileBuffer); // Convert Buffer explicitly + const uint8Array = new Uint8Array(fileBuffer); const pdfDoc = await getDocument({ data: uint8Array }).promise; - + let text = ''; for (let pageNum = 1; pageNum <= pdfDoc.numPages; pageNum++) { const page = await pdfDoc.getPage(pageNum); @@ -1774,8 +1686,6 @@ async function extractTextFromPDF(filePath) { return text; } - -// Your corrected endpoint with limits correctly returned: app.post( '/api/premium/resume/optimize', upload.single('resumeFile'), @@ -1787,19 +1697,27 @@ app.post( return res.status(400).json({ error: 'Missing required fields.' }); } - const userId = req.userId; + const id = req.id; const now = new Date(); - const userProfile = await db.get( - `SELECT is_premium, is_pro_premium, resume_optimizations_used, resume_limit_reset, resume_booster_count - FROM user_profile - WHERE user_id = ?`, - [userId] - ); + // fetch user_profile row + const [profileRows] = await pool.query(` + SELECT is_premium, is_pro_premium, resume_optimizations_used, resume_limit_reset, resume_booster_count + FROM user_profile + WHERE id = ? + `, [id]); + const userProfile = profileRows[0]; + if (!userProfile) { + return res.status(404).json({ error: 'User not found.' }); + } + // figure out usage limit let userPlan = 'basic'; - if (userProfile?.is_pro_premium) userPlan = 'pro'; - else if (userProfile?.is_premium) userPlan = 'premium'; + if (userProfile.is_pro_premium) { + userPlan = 'pro'; + } else if (userProfile.is_premium) { + userPlan = 'premium'; + } const weeklyLimits = { basic: 1, premium: 2, pro: 5 }; const userWeeklyLimit = weeklyLimits[userPlan] || 0; @@ -1808,19 +1726,23 @@ app.post( if (!userProfile.resume_limit_reset || now > resetDate) { resetDate = new Date(now); resetDate.setDate(now.getDate() + 7); - await db.run( - `UPDATE user_profile SET resume_optimizations_used = 0, resume_limit_reset = ? WHERE user_id = ?`, - [resetDate.toISOString(), userId] - ); + + await pool.query(` + UPDATE user_profile + SET resume_optimizations_used = 0, + resume_limit_reset = ? + WHERE id = ? + `, [resetDate.toISOString(), id]); + userProfile.resume_optimizations_used = 0; } const totalLimit = userWeeklyLimit + (userProfile.resume_booster_count || 0); - if (userProfile.resume_optimizations_used >= totalLimit) { - return res.status(403).json({ error: 'Weekly resume optimization limit reached. Consider purchasing a booster pack.' }); + return res.status(403).json({ error: 'Weekly resume optimization limit reached.' }); } + // parse file const filePath = req.file.path; const mimeType = req.file.mimetype; @@ -1838,30 +1760,36 @@ app.post( return res.status(400).json({ error: 'Unsupported or corrupted file upload.' }); } + // Build GPT prompt const prompt = buildResumePrompt(resumeText, jobTitle, jobDescription); + // Call OpenAI + const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY }); const completion = await openai.chat.completions.create({ model: 'gpt-4-turbo', messages: [{ role: 'user', content: prompt }], - temperature: 0.7, + temperature: 0.7 }); const optimizedResume = completion?.choices?.[0]?.message?.content?.trim() || ''; - await db.run( - `UPDATE user_profile SET resume_optimizations_used = resume_optimizations_used + 1 WHERE user_id = ?`, - [userId] - ); + // increment usage + await pool.query(` + UPDATE user_profile + SET resume_optimizations_used = resume_optimizations_used + 1 + WHERE id = ? + `, [id]); const remainingOptimizations = totalLimit - (userProfile.resume_optimizations_used + 1); + // remove uploaded file await fs.unlink(filePath); + res.json({ optimizedResume, remainingOptimizations, - resetDate: resetDate.toISOString() // <-- explicitly returned here! + resetDate: resetDate.toISOString() }); - } catch (err) { console.error('Error optimizing resume:', err); res.status(500).json({ error: 'Failed to optimize resume.' }); @@ -1869,67 +1797,65 @@ app.post( } ); -app.get( - '/api/premium/resume/remaining', - authenticatePremiumUser, - async (req, res) => { - try { - const userId = req.userId; - const now = new Date(); +app.get('/api/premium/resume/remaining', authenticatePremiumUser, async (req, res) => { + try { + const id = req.id; + const now = new Date(); - const userProfile = await db.get( - `SELECT is_premium, is_pro_premium, resume_optimizations_used, resume_limit_reset, resume_booster_count - FROM user_profile - WHERE user_id = ?`, - [userId] - ); - - let userPlan = 'basic'; - if (userProfile?.is_pro_premium) userPlan = 'pro'; - else if (userProfile?.is_premium) userPlan = 'premium'; - - const weeklyLimits = { basic: 1, premium: 2, pro: 5 }; - const userWeeklyLimit = weeklyLimits[userPlan] || 0; - - let resetDate = new Date(userProfile.resume_limit_reset); - if (!userProfile.resume_limit_reset || now > resetDate) { - resetDate = new Date(now); - resetDate.setDate(now.getDate() + 7); - await db.run( - `UPDATE user_profile SET resume_optimizations_used = 0, resume_limit_reset = ? WHERE user_id = ?`, - [resetDate.toISOString(), userId] - ); - userProfile.resume_optimizations_used = 0; - } - - const totalLimit = userWeeklyLimit + (userProfile.resume_booster_count || 0); - const remainingOptimizations = totalLimit - userProfile.resume_optimizations_used; - - res.json({ remainingOptimizations, resetDate }); - } catch (err) { - console.error('Error fetching remaining optimizations:', err); - res.status(500).json({ error: 'Failed to fetch remaining optimizations.' }); + const [rows] = await pool.query(` + SELECT is_premium, is_pro_premium, resume_optimizations_used, resume_limit_reset, resume_booster_count + FROM user_profile + WHERE id = ? + `, [id]); + const userProfile = rows[0]; + if (!userProfile) { + return res.status(404).json({ error: 'User not found.' }); } + + let userPlan = 'basic'; + if (userProfile.is_pro_premium) { + userPlan = 'pro'; + } else if (userProfile.is_premium) { + userPlan = 'premium'; + } + + const weeklyLimits = { basic: 1, premium: 2, pro: 5 }; + const userWeeklyLimit = weeklyLimits[userPlan] || 0; + + let resetDate = new Date(userProfile.resume_limit_reset); + if (!userProfile.resume_limit_reset || now > resetDate) { + resetDate = new Date(now); + resetDate.setDate(now.getDate() + 7); + + await pool.query(` + UPDATE user_profile + SET resume_optimizations_used = 0, + resume_limit_reset = ? + WHERE id = ? + `, [resetDate.toISOString(), id]); + + userProfile.resume_optimizations_used = 0; + } + + const totalLimit = userWeeklyLimit + (userProfile.resume_booster_count || 0); + const remainingOptimizations = totalLimit - userProfile.resume_optimizations_used; + + res.json({ remainingOptimizations, resetDate }); + } catch (err) { + console.error('Error fetching remaining optimizations:', err); + res.status(500).json({ error: 'Failed to fetch remaining optimizations.' }); } -); +}); - -// Helper function to get the week number -function getWeekNumber(date) { - const oneJan = new Date(date.getFullYear(), 0, 1); - const numberOfDays = Math.floor((date - oneJan) / (24 * 60 * 60 * 1000)); - return Math.ceil((date.getDay() + 1 + numberOfDays) / 7); -} - - /* ------------------------------------------------------------------ - FALLBACK (404 for unmatched routes) + FALLBACK 404 ------------------------------------------------------------------ */ app.use((req, res) => { console.warn(`No route matched for ${req.method} ${req.originalUrl}`); res.status(404).json({ error: 'Not found' }); }); +// Start server app.listen(PORT, () => { - console.log(`Premium server running on http://localhost:${PORT}`); + console.log(`Premium server (MySQL) running on http://localhost:${PORT}`); }); diff --git a/schema.sql b/schema.sql new file mode 100644 index 0000000..e69de29 diff --git a/src/App.js b/src/App.js index 405b32c..db7f8ec 100644 --- a/src/App.js +++ b/src/App.js @@ -131,6 +131,7 @@ function App() { text-xs sm:text-sm md:text-base font-semibold `} + onClick={() => navigate('/planning')} > Find Your Career @@ -163,16 +164,12 @@ function App() { text-xs sm:text-sm md:text-base font-semibold `} + onClick={() => navigate('/preparing')} > Prepare for Your Career
- - Preparing Landing - + {/* Only Educational Programs as submenu */} navigate('/enhancing')} > Enhancing Your Career {!canAccessPremium && ( @@ -202,7 +200,13 @@ function App() { )}
- {/* Add your premium sub-links here */} + + Resume Optimizer + + {/* Add more enhancing submenu items here if needed */}
@@ -219,6 +223,7 @@ function App() { text-xs sm:text-sm md:text-base font-semibold `} + onClick={() => navigate('/retirement')} > Retirement Planning {!canAccessPremium && ( @@ -226,7 +231,14 @@ function App() { )}
- {/* Add your premium sub-links here */} + {/* Example retirement submenu item */} + {/* + Financial Tools + */} + {/* Add more retirement submenu items here if needed */}
diff --git a/src/components/AISuggestedMilestones.js b/src/components/AISuggestedMilestones.js index ec2045f..5967a61 100644 --- a/src/components/AISuggestedMilestones.js +++ b/src/components/AISuggestedMilestones.js @@ -1,7 +1,7 @@ import React, { useEffect, useState } from 'react'; import { Button } from './ui/button.js'; -const AISuggestedMilestones = ({ userId, career, careerPathId, authFetch, activeView, projectionData }) => { +const AISuggestedMilestones = ({ id, career, careerProfileId, authFetch, activeView, projectionData }) => { const [suggestedMilestones, setSuggestedMilestones] = useState([]); const [selected, setSelected] = useState([]); const [loading, setLoading] = useState(false); @@ -9,7 +9,7 @@ const AISuggestedMilestones = ({ userId, career, careerPathId, authFetch, active useEffect(() => { const fetchAISuggestions = async () => { - if (!career || !careerPathId || !Array.isArray(projectionData) || projectionData.length === 0) { + if (!career || !careerProfileId || !Array.isArray(projectionData) || projectionData.length === 0) { console.warn('Holding fetch, required data not yet available.'); setAiLoading(true); return; @@ -17,12 +17,12 @@ const AISuggestedMilestones = ({ userId, career, careerPathId, authFetch, active setAiLoading(true); try { - const milestonesRes = await authFetch(`/api/premium/milestones?careerPathId=${careerPathId}`); + const milestonesRes = await authFetch(`/api/premium/milestones?careerProfileId=${careerProfileId}`); const { milestones } = await milestonesRes.json(); const response = await authFetch('/api/premium/milestone/ai-suggestions', { method: 'POST', - body: JSON.stringify({ career, careerPathId, projectionData, existingMilestones: milestones }), + body: JSON.stringify({ career, careerProfileId, projectionData, existingMilestones: milestones }), headers: { 'Content-Type': 'application/json' } }); @@ -43,12 +43,12 @@ const AISuggestedMilestones = ({ userId, career, careerPathId, authFetch, active }; fetchAISuggestions(); - }, [career, careerPathId, projectionData, authFetch]); + }, [career, careerProfileId, projectionData, authFetch]); const regenerateSuggestions = async () => { setAiLoading(true); try { - const milestonesRes = await authFetch(`/api/premium/milestones?careerPathId=${careerPathId}`); + const milestonesRes = await authFetch(`/api/premium/milestones?careerProfileId=${careerProfileId}`); const { milestones } = await milestonesRes.json(); const previouslySuggestedMilestones = suggestedMilestones; @@ -63,7 +63,7 @@ const AISuggestedMilestones = ({ userId, career, careerPathId, authFetch, active method: 'POST', body: JSON.stringify({ career, - careerPathId, + careerProfileId, projectionData: sampledProjectionData, existingMilestones: milestones, previouslySuggestedMilestones, @@ -107,7 +107,7 @@ const AISuggestedMilestones = ({ userId, career, careerPathId, authFetch, active date: m.date, progress: m.progress, milestone_type: activeView || 'Career', - career_path_id: careerPathId + career_profile_id: careerProfileId }; }); diff --git a/src/components/CareerSelectDropdown.js b/src/components/CareerSelectDropdown.js index 6fb392c..5efcafa 100644 --- a/src/components/CareerSelectDropdown.js +++ b/src/components/CareerSelectDropdown.js @@ -1,9 +1,9 @@ // src/components/CareerSelectDropdown.js import React from 'react'; -const CareerSelectDropdown = ({ existingCareerPaths, selectedCareer, onChange, loading, authFetch }) => { - const fetchMilestones = (careerPathId) => { - authFetch(`/api/premium/milestones?careerPathId=${careerPathId}`) +const CareerSelectDropdown = ({ existingCareerProfiles, selectedCareer, onChange, loading, authFetch }) => { + const fetchMilestones = (careerProfileId) => { + authFetch(`/api/premium/milestones?careerProfileId=${careerProfileId}`) .then((response) => response.json()) .then((data) => { console.log('Milestones:', data); @@ -34,12 +34,12 @@ const CareerSelectDropdown = ({ existingCareerPaths, selectedCareer, onChange, l value={selectedCareer?.id || ''} onChange={(e) => { const selectedId = e.target.value; - const selected = existingCareerPaths.find(path => path.id === selectedId); + const selected = existingCareerProfiles.find(path => path.id === selectedId); handleChange(selected); // ✅ Pass the full object }} > - {existingCareerPaths.map((path) => ( + {existingCareerProfiles.map((path) => ( diff --git a/src/components/Dashboard.js b/src/components/Dashboard.js index 9fe9898..cf09c67 100644 --- a/src/components/Dashboard.js +++ b/src/components/Dashboard.js @@ -624,7 +624,7 @@ function Dashboard() { className="confirm-btn" onClick={() => { localStorage.removeItem('token'); - localStorage.removeItem('UserId'); + localStorage.removeItem('id'); setShowSessionExpiredModal(false); navigate('/signin'); }} diff --git a/src/components/EnhancingLanding.js b/src/components/EnhancingLanding.js index cca9f87..fce6094 100644 --- a/src/components/EnhancingLanding.js +++ b/src/components/EnhancingLanding.js @@ -2,22 +2,45 @@ import React from 'react'; import { useNavigate } from 'react-router-dom'; import { Button } from './ui/button.js'; -function EnhancingLanding() { +import EconomicProjections from './EconomicProjections.js'; + +function EnhancingLanding({ userProfile }) { const navigate = useNavigate(); + const socCode = userProfile?.socCode; + const stateName = userProfile?.state; + return ( -
-
-

- Enhancing Your Career -

-

- AptivaAI helps you advance your career. Plan career milestones, enhance your skill set, optimize your resume, and prepare for promotions or transitions. -

-
- - -
+
+
+ + {/* Section 1: Current Career Status */} +
+

📌 Where Am I Now?

+ +
+ + {/* Section 2: Actionable Next Steps */} +
+

🚩 What's Next For Me?

+

+ Identify your next career milestones with AI-driven recommendations and start advancing your career today. +

+ +
+ + {/* Section 3: Interactive Planning & Resume Optimization */} +
+

🚀 How Do I Get There?

+

+ Use our comprehensive planning tools to visualize career paths, optimize your resume, and explore financial scenarios. +

+
+ + +
+
+
); diff --git a/src/components/MilestoneTimeline.js b/src/components/MilestoneTimeline.js index da1cd1a..0b5a00e 100644 --- a/src/components/MilestoneTimeline.js +++ b/src/components/MilestoneTimeline.js @@ -4,12 +4,12 @@ import React, { useEffect, useState, useCallback } from 'react'; import { Button } from './ui/button.js'; /** - * Renders a simple vertical list of milestones for the given careerPathId. + * Renders a simple vertical list of milestones for the given careerProfileId. * Also includes Task CRUD (create/edit/delete) for each milestone, * plus a small "copy milestone" wizard, "financial impacts" form, etc. */ export default function MilestoneTimeline({ - careerPathId, + careerProfileId, authFetch, activeView, // 'Career' or 'Financial' setActiveView, // optional, if you need to switch between views @@ -81,9 +81,9 @@ export default function MilestoneTimeline({ // 2) Fetch milestones => store in "milestones[Career]" / "milestones[Financial]" // ------------------------------------------------------------------ const fetchMilestones = useCallback(async () => { - if (!careerPathId) return; + if (!careerProfileId) return; try { - const res = await authFetch(`/api/premium/milestones?careerPathId=${careerPathId}`); + const res = await authFetch(`/api/premium/milestones?careerProfileId=${careerProfileId}`); if (!res.ok) { console.error('Failed to fetch milestones. Status:', res.status); return; @@ -107,7 +107,7 @@ export default function MilestoneTimeline({ } catch (err) { console.error('Failed to fetch milestones:', err); } - }, [careerPathId, authFetch]); + }, [careerProfileId, authFetch]); useEffect(() => { fetchMilestones(); @@ -122,7 +122,7 @@ export default function MilestoneTimeline({ const res = await authFetch('/api/premium/career-profile/all'); if (res.ok) { const data = await res.json(); - setScenarios(data.careerPaths || []); + setScenarios(data.careerProfiles || []); } } catch (err) { console.error('Error loading scenarios for copy wizard:', err); @@ -185,7 +185,7 @@ export default function MilestoneTimeline({ title: newMilestone.title, description: newMilestone.description, date: newMilestone.date, - career_path_id: careerPathId, + career_profile_id: careerProfileId, progress: newMilestone.progress, status: newMilestone.progress >= 100 ? 'completed' : 'planned', new_salary: diff --git a/src/components/MilestoneTracker.js b/src/components/MilestoneTracker.js index bade554..41e79b7 100644 --- a/src/components/MilestoneTracker.js +++ b/src/components/MilestoneTracker.js @@ -1,5 +1,5 @@ import React, { useState, useEffect } from 'react'; -import { useLocation, useNavigate } from 'react-router-dom'; +import { useLocation } from 'react-router-dom'; import { Line } from 'react-chartjs-2'; import { Chart as ChartJS, @@ -12,18 +12,20 @@ import { } from 'chart.js'; import annotationPlugin from 'chartjs-plugin-annotation'; import { Filler } from 'chart.js'; -import { Button } from './ui/button.js'; + import authFetch from '../utils/authFetch.js'; +import { simulateFinancialProjection } from '../utils/FinancialProjectionService.js'; + +import { Button } from './ui/button.js'; import CareerSelectDropdown from './CareerSelectDropdown.js'; import CareerSearch from './CareerSearch.js'; - -import MilestoneTimeline from './MilestoneTimeline.js'; // Key: This handles Milestone & Task CRUD +import MilestoneTimeline from './MilestoneTimeline.js'; import AISuggestedMilestones from './AISuggestedMilestones.js'; import ScenarioEditModal from './ScenarioEditModal.js'; +import parseFloatOrZero from '../utils/ParseFloatorZero.js'; import './MilestoneTracker.css'; import './MilestoneTimeline.css'; -import { simulateFinancialProjection } from '../utils/FinancialProjectionService.js'; // Register Chart + annotation plugin ChartJS.register( @@ -39,100 +41,140 @@ ChartJS.register( const MilestoneTracker = ({ selectedCareer: initialCareer }) => { const location = useLocation(); - const navigate = useNavigate(); const apiURL = process.env.REACT_APP_API_URL; // -------------------------------------------------- // State // -------------------------------------------------- - const [selectedCareer, setSelectedCareer] = useState(initialCareer || null); - const [careerPathId, setCareerPathId] = useState(null); - const [existingCareerPaths, setExistingCareerPaths] = useState([]); - const [activeView, setActiveView] = useState('Career'); - + // User and Financial Profile Data + const [userProfile, setUserProfile] = useState(null); const [financialProfile, setFinancialProfile] = useState(null); + + // Career & Scenario Data + const [selectedCareer, setSelectedCareer] = useState(initialCareer || null); + const [careerProfileId, setCareerProfileId] = useState(null); + const [existingCareerProfiles, setExistingCareerProfiles] = useState([]); const [scenarioRow, setScenarioRow] = useState(null); const [collegeProfile, setCollegeProfile] = useState(null); - const [scenarioMilestones, setScenarioMilestones] = useState([]); // for annotation - + // Milestones & Simulation + const [scenarioMilestones, setScenarioMilestones] = useState([]); const [projectionData, setProjectionData] = useState([]); const [loanPayoffMonth, setLoanPayoffMonth] = useState(null); const [simulationYearsInput, setSimulationYearsInput] = useState('20'); const simulationYears = parseInt(simulationYearsInput, 10) || 20; - // Show/hide scenario edit modal + // Salary Data & Economic Projections + const [salaryData, setSalaryData] = useState(null); + const [economicProjections, setEconomicProjections] = useState(null); + + // UI Toggles const [showEditModal, setShowEditModal] = useState(false); const [pendingCareerForModal, setPendingCareerForModal] = useState(null); + const [showAISuggestions, setShowAISuggestions] = useState(false); + // If coming from location.state const { projectionData: initialProjectionData = [], loanPayoffMonth: initialLoanPayoffMonth = null } = location.state || {}; // -------------------------------------------------- - // 1) Fetch user’s scenario list + financialProfile + // 1) Fetch User Profile & Financial Profile // -------------------------------------------------- useEffect(() => { - const fetchCareerPaths = async () => { + const fetchUserProfile = async () => { + try { + const res = await authFetch('/api/user-profile'); // or wherever user profile is fetched + if (res.ok) { + const data = await res.json(); + setUserProfile(data); + } else { + console.error('Failed to fetch user profile:', res.status); + } + } catch (error) { + console.error('Error fetching user profile:', error); + } + }; + + const fetchFinancialProfile = async () => { + try { + const res = await authFetch(`${apiURL}/premium/financial-profile`); + if (res.ok) { + const data = await res.json(); + setFinancialProfile(data); + } else { + console.error('Failed to fetch financial profile:', res.status); + } + } catch (error) { + console.error('Error fetching financial profile:', error); + } + }; + + fetchUserProfile(); + fetchFinancialProfile(); + }, [apiURL]); + + const userLocation = userProfile?.area || ''; + const userSalary = financialProfile?.current_salary ?? 0; + + // -------------------------------------------------- + // 2) Fetch user’s Career Profiles => set initial scenario + // -------------------------------------------------- + useEffect(() => { + const fetchCareerProfiles = async () => { const res = await authFetch(`${apiURL}/premium/career-profile/all`); if (!res || !res.ok) return; const data = await res.json(); - setExistingCareerPaths(data.careerPaths); - + setExistingCareerProfiles(data.careerProfiles); + + // If there's a career in location.state, pick that const fromPopout = location.state?.selectedCareer; if (fromPopout) { setSelectedCareer(fromPopout); - setCareerPathId(fromPopout.career_path_id); + setCareerProfileId(fromPopout.career_profile_id); } else { - const storedCareerPathId = localStorage.getItem('lastSelectedCareerPathId'); - if (storedCareerPathId) { - const matchingCareer = data.careerPaths.find(p => p.id === storedCareerPathId); + // Else try localStorage + const storedCareerProfileId = localStorage.getItem('lastSelectedCareerProfileId'); + if (storedCareerProfileId) { + const matchingCareer = data.careerProfiles.find((p) => p.id === storedCareerProfileId); if (matchingCareer) { setSelectedCareer(matchingCareer); - setCareerPathId(storedCareerPathId); + setCareerProfileId(storedCareerProfileId); return; } } - - // fallback to latest scenario if no stored ID or not found + + // Fallback to the "latest" scenario const latest = await authFetch(`${apiURL}/premium/career-profile/latest`); if (latest && latest.ok) { const latestData = await latest.json(); if (latestData?.id) { setSelectedCareer(latestData); - setCareerPathId(latestData.id); + setCareerProfileId(latestData.id); } } } }; - - const fetchFinancialProfile = async () => { - const res = await authFetch(`${apiURL}/premium/financial-profile`); - if (res?.ok) { - const data = await res.json(); - setFinancialProfile(data); - } - }; - - fetchCareerPaths(); - fetchFinancialProfile(); + + fetchCareerProfiles(); }, [apiURL, location.state]); - // -------------------------------------------------- - // 2) When careerPathId changes => fetch scenarioRow + collegeProfile + // 3) Fetch scenarioRow + collegeProfile for chosen careerProfileId // -------------------------------------------------- useEffect(() => { - if (!careerPathId) { + if (!careerProfileId) { setScenarioRow(null); setCollegeProfile(null); setScenarioMilestones([]); return; } - async function fetchScenario() { - const scenRes = await authFetch(`${apiURL}/premium/career-profile/${careerPathId}`); + localStorage.setItem('lastSelectedCareerProfileId', careerProfileId); + + const fetchScenario = async () => { + const scenRes = await authFetch(`${apiURL}/premium/career-profile/${careerProfileId}`); if (scenRes.ok) { const data = await scenRes.json(); setScenarioRow(data); @@ -140,148 +182,278 @@ const MilestoneTracker = ({ selectedCareer: initialCareer }) => { console.error('Failed to fetch scenario row:', scenRes.status); setScenarioRow(null); } - } + }; - async function fetchCollege() { + const fetchCollege = async () => { const colRes = await authFetch( - `${apiURL}/premium/college-profile?careerPathId=${careerPathId}` + `${apiURL}/premium/college-profile?careerProfileId=${careerProfileId}` ); - if (!colRes?.ok) { + if (colRes.ok) { + const data = await colRes.json(); + setCollegeProfile(data); + } else { setCollegeProfile(null); - return; } - const data = await colRes.json(); - setCollegeProfile(data); - } + }; fetchScenario(); fetchCollege(); - }, [careerPathId, apiURL]); + }, [careerProfileId, apiURL]); + // -------------------------------------------------- + // 4) Fetch Salary Data for selectedCareer + userLocation + // -------------------------------------------------- useEffect(() => { - if (careerPathId) { - localStorage.setItem('lastSelectedCareerPathId', careerPathId); + if (!selectedCareer?.soc_code) { + setSalaryData(null); + return; } - }, [careerPathId]); - - // -------------------------------------------------- - // 3) Once scenarioRow + collegeProfile + financialProfile => run simulation - // + fetch milestones for annotation lines - // -------------------------------------------------- - useEffect(() => { - if (!financialProfile || !scenarioRow || !collegeProfile) return; - (async () => { + const areaParam = userLocation || 'U.S.'; + + const fetchSalaryData = async () => { try { - // fetch milestones for this scenario - const milRes = await authFetch( - `${apiURL}/premium/milestones?careerPathId=${careerPathId}` - ); - if (!milRes.ok) { - console.error('Failed to fetch milestones for scenario', careerPathId); + const queryParams = new URLSearchParams({ + socCode: selectedCareer.soc_code, + area: areaParam + }).toString(); + + const res = await fetch(`/api/salary?${queryParams}`); + if (!res.ok) { + console.error('Error fetching salary data:', res.status); + setSalaryData(null); return; } - const milestonesData = await milRes.json(); - const allMilestones = milestonesData.milestones || []; - setScenarioMilestones(allMilestones); - // fetch impacts for each milestone - const impactPromises = allMilestones.map((m) => - authFetch(`${apiURL}/premium/milestone-impacts?milestone_id=${m.id}`) - .then((r) => (r.ok ? r.json() : null)) - .then((data) => data?.impacts || []) - .catch((err) => { - console.warn('Error fetching impacts for milestone', m.id, err); - return []; - }) - ); - const impactsForEach = await Promise.all(impactPromises); - const milestonesWithImpacts = allMilestones.map((m, i) => ({ - ...m, - impacts: impactsForEach[i] || [] - })); - - // flatten - const allImpacts = milestonesWithImpacts.flatMap((m) => m.impacts); - - // Build mergedProfile - const mergedProfile = { - currentSalary: financialProfile.current_salary || 0, - monthlyExpenses: - scenarioRow.planned_monthly_expenses ?? - financialProfile.monthly_expenses ?? - 0, - monthlyDebtPayments: - scenarioRow.planned_monthly_debt_payments ?? - financialProfile.monthly_debt_payments ?? - 0, - retirementSavings: financialProfile.retirement_savings ?? 0, - emergencySavings: financialProfile.emergency_fund ?? 0, - monthlyRetirementContribution: - scenarioRow.planned_monthly_retirement_contribution ?? - financialProfile.retirement_contribution ?? - 0, - monthlyEmergencyContribution: - scenarioRow.planned_monthly_emergency_contribution ?? - financialProfile.emergency_contribution ?? - 0, - surplusEmergencyAllocation: - scenarioRow.planned_surplus_emergency_pct ?? - financialProfile.extra_cash_emergency_pct ?? - 50, - surplusRetirementAllocation: - scenarioRow.planned_surplus_retirement_pct ?? - financialProfile.extra_cash_retirement_pct ?? - 50, - additionalIncome: - scenarioRow.planned_additional_income ?? - financialProfile.additional_income ?? - 0, - - // college - studentLoanAmount: collegeProfile.existing_college_debt || 0, - interestRate: collegeProfile.interest_rate || 5, - loanTerm: collegeProfile.loan_term || 10, - loanDeferralUntilGraduation: !!collegeProfile.loan_deferral_until_graduation, - academicCalendar: collegeProfile.academic_calendar || 'monthly', - annualFinancialAid: collegeProfile.annual_financial_aid || 0, - calculatedTuition: collegeProfile.tuition || 0, - extraPayment: collegeProfile.extra_payment || 0, - inCollege: - collegeProfile.college_enrollment_status === 'currently_enrolled' || - collegeProfile.college_enrollment_status === 'prospective_student', - gradDate: collegeProfile.expected_graduation || null, - programType: collegeProfile.program_type || null, - creditHoursPerYear: collegeProfile.credit_hours_per_year || 0, - hoursCompleted: collegeProfile.hours_completed || 0, - programLength: collegeProfile.program_length || 0, - expectedSalary: - collegeProfile.expected_salary || financialProfile.current_salary || 0, - - // scenario horizon - startDate: new Date().toISOString(), - simulationYears, - - milestoneImpacts: allImpacts - }; - - const { projectionData: pData, loanPaidOffMonth: payoff } = - simulateFinancialProjection(mergedProfile); - - let cumu = mergedProfile.emergencySavings || 0; - const finalData = pData.map((mo) => { - cumu += mo.netSavings || 0; - return { ...mo, cumulativeNetSavings: cumu }; - }); - - setProjectionData(finalData); - setLoanPayoffMonth(payoff); + const data = await res.json(); + if (data.error) { + console.log('No salary data found for these params:', data.error); + } + setSalaryData(data); } catch (err) { - console.error('Error in scenario simulation:', err); + console.error('Exception fetching salary data:', err); + setSalaryData(null); } - })(); - }, [financialProfile, scenarioRow, collegeProfile, careerPathId, apiURL, simulationYears]); + }; + fetchSalaryData(); + }, [selectedCareer, userLocation]); + + // -------------------------------------------------- + // 5) (Optional) Fetch Economic Projections + // -------------------------------------------------- + useEffect(() => { + if (!selectedCareer?.career_name) { + setEconomicProjections(null); + return; + } + + const fetchEconomicProjections = async () => { + try { + const encodedCareer = encodeURIComponent(selectedCareer.career_name); + const res = await authFetch('/api/projections/:socCode'); + if (res.ok) { + const data = await res.json(); + setEconomicProjections(data); + } + } catch (err) { + console.error('Error fetching economic projections:', err); + setEconomicProjections(null); + } + }; + + fetchEconomicProjections(); + }, [selectedCareer, apiURL]); + + + // -------------------------------------------------- + // 6) Once we have scenario + financial + college => run simulation + // -------------------------------------------------- + useEffect(() => { + if (!financialProfile || !scenarioRow || !collegeProfile) return; + + (async () => { + try { + // 1) Fetch milestones for this scenario + const milRes = await authFetch(`${apiURL}/premium/milestones?careerProfileId=${careerProfileId}`); + if (!milRes.ok) { + console.error('Failed to fetch milestones for scenario', careerProfileId); + return; + } + const milestonesData = await milRes.json(); + const allMilestones = milestonesData.milestones || []; + setScenarioMilestones(allMilestones); + + // 2) Fetch impacts for each milestone + const impactPromises = allMilestones.map((m) => + authFetch(`${apiURL}/premium/milestone-impacts?milestone_id=${m.id}`) + .then((r) => (r.ok ? r.json() : null)) + .then((data) => data?.impacts || []) + .catch((err) => { + console.warn('Error fetching impacts for milestone', m.id, err); + return []; + }) + ); + const impactsForEach = await Promise.all(impactPromises); + + // Flatten all milestone impacts + const allImpacts = allMilestones.map((m, i) => ({ + ...m, + impacts: impactsForEach[i] || [], + })).flatMap((m) => m.impacts); + + /******************************************************* + * A) Parse numeric "financialProfile" fields + *******************************************************/ + const financialBase = { + currentSalary: parseFloatOrZero(financialProfile.current_salary, 0), + additionalIncome: parseFloatOrZero(financialProfile.additional_income, 0), + monthlyExpenses: parseFloatOrZero(financialProfile.monthly_expenses, 0), + monthlyDebtPayments: parseFloatOrZero(financialProfile.monthly_debt_payments, 0), + retirementSavings: parseFloatOrZero(financialProfile.retirement_savings, 0), + emergencySavings: parseFloatOrZero(financialProfile.emergency_fund, 0), + retirementContribution: parseFloatOrZero(financialProfile.retirement_contribution, 0), + emergencyContribution: parseFloatOrZero(financialProfile.emergency_contribution, 0), + extraCashEmergencyPct: parseFloatOrZero(financialProfile.extra_cash_emergency_pct, 50), + extraCashRetirementPct: parseFloatOrZero(financialProfile.extra_cash_retirement_pct, 50), + }; + + /******************************************************* + * B) Parse scenario overrides from "scenarioRow" + *******************************************************/ + const scenarioOverrides = { + monthlyExpenses: parseFloatOrZero( + scenarioRow.planned_monthly_expenses, + financialBase.monthlyExpenses + ), + monthlyDebtPayments: parseFloatOrZero( + scenarioRow.planned_monthly_debt_payments, + financialBase.monthlyDebtPayments + ), + monthlyRetirementContribution: parseFloatOrZero( + scenarioRow.planned_monthly_retirement_contribution, + financialBase.retirementContribution + ), + monthlyEmergencyContribution: parseFloatOrZero( + scenarioRow.planned_monthly_emergency_contribution, + financialBase.emergencyContribution + ), + surplusEmergencyAllocation: parseFloatOrZero( + scenarioRow.planned_surplus_emergency_pct, + financialBase.extraCashEmergencyPct + ), + surplusRetirementAllocation: parseFloatOrZero( + scenarioRow.planned_surplus_retirement_pct, + financialBase.extraCashRetirementPct + ), + additionalIncome: parseFloatOrZero( + scenarioRow.planned_additional_income, + financialBase.additionalIncome + ), + }; + + /******************************************************* + * C) Parse numeric "collegeProfile" fields + *******************************************************/ + const collegeData = { + studentLoanAmount: parseFloatOrZero(collegeProfile.existing_college_debt, 0), + interestRate: parseFloatOrZero(collegeProfile.interest_rate, 5), + loanTerm: parseFloatOrZero(collegeProfile.loan_term, 10), + loanDeferralUntilGraduation: !!collegeProfile.loan_deferral_until_graduation, + academicCalendar: collegeProfile.academic_calendar || 'monthly', + annualFinancialAid: parseFloatOrZero(collegeProfile.annual_financial_aid, 0), + calculatedTuition: parseFloatOrZero(collegeProfile.tuition, 0), + extraPayment: parseFloatOrZero(collegeProfile.extra_payment, 0), + inCollege: + collegeProfile.college_enrollment_status === 'currently_enrolled' || + collegeProfile.college_enrollment_status === 'prospective_student', + gradDate: collegeProfile.expected_graduation || null, + programType: collegeProfile.program_type || null, + creditHoursPerYear: parseFloatOrZero(collegeProfile.credit_hours_per_year, 0), + hoursCompleted: parseFloatOrZero(collegeProfile.hours_completed, 0), + programLength: parseFloatOrZero(collegeProfile.program_length, 0), + expectedSalary: + parseFloatOrZero(collegeProfile.expected_salary) || + parseFloatOrZero(financialProfile.current_salary, 0), + }; + + /******************************************************* + * D) Combine them into a single mergedProfile + *******************************************************/ + const mergedProfile = { + // Financial base + currentSalary: financialBase.currentSalary, + // scenario overrides (with scenario > financial precedence) + monthlyExpenses: scenarioOverrides.monthlyExpenses, + monthlyDebtPayments: scenarioOverrides.monthlyDebtPayments, + + // big items from financialProfile that had no scenario override + retirementSavings: financialBase.retirementSavings, + emergencySavings: financialBase.emergencySavings, + + // scenario overrides for monthly contributions + monthlyRetirementContribution: scenarioOverrides.monthlyRetirementContribution, + monthlyEmergencyContribution: scenarioOverrides.monthlyEmergencyContribution, + + // scenario overrides for surplus distribution + surplusEmergencyAllocation: scenarioOverrides.surplusEmergencyAllocation, + surplusRetirementAllocation: scenarioOverrides.surplusRetirementAllocation, + + // scenario override for additionalIncome + additionalIncome: scenarioOverrides.additionalIncome, + + // college fields + studentLoanAmount: collegeData.studentLoanAmount, + interestRate: collegeData.interestRate, + loanTerm: collegeData.loanTerm, + loanDeferralUntilGraduation: collegeData.loanDeferralUntilGraduation, + academicCalendar: collegeData.academicCalendar, + annualFinancialAid: collegeData.annualFinancialAid, + calculatedTuition: collegeData.calculatedTuition, + extraPayment: collegeData.extraPayment, + inCollege: collegeData.inCollege, + gradDate: collegeData.gradDate, + programType: collegeData.programType, + creditHoursPerYear: collegeData.creditHoursPerYear, + hoursCompleted: collegeData.hoursCompleted, + programLength: collegeData.programLength, + expectedSalary: collegeData.expectedSalary, + + // scenario horizon + milestone impacts + startDate: new Date().toISOString(), + simulationYears, + milestoneImpacts: allImpacts + }; + + // 3) Run the simulation + const { projectionData: pData, loanPaidOffMonth: payoff } = + simulateFinancialProjection(mergedProfile); + + // 4) Add cumulative net savings + let cumu = mergedProfile.emergencySavings || 0; + const finalData = pData.map((mo) => { + cumu += mo.netSavings || 0; + return { ...mo, cumulativeNetSavings: cumu }; + }); + + setProjectionData(finalData); + setLoanPayoffMonth(payoff); + } catch (err) { + console.error('Error in scenario simulation:', err); + } + })(); +}, [ + financialProfile, + scenarioRow, + collegeProfile, + careerProfileId, + apiURL, + simulationYears +]); + + // -------------------------------------------------- + // Handlers & Chart Setup + // -------------------------------------------------- const handleSimulationYearsChange = (e) => setSimulationYearsInput(e.target.value); const handleSimulationYearsBlur = () => { if (!simulationYearsInput.trim()) { @@ -300,7 +472,6 @@ const MilestoneTracker = ({ selectedCareer: initialCareer }) => { const month = String(d.getUTCMonth() + 1).padStart(2, '0'); const short = `${year}-${month}`; - // check if we have data for that month if (!projectionData.some((p) => p.month === short)) return; milestoneAnnotationLines[`milestone_${m.id}`] = { @@ -318,7 +489,7 @@ const MilestoneTracker = ({ selectedCareer: initialCareer }) => { }; }); - // If we also want a line for payoff: + // Loan payoff line const annotationConfig = {}; if (loanPayoffMonth) { annotationConfig.loanPaidOffLine = { @@ -342,38 +513,168 @@ const MilestoneTracker = ({ selectedCareer: initialCareer }) => { } const allAnnotations = { ...milestoneAnnotationLines, ...annotationConfig }; + // Salary Gauge + function getRelativePosition(userSal, p10, p90) { + if (!p10 || !p90) return 0; // avoid NaN + if (userSal < p10) return 0; + if (userSal > p90) return 1; + return (userSal - p10) / (p90 - p10); + } + + const SalaryGauge = ({ userSalary, percentileRow, prefix = 'regional' }) => { + if (!percentileRow) return null; + const p10 = percentileRow[`${prefix}_PCT10`]; + const p90 = percentileRow[`${prefix}_PCT90`]; + if (!p10 || !p90) return null; + + const fraction = getRelativePosition(userSalary, p10, p90) * 100; + + return ( +
+
+
+
+

+ You are at {Math.round(fraction)}% between the 10th and 90th percentiles ( + {prefix}). +

+
+ ); + }; + return (
{/* 1) Career dropdown */} { setSelectedCareer(selected); - setCareerPathId(selected?.id || null); + setCareerProfileId(selected?.id || null); }} - loading={!existingCareerPaths.length} + loading={!existingCareerProfiles.length} authFetch={authFetch} /> - {/* 2) MilestoneTimeline for Milestone & Task CRUD */} + {/* 2) Salary Data Display */} + {salaryData && ( +
+

Salary Overview

+ {/* Regional Salaries */} + {salaryData.regional && ( +
+

Regional Salaries (Area: {userLocation || 'U.S.'})

+

+ 10th percentile:{' '} + ${salaryData.regional.regional_PCT10?.toLocaleString() ?? 'N/A'} +

+

+ 25th percentile:{' '} + ${salaryData.regional.regional_PCT25?.toLocaleString() ?? 'N/A'} +

+

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

+

+ 75th percentile:{' '} + ${salaryData.regional.regional_PCT75?.toLocaleString() ?? 'N/A'} +

+

+ 90th percentile:{' '} + ${salaryData.regional.regional_PCT90?.toLocaleString() ?? 'N/A'} +

+ +
+ )} + {/* National Salaries */} + {salaryData.national && ( +
+

National Salaries

+

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

+

+ 25th percentile:{' '} + ${salaryData.national.national_PCT25?.toLocaleString() ?? 'N/A'} +

+

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

+

+ 75th percentile:{' '} + ${salaryData.national.national_PCT75?.toLocaleString() ?? 'N/A'} +

+

+ 90th percentile:{' '} + ${salaryData.national.national_PCT90?.toLocaleString() ?? 'N/A'} +

+ +
+ )} +

+ Your current salary: ${userSalary.toLocaleString()} +

+
+ )} + + {/* 3) Milestone Timeline */} {}} /> - {/* 3) AI-Suggested Milestones */} - + {/* 4) AI Suggestions Button */} + {!showAISuggestions && ( + + )} - {/* 4) The main chart with annotation lines */} + {/* 5) AI-Suggested Milestones */} + {showAISuggestions && ( + + )} + + {/* 6) Financial Projection Chart */} {projectionData.length > 0 && (

Financial Projection

@@ -440,7 +741,7 @@ const MilestoneTracker = ({ selectedCareer: initialCareer }) => {
)} - {/* 5) Simulation length + "Edit" Button => open ScenarioEditModal */} + {/* 7) Simulation length + "Edit" => open ScenarioEditModal */}
{
- {/* 6) Career Search, scenario edit modal, etc. */} + {/* 8) Economic Projections Section */} + {economicProjections && ( +
+

Economic Projections

+

+ Growth Outlook: {economicProjections.growthOutlook || 'N/A'} +

+

+ AI Automation Risk: {economicProjections.aiRisk || 'N/A'} +

+ {economicProjections.chatGPTAnalysis && ( +
+

ChatGPT Analysis:

+

{economicProjections.chatGPTAnalysis}

+
+ )} +
+ )} + + {/* 9) Career Search & Potential new scenario creation */} { setPendingCareerForModal(careerObj.title); }} /> - {pendingCareerForModal && ( )} + {/* 10) Scenario Edit Modal */} { setShowEditModal(false); - // optionally reload to see scenario changes window.location.reload(); }} scenario={scenarioRow} diff --git a/src/components/MultiScenarioView.js b/src/components/MultiScenarioView.js index 39b2911..d997eda 100644 --- a/src/components/MultiScenarioView.js +++ b/src/components/MultiScenarioView.js @@ -9,7 +9,7 @@ import { Button } from './ui/button.js'; * MultiScenarioView * ----------------- * - Loads the user’s global financialProfile - * - Loads all scenarios from `career_paths` + * - Loads all scenarios from `career_profiles` * - Renders a for each scenario * - Handles "Add Scenario", "Clone Scenario" (including college_profile), "Remove Scenario" */ @@ -20,7 +20,7 @@ export default function MultiScenarioView() { // The user’s single overall financial profile const [financialProfile, setFinancialProfile] = useState(null); - // The list of scenario "headers" (rows from career_paths) + // The list of scenario "headers" (rows from career_profiles) const [scenarios, setScenarios] = useState([]); useEffect(() => { @@ -46,7 +46,7 @@ export default function MultiScenarioView() { const scenData = await scenRes.json(); setFinancialProfile(finData); - setScenarios(scenData.careerPaths || []); + setScenarios(scenData.careerProfiles || []); } catch (err) { console.error('MultiScenarioView load error:', err); setError(err.message || 'Failed to load multi-scenarios'); @@ -122,7 +122,7 @@ export default function MultiScenarioView() { // parse the newly created scenario_id const newScenarioData = await res.json(); - const newScenarioId = newScenarioData.career_path_id; + const newScenarioId = newScenarioData.career_profile_id; // 2) Clone the old scenario’s college_profile => new scenario await cloneCollegeProfile(oldScenario.id, newScenarioId); @@ -141,7 +141,7 @@ export default function MultiScenarioView() { try { // fetch old scenario’s college_profile const getRes = await authFetch( - `/api/premium/college-profile?careerPathId=${oldScenarioId}` + `/api/premium/college-profile?careerProfileId=${oldScenarioId}` ); if (!getRes.ok) { console.warn( @@ -162,7 +162,7 @@ export default function MultiScenarioView() { // build new payload const clonePayload = { - career_path_id: newScenarioId, + career_profile_id: newScenarioId, selected_school: oldCollegeData.selected_school, selected_program: oldCollegeData.selected_program, diff --git a/src/components/Paywall.js b/src/components/Paywall.js index 4810edc..a5f9f87 100644 --- a/src/components/Paywall.js +++ b/src/components/Paywall.js @@ -24,7 +24,7 @@ const Paywall = () => { }); if (response.ok) { - navigate('/PremiumOnboarding', { state: { selectedCareer } }); + navigate('/premium-onboarding', { state: { selectedCareer } }); } else if (response.status === 401) { navigate('/GettingStarted', { state: { selectedCareer } }); } else { diff --git a/src/components/PopoutPanel.js b/src/components/PopoutPanel.js index 6a56135..d48656f 100644 --- a/src/components/PopoutPanel.js +++ b/src/components/PopoutPanel.js @@ -116,7 +116,7 @@ function PopoutPanel({ } try { - // 1) Fetch existing career profiles (a.k.a. "careerPaths") + // 1) Fetch existing career profiles (a.k.a. "careerProfiles") const allPathsResponse = await fetch( `${process.env.REACT_APP_API_URL}/premium/career-profile/all`, { @@ -132,11 +132,11 @@ function PopoutPanel({ throw new Error(`HTTP error ${allPathsResponse.status}`); } - // The server returns { careerPaths: [...] } - const { careerPaths } = await allPathsResponse.json(); + // The server returns { careerProfiles: [...] } + const { careerProfiles } = await allPathsResponse.json(); // 2) Check if there's already a career path with the same name - const match = careerPaths.find((cp) => cp.career_name === data.title); + const match = careerProfiles.find((cp) => cp.career_name === data.title); if (match) { // If a path already exists for this career, confirm with the user @@ -149,7 +149,7 @@ function PopoutPanel({ navigate("/paywall", { state: { selectedCareer: { - career_path_id: match.id, // 'id' is the primary key from the DB + career_profile_id: match.id, // 'id' is the primary key from the DB career_name: data.title, }, }, @@ -179,15 +179,15 @@ function PopoutPanel({ throw new Error("Failed to create new career path."); } - // The server returns something like { message: 'Career profile upserted.', career_path_id: 'xxx-xxx' } + // The server returns something like { message: 'Career profile upserted.', career_profile_id: 'xxx-xxx' } const result = await newResponse.json(); - const newlyCreatedId = result?.career_path_id; + const newlyCreatedId = result?.career_profile_id; - // 4) Navigate to /paywall, passing the newly created career_path_id + // 4) Navigate to /paywall, passing the newly created career_profile_id navigate("/paywall", { state: { selectedCareer: { - career_path_id: newlyCreatedId, + career_profile_id: newlyCreatedId, career_name: data.title, }, }, diff --git a/src/components/PremiumOnboarding/CareerOnboarding.js b/src/components/PremiumOnboarding/CareerOnboarding.js index 29f6b65..6e53d12 100644 --- a/src/components/PremiumOnboarding/CareerOnboarding.js +++ b/src/components/PremiumOnboarding/CareerOnboarding.js @@ -4,104 +4,60 @@ import axios from 'axios'; import { Input } from '../ui/input.js'; // Ensure path matches your structure import authFetch from '../../utils/authFetch.js'; +// 1) Import your CareerSearch component +import CareerSearch from '../CareerSearch.js'; // adjust path as necessary + const apiURL = process.env.REACT_APP_API_URL; const CareerOnboarding = ({ nextStep, prevStep, data, setData }) => { - const [userId, setUserId] = useState(null); + // We store local state for “are you working,” “selectedCareer,” etc. const [currentlyWorking, setCurrentlyWorking] = useState(''); const [selectedCareer, setSelectedCareer] = useState(''); - const [careerPathId, setCareerPathId] = useState(null); const [collegeEnrollmentStatus, setCollegeEnrollmentStatus] = useState(''); - const [careers, setCareers] = useState([]); - const [searchInput, setSearchInput] = useState(''); - + // On mount, if data already has these fields, set them useEffect(() => { - const storedUserId = localStorage.getItem('userId'); - if (storedUserId) { - setUserId(storedUserId); - } else { - console.error('User ID not found in localStorage'); - } - }, []); - - useEffect(() => { - const fetchCareerTitles = async () => { - try { - const response = await fetch('/career_clusters.json'); - const data = await response.json(); - - const careerTitlesSet = new Set(); - - const clusters = Object.keys(data); - for (let i = 0; i < clusters.length; i++) { - const cluster = clusters[i]; - const subdivisions = Object.keys(data[cluster]); - - for (let j = 0; j < subdivisions.length; j++) { - const subdivision = subdivisions[j]; - const careersArray = data[cluster][subdivision]; - - for (let k = 0; k < careersArray.length; k++) { - const careerObj = careersArray[k]; - if (careerObj.title) { - careerTitlesSet.add(careerObj.title); - } - } - } - } - - setCareers([...careerTitlesSet]); - - } catch (error) { - console.error("Error fetching or processing career_clusters.json:", error); - } - }; - - fetchCareerTitles(); - }, []); - - useEffect(() => { - if (careers.includes(searchInput)) { - setSelectedCareer(searchInput); - setData(prev => ({ ...prev, career_name: searchInput })); - } - }, [searchInput, careers, setData]); + if (data.currently_working) setCurrentlyWorking(data.currently_working); + if (data.career_name) setSelectedCareer(data.career_name); + if (data.college_enrollment_status) setCollegeEnrollmentStatus(data.college_enrollment_status); + }, [data]); + // Called whenever other change const handleChange = (e) => { setData(prev => ({ ...prev, [e.target.name]: e.target.value })); }; - const handleCareerInputChange = (e) => { - const inputValue = e.target.value; - setSearchInput(inputValue); - - if (careers.includes(inputValue)) { - setSelectedCareer(inputValue); - setData(prev => ({ ...prev, career_name: inputValue })); - } + // Called when user picks a career from CareerSearch and confirms it + const handleCareerSelected = (careerObj) => { + // e.g. { title, soc_code, cip_code, ... } + setSelectedCareer(careerObj.title); + setData(prev => ({ + ...prev, + career_name: careerObj.title, + soc_code: careerObj.soc_code || '' // store SOC if needed + })); }; const handleSubmit = () => { if (!selectedCareer || !currentlyWorking || !collegeEnrollmentStatus) { - alert("Please complete all required fields before continuing."); + alert('Please complete all required fields before continuing.'); return; } - const isInCollege = ( + const isInCollege = collegeEnrollmentStatus === 'currently_enrolled' || - collegeEnrollmentStatus === 'prospective_student' - ); + collegeEnrollmentStatus === 'prospective_student'; + // Merge local state into parent data setData(prevData => ({ ...prevData, career_name: selectedCareer, college_enrollment_status: collegeEnrollmentStatus, currently_working: currentlyWorking, inCollege: isInCollege, + // fallback defaults, or use user-provided status: prevData.status || 'planned', - start_date: prevData.start_date || new Date().toISOString(), - projected_end_date: prevData.projected_end_date || null, - user_id: userId + start_date: prevData.start_date || new Date().toISOString().slice(0, 10), + projected_end_date: prevData.projected_end_date || null })); nextStep(); @@ -117,7 +73,10 @@ const CareerOnboarding = ({ nextStep, prevStep, data, setData }) => {
+ {/* 2) Replace old local “Search for Career” with */}

Search for Career

- - - {careers.map((career, index) => ( - +
{selectedCareer && ( @@ -157,9 +106,9 @@ const CareerOnboarding = ({ nextStep, prevStep, data, setData }) => { className="w-full border rounded p-2" > - - - + + +
@@ -191,13 +140,16 @@ const CareerOnboarding = ({ nextStep, prevStep, data, setData }) => {
diff --git a/src/components/PremiumOnboarding/CollegeOnboarding.js b/src/components/PremiumOnboarding/CollegeOnboarding.js index 51c6602..b8b8198 100644 --- a/src/components/PremiumOnboarding/CollegeOnboarding.js +++ b/src/components/PremiumOnboarding/CollegeOnboarding.js @@ -2,7 +2,7 @@ import React, { useState, useEffect } from 'react'; import authFetch from '../../utils/authFetch.js'; -function CollegeOnboarding({ nextStep, prevStep, data, setData, careerPathId }) { +function CollegeOnboarding({ nextStep, prevStep, data, setData, careerProfileId }) { // CIP / iPEDS local states (purely for CIP data and suggestions) const [schoolData, setSchoolData] = useState([]); const [icTuitionData, setIcTuitionData] = useState([]); diff --git a/src/components/PremiumOnboarding/OnboardingContainer.js b/src/components/PremiumOnboarding/OnboardingContainer.js index 244f816..0214685 100644 --- a/src/components/PremiumOnboarding/OnboardingContainer.js +++ b/src/components/PremiumOnboarding/OnboardingContainer.js @@ -1,13 +1,14 @@ // OnboardingContainer.js import React, { useState, useEffect } from 'react'; +import { useNavigate } from 'react-router-dom'; import PremiumWelcome from './PremiumWelcome.js'; import CareerOnboarding from './CareerOnboarding.js'; import FinancialOnboarding from './FinancialOnboarding.js'; import CollegeOnboarding from './CollegeOnboarding.js'; -import authFetch from '../../utils/authFetch.js'; -import { useNavigate } from 'react-router-dom'; import ReviewPage from './ReviewPage.js'; +import authFetch from '../../utils/authFetch.js'; + const OnboardingContainer = () => { console.log('OnboardingContainer MOUNT'); @@ -15,30 +16,31 @@ const OnboardingContainer = () => { const [careerData, setCareerData] = useState({}); const [financialData, setFinancialData] = useState({}); const [collegeData, setCollegeData] = useState({}); - const navigate = useNavigate(); const nextStep = () => setStep(step + 1); const prevStep = () => setStep(step - 1); - console.log("Final collegeData in OnboardingContainer:", collegeData); + console.log('Final collegeData in OnboardingContainer:', collegeData); - // Now we do the final “all done” submission when the user finishes the last step + // Final “all done” submission when user finishes the last step const handleFinalSubmit = async () => { try { - // Build a scenarioPayload that includes the optional planned_* fields. - // We parseFloat them to avoid sending strings, and default to 0 if empty. + // Build a scenarioPayload that includes optional planned_* fields: const scenarioPayload = { ...careerData, planned_monthly_expenses: parseFloat(careerData.planned_monthly_expenses) || 0, planned_monthly_debt_payments: parseFloat(careerData.planned_monthly_debt_payments) || 0, - planned_monthly_retirement_contribution: parseFloat(careerData.planned_monthly_retirement_contribution) || 0, - planned_monthly_emergency_contribution: parseFloat(careerData.planned_monthly_emergency_contribution) || 0, + planned_monthly_retirement_contribution: + parseFloat(careerData.planned_monthly_retirement_contribution) || 0, + planned_monthly_emergency_contribution: + parseFloat(careerData.planned_monthly_emergency_contribution) || 0, planned_surplus_emergency_pct: parseFloat(careerData.planned_surplus_emergency_pct) || 0, - planned_surplus_retirement_pct: parseFloat(careerData.planned_surplus_retirement_pct) || 0, - planned_additional_income: parseFloat(careerData.planned_additional_income) || 0 + planned_surplus_retirement_pct: + parseFloat(careerData.planned_surplus_retirement_pct) || 0, + planned_additional_income: parseFloat(careerData.planned_additional_income) || 0, }; - + // 1) POST career-profile (scenario) const careerRes = await authFetch('/api/premium/career-profile', { method: 'POST', @@ -47,9 +49,11 @@ const OnboardingContainer = () => { }); if (!careerRes.ok) throw new Error('Failed to save career profile'); const careerJson = await careerRes.json(); - const { career_path_id } = careerJson; - if (!career_path_id) throw new Error('No career_path_id returned by server'); - + const { career_profile_id } = careerJson; // <-- Renamed from career_profile_id + if (!career_profile_id) { + throw new Error('No career_profile_id returned by server'); + } + // 2) POST financial-profile const financialRes = await authFetch('/api/premium/financial-profile', { method: 'POST', @@ -57,12 +61,12 @@ const OnboardingContainer = () => { body: JSON.stringify(financialData), }); if (!financialRes.ok) throw new Error('Failed to save financial profile'); - - // 3) POST college-profile (include career_path_id) + + // 3) POST college-profile (now uses career_profile_id) const mergedCollege = { ...collegeData, - career_path_id, - college_enrollment_status: careerData.college_enrollment_status + career_profile_id, + college_enrollment_status: careerData.college_enrollment_status, }; const collegeRes = await authFetch('/api/premium/college-profile', { method: 'POST', @@ -70,15 +74,14 @@ const OnboardingContainer = () => { body: JSON.stringify(mergedCollege), }); if (!collegeRes.ok) throw new Error('Failed to save college profile'); - - // Done => navigate away + + // All done → navigate away navigate('/milestone-tracker'); } catch (err) { console.error(err); // (optionally show error to user) } }; - const onboardingSteps = [ , @@ -104,12 +107,12 @@ const OnboardingContainer = () => { nextStep={nextStep} data={{ ...collegeData, - // ensure we keep the enrollment status from career if that matters: - college_enrollment_status: careerData.college_enrollment_status + // keep enrollment status from careerData if relevant: + college_enrollment_status: careerData.college_enrollment_status, }} setData={setCollegeData} />, - // Add a final "Review & Submit" step or just automatically call handleFinalSubmit on step 4 + = 100 ? 'completed' : 'planned', new_salary: newMilestone.newSalary @@ -791,7 +791,7 @@ export default function ScenarioContainer({ career={ localScenario.career_name || localScenario.scenario_title || '' } - careerPathId={localScenario.id} + careerProfileId={localScenario.id} authFetch={authFetch} activeView="Financial" projectionData={projectionData} diff --git a/src/components/ScenarioEditModal.js b/src/components/ScenarioEditModal.js index 004e188..b76c788 100644 --- a/src/components/ScenarioEditModal.js +++ b/src/components/ScenarioEditModal.js @@ -533,11 +533,11 @@ export default function ScenarioEditModal({ throw new Error(`Scenario upsert failed: ${msg}`); } const scenData = await scenRes.json(); - const updatedScenarioId = scenData.career_path_id; + const updatedScenarioId = scenData.career_profile_id; // 2) Build college payload const collegePayload = { - career_path_id: updatedScenarioId, + career_profile_id: updatedScenarioId, college_enrollment_status: finalCollegeStatus, is_in_state: formData.is_in_state ? 1 : 0, is_in_district: formData.is_in_district ? 1 : 0, @@ -621,7 +621,7 @@ export default function ScenarioEditModal({ const [scenResp2, colResp2, finResp] = await Promise.all([ authFetch(`/api/premium/career-profile/${updatedScenarioId}`), authFetch( - `/api/premium/college-profile?careerPathId=${updatedScenarioId}` + `/api/premium/college-profile?careerProfileId=${updatedScenarioId}` ), authFetch(`/api/premium/financial-profile`) ]); diff --git a/src/components/ScenarioEditWizard.js b/src/components/ScenarioEditWizard.js index 668ebdb..26ff516 100644 --- a/src/components/ScenarioEditWizard.js +++ b/src/components/ScenarioEditWizard.js @@ -32,7 +32,7 @@ export default function ScenarioEditWizard({ const [scenRes, finRes, colRes] = await Promise.all([ authFetch(`/api/premium/career-profile/${scenarioId}`), authFetch(`/api/premium/financial-profile`), - authFetch(`/api/premium/college-profile?careerPathId=${scenarioId}`) + authFetch(`/api/premium/college-profile?careerProfileId=${scenarioId}`) ]); if (!scenRes.ok || !finRes.ok || !colRes.ok) { throw new Error('Failed fetching existing scenario or financial or college.'); @@ -59,7 +59,7 @@ export default function ScenarioEditWizard({ planned_surplus_emergency_pct: scenData.planned_surplus_emergency_pct, planned_surplus_retirement_pct: scenData.planned_surplus_retirement_pct, planned_additional_income: scenData.planned_additional_income, - user_id: scenData.user_id, + id: scenData.id, // etc... }); diff --git a/src/components/SignIn.js b/src/components/SignIn.js index 6392584..9bf5e25 100644 --- a/src/components/SignIn.js +++ b/src/components/SignIn.js @@ -33,11 +33,11 @@ function SignIn({ setIsAuthenticated, setUser }) { const data = await response.json(); // Destructure user, which includes is_premium, etc. - const { token, userId, user } = data; + const { token, id, user } = data; - // Store token & userId in localStorage + // Store token & id in localStorage localStorage.setItem('token', token); - localStorage.setItem('userId', userId); + localStorage.setItem('id', id); // Mark user as authenticated setIsAuthenticated(true); diff --git a/src/utils/FinancialProjectionService.js b/src/utils/FinancialProjectionService.js index a7020f2..01f7bc7 100644 --- a/src/utils/FinancialProjectionService.js +++ b/src/utils/FinancialProjectionService.js @@ -417,6 +417,7 @@ export function simulateFinancialProjection(userProfile) { netMonthlyIncome: +netMonthlyIncome.toFixed(2), totalExpenses: +actualExpensesPaid.toFixed(2), + effectiveRetirementContribution: +effectiveRetirementContribution.toFixed(2), effectiveEmergencyContribution: +effectiveEmergencyContribution.toFixed(2), @@ -442,6 +443,7 @@ export function simulateFinancialProjection(userProfile) { wasInDeferral = (stillInCollege && loanDeferralUntilGraduation); } + // final loanPaidOffMonth if never set if (loanBalance <= 0 && !loanPaidOffMonth) { loanPaidOffMonth = scenarioStartClamped.clone().add(maxMonths, 'months').format('YYYY-MM'); diff --git a/src/utils/ParseFloatorZero.js b/src/utils/ParseFloatorZero.js new file mode 100644 index 0000000..6d23c44 --- /dev/null +++ b/src/utils/ParseFloatorZero.js @@ -0,0 +1,5 @@ +// parseFloatOrZero.js +export default function parseFloatOrZero(val, defaultVal = 0) { + const num = parseFloat(val); + return isNaN(num) ? defaultVal : num; +} diff --git a/src/utils/buildMilestonePromptData.js b/src/utils/buildMilestonePromptData.js index 2313594..ebf5f90 100644 --- a/src/utils/buildMilestonePromptData.js +++ b/src/utils/buildMilestonePromptData.js @@ -14,7 +14,7 @@ export function buildMilestonePromptData({ isCollegeMode }) { return { - careerPath: { + careerProfile: { name: selectedCareer?.career_name, socCode: selectedCareer?.soc_code, cluster: careerCluster, diff --git a/user_profile.db b/user_profile.db index f4a9d32..64f498f 100644 Binary files a/user_profile.db and b/user_profile.db differ