Added task endpoints to server3 and updated delete milestone to also delete tasks.

This commit is contained in:
Josh 2025-05-02 17:01:30 +00:00
parent b6c6814438
commit 3103b9ab29

View File

@ -9,7 +9,6 @@ import jwt from 'jsonwebtoken';
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';
import path from 'path'; import path from 'path';
import { fileURLToPath } from 'url'; import { fileURLToPath } from 'url';
// If you still need the projection logic somewhere else
import { simulateFinancialProjection } from '../src/utils/FinancialProjectionService.js'; import { simulateFinancialProjection } from '../src/utils/FinancialProjectionService.js';
const __filename = fileURLToPath(import.meta.url); const __filename = fileURLToPath(import.meta.url);
@ -56,7 +55,6 @@ const authenticatePremiumUser = (req, res, next) => {
/* ------------------------------------------------------------------ /* ------------------------------------------------------------------
CAREER PROFILE ENDPOINTS CAREER PROFILE ENDPOINTS
(Renamed from planned-path to career-profile)
------------------------------------------------------------------ */ ------------------------------------------------------------------ */
// GET the latest selected 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) => { app.post('/api/premium/career-profile', authenticatePremiumUser, async (req, res) => {
const { const {
scenario_title, scenario_title,
@ -143,7 +140,6 @@ app.post('/api/premium/career-profile', authenticatePremiumUser, async (req, res
const newCareerPathId = uuidv4(); const newCareerPathId = uuidv4();
const now = new Date().toISOString(); const now = new Date().toISOString();
// Upsert via ON CONFLICT(user_id, career_name)
await db.run(` await db.run(`
INSERT INTO career_paths ( INSERT INTO career_paths (
id, id,
@ -190,33 +186,30 @@ app.post('/api/premium/career-profile', authenticatePremiumUser, async (req, res
updated_at = ? updated_at = ?
`, [ `, [
// 18 items for the INSERT columns newCareerPathId,
newCareerPathId, // id req.userId,
req.userId, // user_id scenario_title || null,
scenario_title || null, // scenario_title career_name,
career_name, // career_name status || 'planned',
status || 'planned', // status start_date || now,
start_date || now, // start_date projected_end_date || null,
projected_end_date || null, // projected_end_date college_enrollment_status || null,
college_enrollment_status || null, // college_enrollment_status currently_working || null,
currently_working || null, // currently_working
planned_monthly_expenses ?? null, // planned_monthly_expenses planned_monthly_expenses ?? null,
planned_monthly_debt_payments ?? null, // planned_monthly_debt_payments planned_monthly_debt_payments ?? null,
planned_monthly_retirement_contribution ?? null, planned_monthly_retirement_contribution ?? null,
planned_monthly_emergency_contribution ?? null, planned_monthly_emergency_contribution ?? null,
planned_surplus_emergency_pct ?? null, planned_surplus_emergency_pct ?? null,
planned_surplus_retirement_pct ?? null, planned_surplus_retirement_pct ?? null,
planned_additional_income ?? null, planned_additional_income ?? null,
now, // created_at now,
now, // updated_at now,
// Then 1 more param for "updated_at = ?" in the conflict update
now now
]); ]);
// Optionally fetch the row's ID or entire row after upsert
const result = await db.get(` const result = await db.get(`
SELECT id SELECT id
FROM career_paths FROM career_paths
@ -234,93 +227,67 @@ app.post('/api/premium/career-profile', authenticatePremiumUser, async (req, res
} }
}); });
// DELETE a career path (scenario) by ID (and associated data)
// Delete a career path (scenario) by ID
app.delete('/api/premium/career-profile/:careerPathId', authenticatePremiumUser, async (req, res) => { app.delete('/api/premium/career-profile/:careerPathId', authenticatePremiumUser, async (req, res) => {
const { careerPathId } = req.params; const { careerPathId } = req.params;
try { try {
// 1) Confirm that this career_path belongs to the user // 1) Confirm that this career_path belongs to the user
const existing = await db.get( const existing = await db.get(`
`
SELECT id SELECT id
FROM career_paths FROM career_paths
WHERE id = ? WHERE id = ?
AND user_id = ? AND user_id = ?
`, `, [careerPathId, req.userId]);
[careerPathId, req.userId]
);
if (!existing) { if (!existing) {
return res.status(404).json({ error: 'Career path not found or not yours.' }); return res.status(404).json({ error: 'Career path not found or not yours.' });
} }
// 2) Optionally delete the college_profile for this scenario // 2) Delete the college_profile for this scenario
// (If you always keep 1-to-1 relationship: careerPathId => college_profile) await db.run(`
await db.run(
`
DELETE FROM college_profiles DELETE FROM college_profiles
WHERE user_id = ? WHERE user_id = ?
AND career_path_id = ? AND career_path_id = ?
`, `, [req.userId, careerPathId]);
[req.userId, careerPathId]
);
// 3) Optionally delete scenarios milestones // 3) Delete scenarios milestones (and tasks/impacts)
// (and any associated tasks, impacts, etc.) const scenarioMilestones = await db.all(`
// If you store tasks in tasks table, and impacts in milestone_impacts table:
// First find scenario milestones
const scenarioMilestones = await db.all(
`
SELECT id SELECT id
FROM milestones FROM milestones
WHERE user_id = ? WHERE user_id = ?
AND career_path_id = ? AND career_path_id = ?
`, `, [req.userId, careerPathId]);
[req.userId, careerPathId]
);
const milestoneIds = scenarioMilestones.map((m) => m.id); const milestoneIds = scenarioMilestones.map((m) => m.id);
if (milestoneIds.length > 0) { if (milestoneIds.length > 0) {
// Delete tasks for these milestones
const placeholders = milestoneIds.map(() => '?').join(','); const placeholders = milestoneIds.map(() => '?').join(',');
await db.run(
` // Delete tasks
await db.run(`
DELETE FROM tasks DELETE FROM tasks
WHERE milestone_id IN (${placeholders}) WHERE milestone_id IN (${placeholders})
`, `, milestoneIds);
milestoneIds
);
// Delete impacts for these milestones // Delete impacts
await db.run( await db.run(`
`
DELETE FROM milestone_impacts DELETE FROM milestone_impacts
WHERE milestone_id IN (${placeholders}) WHERE milestone_id IN (${placeholders})
`, `, milestoneIds);
milestoneIds
);
// Finally delete the milestones themselves // Finally delete the milestones themselves
await db.run( await db.run(`
`
DELETE FROM milestones DELETE FROM milestones
WHERE id IN (${placeholders}) WHERE id IN (${placeholders})
`, `, milestoneIds);
milestoneIds
);
} }
// 4) Finally delete the career_path row // 4) Delete the career_path row
await db.run( await db.run(`
`
DELETE FROM career_paths DELETE FROM career_paths
WHERE user_id = ? WHERE user_id = ?
AND id = ? AND id = ?
`, `, [req.userId, careerPathId]);
[req.userId, careerPathId]
);
res.json({ message: 'Career path and related data successfully deleted.' }); res.json({ message: 'Career path and related data successfully deleted.' });
} catch (error) { } catch (error) {
@ -329,7 +296,6 @@ app.delete('/api/premium/career-profile/:careerPathId', authenticatePremiumUser,
} }
}); });
/* ------------------------------------------------------------------ /* ------------------------------------------------------------------
Milestone ENDPOINTS Milestone ENDPOINTS
------------------------------------------------------------------ */ ------------------------------------------------------------------ */
@ -355,7 +321,6 @@ app.post('/api/premium/milestone', authenticatePremiumUser, async (req, res) =>
is_universal is_universal
} = m; } = m;
// Validate some required fields
if (!milestone_type || !title || !date || !career_path_id) { if (!milestone_type || !title || !date || !career_path_id) {
return res.status(400).json({ return res.status(400).json({
error: 'One or more milestones missing required fields', error: 'One or more milestones missing required fields',
@ -393,7 +358,7 @@ app.post('/api/premium/milestone', authenticatePremiumUser, async (req, res) =>
progress || 0, progress || 0,
status || 'planned', status || 'planned',
new_salary || null, new_salary || null,
is_universal ? 1 : 0, // store 1 or 0 is_universal ? 1 : 0,
now, now,
now now
]); ]);
@ -413,7 +378,6 @@ app.post('/api/premium/milestone', authenticatePremiumUser, async (req, res) =>
tasks: [] tasks: []
}); });
} }
// Return array of created milestones
return res.status(201).json(createdMilestones); return res.status(201).json(createdMilestones);
} }
@ -472,7 +436,6 @@ app.post('/api/premium/milestone', authenticatePremiumUser, async (req, res) =>
now now
]); ]);
// Return the newly created single milestone object
const newMilestone = { const newMilestone = {
id, id,
user_id: req.userId, user_id: req.userId,
@ -525,7 +488,6 @@ app.put('/api/premium/milestones/:milestoneId', authenticatePremiumUser, async (
const now = new Date().toISOString(); const now = new Date().toISOString();
// Merge fields with existing if not provided
const finalMilestoneType = milestone_type || existing.milestone_type; const finalMilestoneType = milestone_type || existing.milestone_type;
const finalTitle = title || existing.title; const finalTitle = title || existing.title;
const finalDesc = description || existing.description; const finalDesc = description || existing.description;
@ -537,7 +499,6 @@ app.put('/api/premium/milestones/:milestoneId', authenticatePremiumUser, async (
const finalIsUniversal = const finalIsUniversal =
is_universal != null ? (is_universal ? 1 : 0) : existing.is_universal; is_universal != null ? (is_universal ? 1 : 0) : existing.is_universal;
// Update row
await db.run(` await db.run(`
UPDATE milestones UPDATE milestones
SET SET
@ -568,14 +529,12 @@ app.put('/api/premium/milestones/:milestoneId', authenticatePremiumUser, async (
req.userId req.userId
]); ]);
// Return the updated record with tasks // Return the updated milestone with tasks
const updatedMilestoneRow = await db.get(` const updatedMilestoneRow = await db.get(`
SELECT * SELECT *
FROM milestones FROM milestones
WHERE id = ? WHERE id = ?
`, [milestoneId]); `, [milestoneId]);
// Fetch tasks for this milestone
const tasks = await db.all(` const tasks = await db.all(`
SELECT * SELECT *
FROM tasks FROM tasks
@ -597,11 +556,9 @@ app.put('/api/premium/milestones/:milestoneId', authenticatePremiumUser, async (
// GET all milestones for a given careerPathId // GET all milestones for a given careerPathId
app.get('/api/premium/milestones', authenticatePremiumUser, async (req, res) => { app.get('/api/premium/milestones', authenticatePremiumUser, async (req, res) => {
const { careerPathId } = req.query; const { careerPathId } = req.query;
try { try {
// if user wants universal=1 only, e.g. careerPathId=universal // universal milestones
if (careerPathId === 'universal') { if (careerPathId === 'universal') {
// For example, fetch all is_universal=1 for the user:
const universalRows = await db.all(` const universalRows = await db.all(`
SELECT * SELECT *
FROM milestones FROM milestones
@ -609,7 +566,6 @@ app.get('/api/premium/milestones', authenticatePremiumUser, async (req, res) =>
AND is_universal = 1 AND is_universal = 1
`, [req.userId]); `, [req.userId]);
// attach tasks if needed
const milestoneIds = universalRows.map(m => m.id); const milestoneIds = universalRows.map(m => m.id);
let tasksByMilestone = {}; let tasksByMilestone = {};
if (milestoneIds.length > 0) { if (milestoneIds.length > 0) {
@ -624,6 +580,7 @@ app.get('/api/premium/milestones', authenticatePremiumUser, async (req, res) =>
return acc; return acc;
}, {}); }, {});
} }
const uniMils = universalRows.map(m => ({ const uniMils = universalRows.map(m => ({
...m, ...m,
tasks: tasksByMilestone[m.id] || [] 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.' }); return res.status(400).json({ error: 'Missing milestoneId or scenarioIds.' });
} }
// 1) Fetch the original
const original = await db.get(` const original = await db.get(`
SELECT * SELECT *
FROM milestones FROM milestones
WHERE id = ? WHERE id = ?
AND user_id = ? AND user_id = ?
`, [milestoneId, req.userId]); `, [milestoneId, req.userId]);
if (!original) { if (!original) {
return res.status(404).json({ error: 'Milestone not found or not owned by user.' }); 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) { if (original.is_universal !== 1) {
await db.run(` await db.run(`
UPDATE milestones UPDATE milestones
@ -695,12 +649,9 @@ app.post('/api/premium/milestone/copy', authenticatePremiumUser, async (req, res
WHERE id = ? WHERE id = ?
AND user_id = ? AND user_id = ?
`, [ milestoneId, req.userId ]); `, [ milestoneId, req.userId ]);
// Also refresh "original" object if you want
original.is_universal = 1; original.is_universal = 1;
} }
// 3) If no origin_milestone_id, set it
let originId = original.origin_milestone_id || original.id; let originId = original.origin_milestone_id || original.id;
if (!original.origin_milestone_id) { if (!original.origin_milestone_id) {
await db.run(` await db.run(`
@ -711,7 +662,6 @@ app.post('/api/premium/milestone/copy', authenticatePremiumUser, async (req, res
`, [ originId, milestoneId, req.userId ]); `, [ originId, milestoneId, req.userId ]);
} }
// 4) fetch tasks & impacts
const tasks = await db.all(` const tasks = await db.all(`
SELECT * SELECT *
FROM tasks FROM tasks
@ -728,13 +678,9 @@ app.post('/api/premium/milestone/copy', authenticatePremiumUser, async (req, res
const copiesCreated = []; const copiesCreated = [];
for (let scenarioId of scenarioIds) { for (let scenarioId of scenarioIds) {
if (scenarioId === original.career_path_id) { if (scenarioId === original.career_path_id) continue; // skip if same scenario
continue;
}
const newMilestoneId = uuidv4(); const newMilestoneId = uuidv4();
// Always set isUniversal=1 on copies
const isUniversal = 1; const isUniversal = 1;
await db.run(` await db.run(`
@ -814,7 +760,8 @@ app.post('/api/premium/milestone/copy', authenticatePremiumUser, async (req, res
end_date, end_date,
created_at, created_at,
updated_at updated_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) )
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
`, [ `, [
newImpactId, newImpactId,
newMilestoneId, 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.' }); 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; const originId = existing.origin_milestone_id || existing.id;
// 2) Delete all copies referencing that origin // Find all those milestone IDs
await db.run(` const allMilsToDelete = await db.all(`
DELETE FROM milestones SELECT id
FROM milestones
WHERE user_id = ? WHERE user_id = ?
AND origin_milestone_id = ? AND (id = ? OR origin_milestone_id = ?)
`, [req.userId, originId]); `, [req.userId, originId, originId]);
// Also delete the original if it doesn't store itself in origin_milestone_id 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(` await db.run(`
DELETE FROM milestones DELETE FROM milestones
WHERE user_id = ? WHERE user_id = ?
AND id = ? AND (id = ? OR origin_milestone_id = ?)
AND origin_milestone_id IS NULL `, [req.userId, originId, originId]);
`, [req.userId, originId]); }
res.json({ message: 'Deleted from all scenarios' }); res.json({ message: 'Deleted from all scenarios' });
} catch (err) { } 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) => { app.delete('/api/premium/milestones/:milestoneId', authenticatePremiumUser, async (req, res) => {
const { milestoneId } = req.params; 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.' }); 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(` await db.run(`
DELETE FROM milestones DELETE FROM milestones
WHERE id = ? WHERE id = ?
AND user_id = ? AND user_id = ?
`, [milestoneId, req.userId]); `, [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.' }); res.json({ message: 'Milestone deleted from this scenario.' });
} catch (err) { } catch (err) {
console.error('Error deleting single milestone:', 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) => { app.get('/api/premium/financial-profile', authenticatePremiumUser, async (req, res) => {
try { try {
const row = await db.get(` const row = await db.get(`
@ -954,7 +924,6 @@ app.post('/api/premium/financial-profile', authenticatePremiumUser, async (req,
} = req.body; } = req.body;
try { try {
// Check if row exists
const existing = await db.get(` const existing = await db.get(`
SELECT user_id SELECT user_id
FROM financial_profiles FROM financial_profiles
@ -962,7 +931,6 @@ app.post('/api/premium/financial-profile', authenticatePremiumUser, async (req,
`, [req.userId]); `, [req.userId]);
if (!existing) { if (!existing) {
// Insert new row
await db.run(` await db.run(`
INSERT INTO financial_profiles ( INSERT INTO financial_profiles (
user_id, user_id,
@ -988,12 +956,11 @@ app.post('/api/premium/financial-profile', authenticatePremiumUser, async (req,
retirement_savings || 0, retirement_savings || 0,
emergency_fund || 0, emergency_fund || 0,
retirement_contribution || 0, retirement_contribution || 0,
emergency_contribution || 0, // store new field emergency_contribution || 0,
extra_cash_emergency_pct || 0, extra_cash_emergency_pct || 0,
extra_cash_retirement_pct || 0 extra_cash_retirement_pct || 0
]); ]);
} else { } else {
// Update existing
await db.run(` await db.run(`
UPDATE financial_profiles UPDATE financial_profiles
SET SET
@ -1017,7 +984,7 @@ app.post('/api/premium/financial-profile', authenticatePremiumUser, async (req,
retirement_savings || 0, retirement_savings || 0,
emergency_fund || 0, emergency_fund || 0,
retirement_contribution || 0, retirement_contribution || 0,
emergency_contribution || 0, // updated field emergency_contribution || 0,
extra_cash_emergency_pct || 0, extra_cash_emergency_pct || 0,
extra_cash_retirement_pct || 0, extra_cash_retirement_pct || 0,
req.userId req.userId
@ -1034,8 +1001,7 @@ app.post('/api/premium/financial-profile', authenticatePremiumUser, async (req,
/* ------------------------------------------------------------------ /* ------------------------------------------------------------------
COLLEGE PROFILES COLLEGE PROFILES
------------------------------------------------------------------ */ ------------------------------------------------------------------ */
app.post('/api/premium/college-profile', authenticatePremiumUser, async (req, res) => {
app.post('/api/premium/college-profile', authenticatePremiumUser, async (req, res) => {
const { const {
career_path_id, career_path_id,
selected_school, selected_school,
@ -1064,11 +1030,8 @@ app.post('/api/premium/financial-profile', authenticatePremiumUser, async (req,
try { try {
const user_id = req.userId; 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(); const newId = uuidv4();
// Now do an INSERT ... ON CONFLICT(...fields...). In SQLite, we reference 'excluded' for the new values.
await db.run(` await db.run(`
INSERT INTO college_profiles ( INSERT INTO college_profiles (
id, id,
@ -1128,8 +1091,6 @@ app.post('/api/premium/financial-profile', authenticatePremiumUser, async (req,
CURRENT_TIMESTAMP, CURRENT_TIMESTAMP,
CURRENT_TIMESTAMP CURRENT_TIMESTAMP
) )
-- The magic:
ON CONFLICT(user_id, career_path_id, selected_school, selected_program, program_type) ON CONFLICT(user_id, career_path_id, selected_school, selected_program, program_type)
DO UPDATE SET DO UPDATE SET
is_in_state = excluded.is_in_state, is_in_state = excluded.is_in_state,
@ -1152,7 +1113,6 @@ app.post('/api/premium/financial-profile', authenticatePremiumUser, async (req,
tuition = excluded.tuition, tuition = excluded.tuition,
tuition_paid = excluded.tuition_paid, tuition_paid = excluded.tuition_paid,
updated_at = CURRENT_TIMESTAMP updated_at = CURRENT_TIMESTAMP
;
`, { `, {
':id': newId, ':id': newId,
':user_id': user_id, ':user_id': user_id,
@ -1181,23 +1141,18 @@ app.post('/api/premium/financial-profile', authenticatePremiumUser, async (req,
':tuition_paid': tuition_paid || 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({ res.status(201).json({
message: 'College profile upsert done.', 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) { } catch (error) {
console.error('Error saving college profile:', error); console.error('Error saving college profile:', error);
res.status(500).json({ error: 'Failed to save college profile.' }); res.status(500).json({ error: 'Failed to save college profile.' });
} }
}); });
app.get('/api/premium/college-profile', authenticatePremiumUser, async (req, res) => { app.get('/api/premium/college-profile', authenticatePremiumUser, async (req, res) => {
const { careerPathId } = req.query; const { careerPathId } = req.query;
// find row try {
const row = await db.get(` const row = await db.get(`
SELECT * SELECT *
FROM college_profiles FROM college_profiles
@ -1207,6 +1162,10 @@ app.get('/api/premium/college-profile', authenticatePremiumUser, async (req, res
LIMIT 1 LIMIT 1
`, [req.userId, careerPathId]); `, [req.userId, careerPathId]);
res.json(row || {}); 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) => { app.post('/api/premium/financial-projection/:careerPathId', authenticatePremiumUser, async (req, res) => {
const { careerPathId } = req.params; const { careerPathId } = req.params;
const { projectionData, loanPaidOffMonth, finalEmergencySavings, finalRetirementSavings, finalLoanBalance } = req.body; const {
projectionData,
loanPaidOffMonth,
finalEmergencySavings,
finalRetirementSavings,
finalLoanBalance
} = req.body;
try { try {
const projectionId = uuidv4(); const projectionId = uuidv4();
await db.run(` await db.run(`
INSERT INTO financial_projections ( INSERT INTO financial_projections (
id, user_id, career_path_id, projection_data, 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) => { app.post('/api/premium/tasks', authenticatePremiumUser, async (req, res) => {
try { try {
const { milestone_id, title, description, due_date } = req.body; const { milestone_id, title, description, due_date } = req.body;
// Ensure required fields
if (!milestone_id || !title) { if (!milestone_id || !title) {
return res.status(400).json({ return res.status(400).json({
error: 'Missing required fields', error: 'Missing required fields',
@ -1295,7 +1260,6 @@ app.post('/api/premium/tasks', authenticatePremiumUser, async (req, res) => {
FROM milestones FROM milestones
WHERE id = ? WHERE id = ?
`, [milestone_id]); `, [milestone_id]);
if (!milestone || milestone.user_id !== req.userId) { if (!milestone || milestone.user_id !== req.userId) {
return res.status(403).json({ error: 'Milestone not found or not yours.' }); 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 taskId = uuidv4();
const now = new Date().toISOString(); const now = new Date().toISOString();
// Insert the new task
await db.run(` await db.run(`
INSERT INTO tasks ( INSERT INTO tasks (
id, id,
@ -1327,7 +1290,6 @@ app.post('/api/premium/tasks', authenticatePremiumUser, async (req, res) => {
now now
]); ]);
// Return the newly created task as JSON
const newTask = { const newTask = {
id: taskId, id: taskId,
milestone_id, milestone_id,
@ -1337,7 +1299,6 @@ app.post('/api/premium/tasks', authenticatePremiumUser, async (req, res) => {
due_date: due_date || null, due_date: due_date || null,
status: 'not_started' status: 'not_started'
}; };
res.status(201).json(newTask); res.status(201).json(newTask);
} catch (err) { } catch (err) {
console.error('Error creating task:', 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 // UPDATE an existing task
app.get('/api/premium/milestones', authenticatePremiumUser, async (req, res) => { app.put('/api/premium/tasks/:taskId', authenticatePremiumUser, async (req, res) => {
const { careerPathId } = req.query;
try { try {
// 1. Fetch the milestones for this user + path const { taskId } = req.params;
const milestones = await db.all(` const { title, description, due_date, status } = req.body;
SELECT *
FROM milestones
WHERE user_id = ?
AND career_path_id = ?
`, [req.userId, careerPathId]);
// 2. For each milestone, fetch tasks (or do a single join—see note below) // Check ownership
// We'll do it in Node code for clarity: const existing = await db.get(`
const milestoneIds = milestones.map(m => m.id); SELECT user_id
let tasksByMilestone = {};
if (milestoneIds.length > 0) {
const tasks = await db.all(`
SELECT *
FROM tasks FROM tasks
WHERE milestone_id IN (${milestoneIds.map(() => '?').join(',')}) WHERE id = ?
`, milestoneIds); `, [taskId]);
// Group tasks by milestone_id if (!existing || existing.user_id !== req.userId) {
tasksByMilestone = tasks.reduce((acc, t) => { return res.status(404).json({ error: 'Task not found or not owned by you.' });
if (!acc[t.milestone_id]) acc[t.milestone_id] = [];
acc[t.milestone_id].push(t);
return acc;
}, {});
} }
// 3. Attach tasks to each milestone object const now = new Date().toISOString();
const milestonesWithTasks = milestones.map(m => ({ await db.run(`
...m, UPDATE tasks
tasks: tasksByMilestone[m.id] || [] 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) { } catch (err) {
console.error('Error fetching milestones with tasks:', err); console.error('Error updating task:', err);
res.status(500).json({ error: 'Failed to fetch milestones.' }); res.status(500).json({ error: 'Failed to update task.' });
} }
}); });
/************************************************************************ // DELETE a task
* MILESTONE IMPACTS ENDPOINTS 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) => { app.get('/api/premium/milestone-impacts', authenticatePremiumUser, async (req, res) => {
try { try {
// Example: GET /api/premium/milestone-impacts?milestone_id=12345
const { milestone_id } = req.query; const { milestone_id } = req.query;
if (!milestone_id) { if (!milestone_id) {
return res.status(400).json({ error: 'milestone_id is required.' }); 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.' }); 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(` const impacts = await db.all(`
SELECT SELECT
id, id,
@ -1448,7 +1441,6 @@ app.post('/api/premium/milestone-impacts', authenticatePremiumUser, async (req,
updated_at updated_at
} = req.body; } = req.body;
// Basic checks
if (!milestone_id || !impact_type) { if (!milestone_id || !impact_type) {
return res.status(400).json({ return res.status(400).json({
error: 'milestone_id and impact_type are required.' 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.' }); 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 newUUID = uuidv4();
const now = new Date().toISOString(); const now = new Date().toISOString();
const finalCreated = created_at || now; const finalCreated = created_at || now;
const finalUpdated = updated_at || now; const finalUpdated = updated_at || now;
// Insert row WITH that UUID into the "id" column
await db.run(` await db.run(`
INSERT INTO milestone_impacts ( INSERT INTO milestone_impacts (
id, id,
@ -1497,7 +1487,6 @@ app.post('/api/premium/milestone-impacts', authenticatePremiumUser, async (req,
finalUpdated finalUpdated
]); ]);
// Fetch & return the inserted row
const insertedRow = await db.get(` const insertedRow = await db.get(`
SELECT SELECT
id, id,
@ -1520,9 +1509,7 @@ app.post('/api/premium/milestone-impacts', authenticatePremiumUser, async (req,
} }
}); });
/************************************************************************ // UPDATE an existing milestone impact
* UPDATE an existing milestone impact (PUT)
************************************************************************/
app.put('/api/premium/milestone-impacts/:impactId', authenticatePremiumUser, async (req, res) => { app.put('/api/premium/milestone-impacts/:impactId', authenticatePremiumUser, async (req, res) => {
try { try {
const { impactId } = req.params; const { impactId } = req.params;
@ -1535,7 +1522,7 @@ app.put('/api/premium/milestone-impacts/:impactId', authenticatePremiumUser, asy
end_date = null end_date = null
} = req.body; } = req.body;
// 1) Check this impact belongs to user // check ownership
const existing = await db.get(` const existing = await db.get(`
SELECT mi.id, m.user_id SELECT mi.id, m.user_id
FROM milestone_impacts mi FROM milestone_impacts mi
@ -1547,8 +1534,6 @@ app.put('/api/premium/milestone-impacts/:impactId', authenticatePremiumUser, asy
} }
const now = new Date().toISOString(); const now = new Date().toISOString();
// 2) Update
await db.run(` await db.run(`
UPDATE milestone_impacts UPDATE milestone_impacts
SET SET
@ -1571,7 +1556,6 @@ app.put('/api/premium/milestone-impacts/:impactId', authenticatePremiumUser, asy
impactId impactId
]); ]);
// 3) Return updated
const updatedRow = await db.get(` const updatedRow = await db.get(`
SELECT SELECT
id, 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) => { app.delete('/api/premium/milestone-impacts/:impactId', authenticatePremiumUser, async (req, res) => {
try { try {
const { impactId } = req.params; const { impactId } = req.params;
// 1) check ownership // check ownership
const existing = await db.get(` const existing = await db.get(`
SELECT mi.id, m.user_id SELECT mi.id, m.user_id
FROM milestone_impacts mi 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.' }); return res.status(404).json({ error: 'Impact not found or not owned by user.' });
} }
// 2) Delete
await db.run(` await db.run(`
DELETE FROM milestone_impacts DELETE FROM milestone_impacts
WHERE id = ? WHERE id = ?
@ -1626,6 +1607,9 @@ app.delete('/api/premium/milestone-impacts/:impactId', authenticatePremiumUser,
} }
}); });
/* ------------------------------------------------------------------
FALLBACK (404 for unmatched routes)
------------------------------------------------------------------ */
app.use((req, res) => { app.use((req, res) => {
console.warn(`No route matched for ${req.method} ${req.originalUrl}`); console.warn(`No route matched for ${req.method} ${req.originalUrl}`);
res.status(404).json({ error: 'Not found' }); res.status(404).json({ error: 'Not found' });