diff --git a/backend/server3.js b/backend/server3.js index 3efd846..a21e519 100644 --- a/backend/server3.js +++ b/backend/server3.js @@ -9,7 +9,6 @@ import jwt from 'jsonwebtoken'; import { v4 as uuidv4 } from 'uuid'; import path from 'path'; import { fileURLToPath } from 'url'; -// If you still need the projection logic somewhere else import { simulateFinancialProjection } from '../src/utils/FinancialProjectionService.js'; const __filename = fileURLToPath(import.meta.url); @@ -56,7 +55,6 @@ const authenticatePremiumUser = (req, res, next) => { /* ------------------------------------------------------------------ CAREER PROFILE ENDPOINTS - (Renamed from planned-path to career-profile) ------------------------------------------------------------------ */ // GET the latest selected career profile @@ -114,8 +112,7 @@ app.get('/api/premium/career-profile/:careerPathId', authenticatePremiumUser, as } }); -// POST a new career profile - +// POST a new career profile (upsert) app.post('/api/premium/career-profile', authenticatePremiumUser, async (req, res) => { const { scenario_title, @@ -143,7 +140,6 @@ app.post('/api/premium/career-profile', authenticatePremiumUser, async (req, res const newCareerPathId = uuidv4(); const now = new Date().toISOString(); - // Upsert via ON CONFLICT(user_id, career_name) await db.run(` INSERT INTO career_paths ( id, @@ -190,33 +186,30 @@ app.post('/api/premium/career-profile', authenticatePremiumUser, async (req, res updated_at = ? `, [ - // 18 items for the INSERT columns - newCareerPathId, // id - req.userId, // user_id - scenario_title || null, // scenario_title - career_name, // career_name - status || 'planned', // status - start_date || now, // start_date - projected_end_date || null, // projected_end_date - college_enrollment_status || null, // college_enrollment_status - currently_working || null, // currently_working + newCareerPathId, + req.userId, + scenario_title || null, + career_name, + status || 'planned', + start_date || now, + projected_end_date || null, + college_enrollment_status || null, + currently_working || null, - planned_monthly_expenses ?? null, // planned_monthly_expenses - planned_monthly_debt_payments ?? null, // planned_monthly_debt_payments + 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, // created_at - now, // updated_at + now, + now, - // Then 1 more param for "updated_at = ?" in the conflict update now ]); - // Optionally fetch the row's ID or entire row after upsert const result = await db.get(` SELECT id FROM career_paths @@ -234,93 +227,67 @@ app.post('/api/premium/career-profile', authenticatePremiumUser, async (req, res } }); - -// Delete a career path (scenario) by ID +// 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; try { // 1) Confirm that this career_path belongs to the user - const existing = await db.get( - ` + const existing = await db.get(` SELECT id FROM career_paths WHERE id = ? AND user_id = ? - `, - [careerPathId, req.userId] - ); + `, [careerPathId, req.userId]); if (!existing) { return res.status(404).json({ error: 'Career path not found or not yours.' }); } - // 2) Optionally delete the college_profile for this scenario - // (If you always keep 1-to-1 relationship: careerPathId => college_profile) - await db.run( - ` + // 2) Delete the college_profile for this scenario + await db.run(` DELETE FROM college_profiles WHERE user_id = ? AND career_path_id = ? - `, - [req.userId, careerPathId] - ); + `, [req.userId, careerPathId]); - // 3) Optionally delete scenario’s milestones - // (and any associated tasks, impacts, etc.) - // If you store tasks in tasks table, and impacts in milestone_impacts table: - - // First find scenario milestones - const scenarioMilestones = await db.all( - ` + // 3) Delete scenario’s milestones (and tasks/impacts) + const scenarioMilestones = await db.all(` SELECT id FROM milestones WHERE user_id = ? AND career_path_id = ? - `, - [req.userId, careerPathId] - ); + `, [req.userId, careerPathId]); const milestoneIds = scenarioMilestones.map((m) => m.id); if (milestoneIds.length > 0) { - // Delete tasks for these milestones const placeholders = milestoneIds.map(() => '?').join(','); - await db.run( - ` + + // Delete tasks + await db.run(` DELETE FROM tasks WHERE milestone_id IN (${placeholders}) - `, - milestoneIds - ); + `, milestoneIds); - // Delete impacts for these milestones - await db.run( - ` + // Delete impacts + await db.run(` DELETE FROM milestone_impacts WHERE milestone_id IN (${placeholders}) - `, - milestoneIds - ); + `, milestoneIds); // Finally delete the milestones themselves - await db.run( - ` + await db.run(` DELETE FROM milestones WHERE id IN (${placeholders}) - `, - milestoneIds - ); + `, milestoneIds); } - // 4) Finally delete the career_path row - await db.run( - ` + // 4) Delete the career_path row + await db.run(` DELETE FROM career_paths WHERE user_id = ? AND id = ? - `, - [req.userId, careerPathId] - ); + `, [req.userId, careerPathId]); res.json({ message: 'Career path and related data successfully deleted.' }); } catch (error) { @@ -329,7 +296,6 @@ app.delete('/api/premium/career-profile/:careerPathId', authenticatePremiumUser, } }); - /* ------------------------------------------------------------------ Milestone ENDPOINTS ------------------------------------------------------------------ */ @@ -355,7 +321,6 @@ app.post('/api/premium/milestone', authenticatePremiumUser, async (req, res) => is_universal } = m; - // Validate some required fields if (!milestone_type || !title || !date || !career_path_id) { return res.status(400).json({ error: 'One or more milestones missing required fields', @@ -393,7 +358,7 @@ app.post('/api/premium/milestone', authenticatePremiumUser, async (req, res) => progress || 0, status || 'planned', new_salary || null, - is_universal ? 1 : 0, // store 1 or 0 + is_universal ? 1 : 0, now, now ]); @@ -413,7 +378,6 @@ app.post('/api/premium/milestone', authenticatePremiumUser, async (req, res) => tasks: [] }); } - // Return array of created milestones return res.status(201).json(createdMilestones); } @@ -472,7 +436,6 @@ app.post('/api/premium/milestone', authenticatePremiumUser, async (req, res) => now ]); - // Return the newly created single milestone object const newMilestone = { id, user_id: req.userId, @@ -525,7 +488,6 @@ app.put('/api/premium/milestones/:milestoneId', authenticatePremiumUser, async ( const now = new Date().toISOString(); - // Merge fields with existing if not provided const finalMilestoneType = milestone_type || existing.milestone_type; const finalTitle = title || existing.title; const finalDesc = description || existing.description; @@ -537,7 +499,6 @@ app.put('/api/premium/milestones/:milestoneId', authenticatePremiumUser, async ( const finalIsUniversal = is_universal != null ? (is_universal ? 1 : 0) : existing.is_universal; - // Update row await db.run(` UPDATE milestones SET @@ -568,14 +529,12 @@ app.put('/api/premium/milestones/:milestoneId', authenticatePremiumUser, async ( req.userId ]); - // Return the updated record with tasks + // Return the updated milestone with tasks const updatedMilestoneRow = await db.get(` SELECT * FROM milestones WHERE id = ? `, [milestoneId]); - - // Fetch tasks for this milestone const tasks = await db.all(` SELECT * FROM tasks @@ -597,11 +556,9 @@ app.put('/api/premium/milestones/:milestoneId', authenticatePremiumUser, async ( // GET all milestones for a given careerPathId app.get('/api/premium/milestones', authenticatePremiumUser, async (req, res) => { const { careerPathId } = req.query; - try { - // if user wants universal=1 only, e.g. careerPathId=universal + // universal milestones if (careerPathId === 'universal') { - // For example, fetch all is_universal=1 for the user: const universalRows = await db.all(` SELECT * FROM milestones @@ -609,7 +566,6 @@ app.get('/api/premium/milestones', authenticatePremiumUser, async (req, res) => AND is_universal = 1 `, [req.userId]); - // attach tasks if needed const milestoneIds = universalRows.map(m => m.id); let tasksByMilestone = {}; if (milestoneIds.length > 0) { @@ -624,6 +580,7 @@ app.get('/api/premium/milestones', authenticatePremiumUser, async (req, res) => return acc; }, {}); } + const uniMils = universalRows.map(m => ({ ...m, tasks: tasksByMilestone[m.id] || [] @@ -675,19 +632,16 @@ app.post('/api/premium/milestone/copy', authenticatePremiumUser, async (req, res return res.status(400).json({ error: 'Missing milestoneId or scenarioIds.' }); } - // 1) Fetch the original const original = await db.get(` SELECT * FROM milestones WHERE id = ? AND user_id = ? `, [milestoneId, req.userId]); - if (!original) { return res.status(404).json({ error: 'Milestone not found or not owned by user.' }); } - // 2) Force is_universal=1 on the original if (original.is_universal !== 1) { await db.run(` UPDATE milestones @@ -695,12 +649,9 @@ app.post('/api/premium/milestone/copy', authenticatePremiumUser, async (req, res WHERE id = ? AND user_id = ? `, [ milestoneId, req.userId ]); - - // Also refresh "original" object if you want original.is_universal = 1; } - // 3) If no origin_milestone_id, set it let originId = original.origin_milestone_id || original.id; if (!original.origin_milestone_id) { await db.run(` @@ -711,7 +662,6 @@ app.post('/api/premium/milestone/copy', authenticatePremiumUser, async (req, res `, [ originId, milestoneId, req.userId ]); } - // 4) fetch tasks & impacts const tasks = await db.all(` SELECT * FROM tasks @@ -728,13 +678,9 @@ app.post('/api/premium/milestone/copy', authenticatePremiumUser, async (req, res const copiesCreated = []; for (let scenarioId of scenarioIds) { - if (scenarioId === original.career_path_id) { - continue; - } + if (scenarioId === original.career_path_id) continue; // skip if same scenario const newMilestoneId = uuidv4(); - - // Always set isUniversal=1 on copies const isUniversal = 1; await db.run(` @@ -814,7 +760,8 @@ app.post('/api/premium/milestone/copy', authenticatePremiumUser, async (req, res end_date, created_at, updated_at - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) + ) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) `, [ newImpactId, newMilestoneId, @@ -859,22 +806,40 @@ app.delete('/api/premium/milestones/:milestoneId/all', authenticatePremiumUser, 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 originId = existing.origin_milestone_id || existing.id; - // 2) Delete all copies referencing that origin - await db.run(` - DELETE FROM milestones + // Find all those milestone IDs + const allMilsToDelete = await db.all(` + SELECT id + FROM milestones WHERE user_id = ? - AND origin_milestone_id = ? - `, [req.userId, originId]); + AND (id = ? OR origin_milestone_id = ?) + `, [req.userId, originId, originId]); - // Also delete the original if it doesn't store itself in origin_milestone_id - await db.run(` - DELETE FROM milestones - WHERE user_id = ? - AND id = ? - AND origin_milestone_id IS NULL - `, [req.userId, originId]); + const milIDs = allMilsToDelete.map(m => m.id); + if (milIDs.length > 0) { + const placeholders = milIDs.map(() => '?').join(','); + + // Delete tasks for those milestones + await db.run(` + DELETE FROM tasks + WHERE milestone_id IN (${placeholders}) + `, milIDs); + + // Delete impacts for those milestones + await db.run(` + DELETE FROM milestone_impacts + WHERE milestone_id IN (${placeholders}) + `, milIDs); + + // Finally remove the milestones themselves + await db.run(` + DELETE FROM milestones + WHERE user_id = ? + AND (id = ? OR origin_milestone_id = ?) + `, [req.userId, originId, originId]); + } res.json({ message: 'Deleted from all scenarios' }); } catch (err) { @@ -883,7 +848,7 @@ app.delete('/api/premium/milestones/:milestoneId/all', authenticatePremiumUser, } }); -// DELETE milestone from this scenario only +// DELETE milestone from THIS scenario only app.delete('/api/premium/milestones/:milestoneId', authenticatePremiumUser, async (req, res) => { const { milestoneId } = req.params; @@ -900,18 +865,25 @@ app.delete('/api/premium/milestones/:milestoneId', authenticatePremiumUser, asyn return res.status(404).json({ error: 'Milestone not found or not owned by user.' }); } - // 2) Delete the single row + // 2) Delete tasks associated with this milestone + await db.run(` + DELETE FROM tasks + WHERE milestone_id = ? + `, [milestoneId]); + + // 3) Delete milestone impacts + await db.run(` + DELETE FROM milestone_impacts + WHERE milestone_id = ? + `, [milestoneId]); + + // 4) Finally remove the milestone await db.run(` DELETE FROM milestones WHERE id = ? AND user_id = ? `, [milestoneId, req.userId]); - // optionally also remove tasks + impacts if you want - // e.g.: - // await db.run('DELETE FROM tasks WHERE milestone_id = ?', [milestoneId]); - // await db.run('DELETE FROM milestone_impacts WHERE milestone_id = ?', [milestoneId]); - res.json({ message: 'Milestone deleted from this scenario.' }); } catch (err) { console.error('Error deleting single milestone:', err); @@ -919,11 +891,9 @@ app.delete('/api/premium/milestones/:milestoneId', authenticatePremiumUser, asyn } }); - /* ------------------------------------------------------------------ - FINANCIAL PROFILES (Renamed emergency_contribution) + FINANCIAL PROFILES ------------------------------------------------------------------ */ - app.get('/api/premium/financial-profile', authenticatePremiumUser, async (req, res) => { try { const row = await db.get(` @@ -948,13 +918,12 @@ app.post('/api/premium/financial-profile', authenticatePremiumUser, async (req, retirement_savings, retirement_contribution, emergency_fund, - emergency_contribution, + emergency_contribution, extra_cash_emergency_pct, extra_cash_retirement_pct } = req.body; try { - // Check if row exists const existing = await db.get(` SELECT user_id FROM financial_profiles @@ -962,7 +931,6 @@ app.post('/api/premium/financial-profile', authenticatePremiumUser, async (req, `, [req.userId]); if (!existing) { - // Insert new row await db.run(` INSERT INTO financial_profiles ( user_id, @@ -988,12 +956,11 @@ app.post('/api/premium/financial-profile', authenticatePremiumUser, async (req, retirement_savings || 0, emergency_fund || 0, retirement_contribution || 0, - emergency_contribution || 0, // store new field + emergency_contribution || 0, extra_cash_emergency_pct || 0, extra_cash_retirement_pct || 0 ]); } else { - // Update existing await db.run(` UPDATE financial_profiles SET @@ -1017,7 +984,7 @@ app.post('/api/premium/financial-profile', authenticatePremiumUser, async (req, retirement_savings || 0, emergency_fund || 0, retirement_contribution || 0, - emergency_contribution || 0, // updated field + emergency_contribution || 0, extra_cash_emergency_pct || 0, extra_cash_retirement_pct || 0, req.userId @@ -1034,179 +1001,171 @@ 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, + selected_school, + selected_program, + program_type, + is_in_state, + is_in_district, + college_enrollment_status, + is_online, + credit_hours_per_year, + credit_hours_required, + hours_completed, + program_length, + expected_graduation, + existing_college_debt, + interest_rate, + loan_term, + loan_deferral_until_graduation, + extra_payment, + expected_salary, + academic_calendar, + annual_financial_aid, + tuition, + tuition_paid + } = req.body; - app.post('/api/premium/college-profile', authenticatePremiumUser, async (req, res) => { - const { - career_path_id, - selected_school, - selected_program, - program_type, - is_in_state, - is_in_district, - college_enrollment_status, - is_online, - credit_hours_per_year, - credit_hours_required, - hours_completed, - program_length, - expected_graduation, - existing_college_debt, - interest_rate, - loan_term, - loan_deferral_until_graduation, - extra_payment, - expected_salary, - academic_calendar, - annual_financial_aid, - tuition, - tuition_paid - } = req.body; - - try { - const user_id = req.userId; - // For upsert, we either generate a new ID or (optionally) do a lookup for the old row's ID if you want to preserve it - // For simplicity, let's generate a new ID each time. We'll handle the conflict resolution below. - const newId = uuidv4(); - - // Now do an INSERT ... ON CONFLICT(...fields...). In SQLite, we reference 'excluded' for the new values. - await db.run(` - INSERT INTO college_profiles ( - 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, - created_at, - updated_at - ) - 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 - ) - - -- The magic: - 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, - 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 - }); - - // If it was a conflict, the existing row is updated. - // If not, a new row is inserted with ID = newId. - - res.status(201).json({ - message: 'College profile upsert done.', - // You might do an extra SELECT here to find which ID the final row uses if you need it - }); - } catch (error) { - console.error('Error saving college profile:', error); - res.status(500).json({ error: 'Failed to save college profile.' }); - } - }); - + try { + const user_id = req.userId; + const newId = uuidv4(); + + await db.run(` + INSERT INTO college_profiles ( + 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, + created_at, + updated_at + ) + 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, + 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.' + }); + } catch (error) { + console.error('Error saving college profile:', error); + res.status(500).json({ error: 'Failed to save college profile.' }); + } +}); app.get('/api/premium/college-profile', authenticatePremiumUser, async (req, res) => { const { careerPathId } = req.query; - // find row - const row = await db.get(` - SELECT * - FROM college_profiles - WHERE user_id = ? - AND career_path_id = ? - ORDER BY created_at DESC - LIMIT 1 - `, [req.userId, careerPathId]); - res.json(row || {}); + try { + const row = await db.get(` + SELECT * + FROM college_profiles + WHERE user_id = ? + AND career_path_id = ? + ORDER BY created_at DESC + LIMIT 1 + `, [req.userId, careerPathId]); + res.json(row || {}); + } catch (error) { + console.error('Error fetching college profile:', error); + res.status(500).json({ error: 'Failed to fetch college profile.' }); + } }); /* ------------------------------------------------------------------ @@ -1214,11 +1173,16 @@ app.get('/api/premium/college-profile', authenticatePremiumUser, async (req, res ------------------------------------------------------------------ */ app.post('/api/premium/financial-projection/:careerPathId', authenticatePremiumUser, async (req, res) => { const { careerPathId } = req.params; - const { projectionData, loanPaidOffMonth, finalEmergencySavings, finalRetirementSavings, finalLoanBalance } = req.body; + const { + projectionData, + loanPaidOffMonth, + finalEmergencySavings, + finalRetirementSavings, + finalLoanBalance + } = req.body; try { const projectionId = uuidv4(); - await db.run(` INSERT INTO financial_projections ( id, user_id, career_path_id, projection_data, @@ -1275,13 +1239,14 @@ app.get('/api/premium/financial-projection/:careerPathId', authenticatePremiumUs } }); +/* ------------------------------------------------------------------ + TASK ENDPOINTS + ------------------------------------------------------------------ */ -// POST create a new task +// CREATE a new task (already existed, repeated here for clarity) app.post('/api/premium/tasks', authenticatePremiumUser, async (req, res) => { try { const { milestone_id, title, description, due_date } = req.body; - - // Ensure required fields if (!milestone_id || !title) { return res.status(400).json({ error: 'Missing required fields', @@ -1295,7 +1260,6 @@ app.post('/api/premium/tasks', authenticatePremiumUser, async (req, res) => { FROM milestones WHERE id = ? `, [milestone_id]); - if (!milestone || milestone.user_id !== req.userId) { return res.status(403).json({ error: 'Milestone not found or not yours.' }); } @@ -1303,7 +1267,6 @@ app.post('/api/premium/tasks', authenticatePremiumUser, async (req, res) => { const taskId = uuidv4(); const now = new Date().toISOString(); - // Insert the new task await db.run(` INSERT INTO tasks ( id, @@ -1327,7 +1290,6 @@ app.post('/api/premium/tasks', authenticatePremiumUser, async (req, res) => { now ]); - // Return the newly created task as JSON const newTask = { id: taskId, milestone_id, @@ -1337,7 +1299,6 @@ app.post('/api/premium/tasks', authenticatePremiumUser, async (req, res) => { due_date: due_date || null, status: 'not_started' }; - res.status(201).json(newTask); } catch (err) { console.error('Error creating task:', err); @@ -1345,57 +1306,90 @@ app.post('/api/premium/tasks', authenticatePremiumUser, async (req, res) => { } }); -// GET tasks for a milestone -app.get('/api/premium/milestones', authenticatePremiumUser, async (req, res) => { - const { careerPathId } = req.query; - +// UPDATE an existing task +app.put('/api/premium/tasks/:taskId', authenticatePremiumUser, async (req, res) => { try { - // 1. Fetch the milestones for this user + path - const milestones = await db.all(` - SELECT * - FROM milestones - WHERE user_id = ? - AND career_path_id = ? - `, [req.userId, careerPathId]); + const { taskId } = req.params; + const { title, description, due_date, status } = req.body; - // 2. For each milestone, fetch tasks (or do a single join—see note below) - // We'll do it in Node code for clarity: - const milestoneIds = milestones.map(m => m.id); - let tasksByMilestone = {}; - if (milestoneIds.length > 0) { - const tasks = await db.all(` - SELECT * - FROM tasks - WHERE milestone_id IN (${milestoneIds.map(() => '?').join(',')}) - `, milestoneIds); + // Check ownership + const existing = await db.get(` + SELECT user_id + FROM tasks + WHERE id = ? + `, [taskId]); - // Group tasks by milestone_id - tasksByMilestone = tasks.reduce((acc, t) => { - if (!acc[t.milestone_id]) acc[t.milestone_id] = []; - acc[t.milestone_id].push(t); - return acc; - }, {}); + if (!existing || existing.user_id !== req.userId) { + return res.status(404).json({ error: 'Task not found or not owned by you.' }); } - // 3. Attach tasks to each milestone object - const milestonesWithTasks = milestones.map(m => ({ - ...m, - tasks: tasksByMilestone[m.id] || [] - })); + const now = new Date().toISOString(); + await db.run(` + UPDATE tasks + SET + title = COALESCE(?, title), + description = COALESCE(?, description), + due_date = COALESCE(?, due_date), + status = COALESCE(?, status), + updated_at = ? + WHERE id = ? + `, [ + title || null, + description || null, + due_date || null, + status || null, + now, + taskId + ]); - res.json({ milestones: milestonesWithTasks }); + // Return the updated task + const updatedTask = await db.get(` + SELECT * + FROM tasks + WHERE id = ? + `, [taskId]); + + res.json(updatedTask); } catch (err) { - console.error('Error fetching milestones with tasks:', err); - res.status(500).json({ error: 'Failed to fetch milestones.' }); + console.error('Error updating task:', err); + res.status(500).json({ error: 'Failed to update task.' }); } }); -/************************************************************************ - * MILESTONE IMPACTS ENDPOINTS - ************************************************************************/ +// DELETE a task +app.delete('/api/premium/tasks/:taskId', authenticatePremiumUser, async (req, res) => { + try { + const { taskId } = req.params; + + // Verify ownership + const existing = await db.get(` + SELECT user_id + FROM tasks + WHERE id = ? + `, [taskId]); + + if (!existing || existing.user_id !== req.userId) { + return res.status(404).json({ error: 'Task not found or not owned by you.' }); + } + + await db.run(` + DELETE FROM tasks + WHERE id = ? + `, [taskId]); + + res.json({ message: 'Task deleted successfully.' }); + } catch (err) { + console.error('Error deleting task:', err); + res.status(500).json({ error: 'Failed to delete task.' }); + } +}); + +/* ------------------------------------------------------------------ + MILESTONE IMPACTS ENDPOINTS + ------------------------------------------------------------------ */ + app.get('/api/premium/milestone-impacts', authenticatePremiumUser, async (req, res) => { try { - // Example: GET /api/premium/milestone-impacts?milestone_id=12345 const { milestone_id } = req.query; if (!milestone_id) { return res.status(400).json({ error: 'milestone_id is required.' }); @@ -1411,7 +1405,6 @@ app.get('/api/premium/milestone-impacts', authenticatePremiumUser, async (req, r return res.status(404).json({ error: 'Milestone not found or not owned by this user.' }); } - // Fetch all impacts for that milestone const impacts = await db.all(` SELECT id, @@ -1448,7 +1441,6 @@ app.post('/api/premium/milestone-impacts', authenticatePremiumUser, async (req, updated_at } = req.body; - // Basic checks if (!milestone_id || !impact_type) { return res.status(400).json({ error: 'milestone_id and impact_type are required.' @@ -1465,13 +1457,11 @@ app.post('/api/premium/milestone-impacts', authenticatePremiumUser, async (req, return res.status(403).json({ error: 'Milestone not found or not owned by this user.' }); } - // Generate UUID for this new Impact const newUUID = uuidv4(); const now = new Date().toISOString(); const finalCreated = created_at || now; const finalUpdated = updated_at || now; - // Insert row WITH that UUID into the "id" column await db.run(` INSERT INTO milestone_impacts ( id, @@ -1497,7 +1487,6 @@ app.post('/api/premium/milestone-impacts', authenticatePremiumUser, async (req, finalUpdated ]); - // Fetch & return the inserted row const insertedRow = await db.get(` SELECT id, @@ -1520,9 +1509,7 @@ app.post('/api/premium/milestone-impacts', authenticatePremiumUser, async (req, } }); -/************************************************************************ - * UPDATE an existing milestone impact (PUT) - ************************************************************************/ +// UPDATE an existing milestone impact app.put('/api/premium/milestone-impacts/:impactId', authenticatePremiumUser, async (req, res) => { try { const { impactId } = req.params; @@ -1535,7 +1522,7 @@ app.put('/api/premium/milestone-impacts/:impactId', authenticatePremiumUser, asy end_date = null } = req.body; - // 1) Check this impact belongs to user + // check ownership const existing = await db.get(` SELECT mi.id, m.user_id FROM milestone_impacts mi @@ -1547,13 +1534,11 @@ app.put('/api/premium/milestone-impacts/:impactId', authenticatePremiumUser, asy } const now = new Date().toISOString(); - - // 2) Update await db.run(` UPDATE milestone_impacts SET milestone_id = ?, - impact_type = ?, + impact_type = ?, direction = ?, amount = ?, start_date = ?, @@ -1571,7 +1556,6 @@ app.put('/api/premium/milestone-impacts/:impactId', authenticatePremiumUser, asy impactId ]); - // 3) Return updated const updatedRow = await db.get(` SELECT id, @@ -1594,14 +1578,12 @@ app.put('/api/premium/milestone-impacts/:impactId', authenticatePremiumUser, asy } }); -/************************************************************************ - * DELETE an existing milestone impact - ************************************************************************/ +// DELETE an existing milestone impact app.delete('/api/premium/milestone-impacts/:impactId', authenticatePremiumUser, async (req, res) => { try { const { impactId } = req.params; - // 1) check ownership + // check ownership const existing = await db.get(` SELECT mi.id, m.user_id FROM milestone_impacts mi @@ -1613,7 +1595,6 @@ app.delete('/api/premium/milestone-impacts/:impactId', authenticatePremiumUser, return res.status(404).json({ error: 'Impact not found or not owned by user.' }); } - // 2) Delete await db.run(` DELETE FROM milestone_impacts WHERE id = ? @@ -1626,6 +1607,9 @@ app.delete('/api/premium/milestone-impacts/:impactId', authenticatePremiumUser, } }); +/* ------------------------------------------------------------------ + FALLBACK (404 for unmatched routes) + ------------------------------------------------------------------ */ app.use((req, res) => { console.warn(`No route matched for ${req.method} ${req.originalUrl}`); res.status(404).json({ error: 'Not found' });