1536 lines
43 KiB
JavaScript
1536 lines
43 KiB
JavaScript
// server3.js
|
|
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 { 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);
|
|
const __dirname = path.dirname(__filename);
|
|
|
|
dotenv.config({ path: path.resolve(__dirname, '..', '.env') });
|
|
|
|
const app = express();
|
|
const PORT = process.env.PREMIUM_PORT || 5002;
|
|
|
|
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();
|
|
|
|
app.use(helmet());
|
|
app.use(express.json());
|
|
|
|
const allowedOrigins = ['https://dev1.aptivaai.com'];
|
|
app.use(cors({ origin: allowedOrigins, credentials: true }));
|
|
|
|
const authenticatePremiumUser = (req, res, next) => {
|
|
const token = req.headers.authorization?.split(' ')[1];
|
|
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;
|
|
next();
|
|
} catch (error) {
|
|
return res.status(403).json({ error: 'Invalid or expired token' });
|
|
}
|
|
};
|
|
|
|
/* ------------------------------------------------------------------
|
|
CAREER PROFILE ENDPOINTS
|
|
(Renamed from planned-path to career-profile)
|
|
------------------------------------------------------------------ */
|
|
|
|
// GET the latest selected career profile
|
|
app.get('/api/premium/career-profile/latest', authenticatePremiumUser, async (req, res) => {
|
|
try {
|
|
const row = await db.get(`
|
|
SELECT *
|
|
FROM career_paths
|
|
WHERE user_id = ?
|
|
ORDER BY start_date DESC
|
|
LIMIT 1
|
|
`, [req.userId]);
|
|
res.json(row || {});
|
|
} catch (error) {
|
|
console.error('Error fetching latest career profile:', error);
|
|
res.status(500).json({ error: 'Failed to fetch latest career profile' });
|
|
}
|
|
});
|
|
|
|
// GET all career profiles for the user
|
|
app.get('/api/premium/career-profile/all', authenticatePremiumUser, async (req, res) => {
|
|
try {
|
|
const rows = await db.all(`
|
|
SELECT *
|
|
FROM career_paths
|
|
WHERE user_id = ?
|
|
ORDER BY start_date ASC
|
|
`, [req.userId]);
|
|
res.json({ careerPaths: rows });
|
|
} catch (error) {
|
|
console.error('Error fetching career profiles:', error);
|
|
res.status(500).json({ error: 'Failed to fetch career profiles' });
|
|
}
|
|
});
|
|
|
|
// GET a single career profile (scenario) by ID
|
|
app.get('/api/premium/career-profile/:careerPathId', authenticatePremiumUser, async (req, res) => {
|
|
const { careerPathId } = req.params;
|
|
try {
|
|
const row = await db.get(`
|
|
SELECT *
|
|
FROM career_paths
|
|
WHERE id = ?
|
|
AND user_id = ?
|
|
`, [careerPathId, req.userId]);
|
|
|
|
if (!row) {
|
|
return res.status(404).json({ error: 'Career path (scenario) not found or not yours.' });
|
|
}
|
|
|
|
res.json(row);
|
|
} catch (error) {
|
|
console.error('Error fetching single career profile:', error);
|
|
res.status(500).json({ error: 'Failed to fetch career profile by ID.' });
|
|
}
|
|
});
|
|
|
|
// POST a new career profile
|
|
// server3.js
|
|
app.post('/api/premium/career-profile', authenticatePremiumUser, async (req, res) => {
|
|
const {
|
|
career_name,
|
|
status,
|
|
start_date,
|
|
projected_end_date,
|
|
college_enrollment_status,
|
|
currently_working,
|
|
|
|
// NEW planned columns
|
|
planned_monthly_expenses,
|
|
planned_monthly_debt_payments,
|
|
planned_monthly_retirement_contribution,
|
|
planned_monthly_emergency_contribution,
|
|
planned_surplus_emergency_pct,
|
|
planned_surplus_retirement_pct,
|
|
planned_additional_income
|
|
} = req.body;
|
|
|
|
if (!career_name) {
|
|
return res.status(400).json({ error: 'career_name is required.' });
|
|
}
|
|
|
|
try {
|
|
const newCareerPathId = uuidv4();
|
|
const now = new Date().toISOString();
|
|
|
|
// Insert or update row in career_paths. We rely on ON CONFLICT(user_id, career_name).
|
|
// If you want a different conflict target, change accordingly.
|
|
await db.run(`
|
|
INSERT INTO career_paths (
|
|
id,
|
|
user_id,
|
|
career_name,
|
|
status,
|
|
start_date,
|
|
projected_end_date,
|
|
college_enrollment_status,
|
|
currently_working,
|
|
|
|
planned_monthly_expenses,
|
|
planned_monthly_debt_payments,
|
|
planned_monthly_retirement_contribution,
|
|
planned_monthly_emergency_contribution,
|
|
planned_surplus_emergency_pct,
|
|
planned_surplus_retirement_pct,
|
|
planned_additional_income,
|
|
|
|
created_at,
|
|
updated_at
|
|
)
|
|
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,
|
|
|
|
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,
|
|
career_name,
|
|
status || 'planned',
|
|
start_date || now,
|
|
projected_end_date || null,
|
|
college_enrollment_status || null,
|
|
currently_working || null,
|
|
|
|
// new planned columns
|
|
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 // updated_at on conflict
|
|
]);
|
|
|
|
// Optionally fetch the row's ID (or entire row) after upsert:
|
|
const result = await db.get(`
|
|
SELECT id
|
|
FROM career_paths
|
|
WHERE user_id = ?
|
|
AND career_name = ?
|
|
`, [req.userId, career_name]);
|
|
|
|
res.status(200).json({
|
|
message: 'Career profile upserted.',
|
|
career_path_id: result?.id
|
|
});
|
|
} catch (error) {
|
|
console.error('Error upserting career profile:', error);
|
|
res.status(500).json({ error: 'Failed to upsert career profile.' });
|
|
}
|
|
});
|
|
|
|
/* ------------------------------------------------------------------
|
|
Milestone ENDPOINTS
|
|
------------------------------------------------------------------ */
|
|
|
|
// CREATE one or more milestones
|
|
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)) {
|
|
const createdMilestones = [];
|
|
for (const m of body.milestones) {
|
|
const {
|
|
milestone_type,
|
|
title,
|
|
description,
|
|
date,
|
|
career_path_id,
|
|
progress,
|
|
status,
|
|
new_salary,
|
|
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',
|
|
details: m
|
|
});
|
|
}
|
|
|
|
const id = uuidv4();
|
|
const now = new Date().toISOString();
|
|
|
|
await db.run(`
|
|
INSERT INTO milestones (
|
|
id,
|
|
user_id,
|
|
career_path_id,
|
|
milestone_type,
|
|
title,
|
|
description,
|
|
date,
|
|
progress,
|
|
status,
|
|
new_salary,
|
|
is_universal,
|
|
created_at,
|
|
updated_at
|
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
`, [
|
|
id,
|
|
req.userId,
|
|
career_path_id,
|
|
milestone_type,
|
|
title,
|
|
description || '',
|
|
date,
|
|
progress || 0,
|
|
status || 'planned',
|
|
new_salary || null,
|
|
is_universal ? 1 : 0, // store 1 or 0
|
|
now,
|
|
now
|
|
]);
|
|
|
|
createdMilestones.push({
|
|
id,
|
|
user_id: req.userId,
|
|
career_path_id,
|
|
milestone_type,
|
|
title,
|
|
description: description || '',
|
|
date,
|
|
progress: progress || 0,
|
|
status: status || 'planned',
|
|
new_salary: new_salary || null,
|
|
is_universal: is_universal ? 1 : 0,
|
|
tasks: []
|
|
});
|
|
}
|
|
// Return array of created milestones
|
|
return res.status(201).json(createdMilestones);
|
|
}
|
|
|
|
// CASE 2: Single milestone creation
|
|
const {
|
|
milestone_type,
|
|
title,
|
|
description,
|
|
date,
|
|
career_path_id,
|
|
progress,
|
|
status,
|
|
new_salary,
|
|
is_universal
|
|
} = body;
|
|
|
|
if (!milestone_type || !title || !date || !career_path_id) {
|
|
return res.status(400).json({
|
|
error: 'Missing required fields',
|
|
details: { milestone_type, title, date, career_path_id }
|
|
});
|
|
}
|
|
|
|
const id = uuidv4();
|
|
const now = new Date().toISOString();
|
|
|
|
await db.run(`
|
|
INSERT INTO milestones (
|
|
id,
|
|
user_id,
|
|
career_path_id,
|
|
milestone_type,
|
|
title,
|
|
description,
|
|
date,
|
|
progress,
|
|
status,
|
|
new_salary,
|
|
is_universal,
|
|
created_at,
|
|
updated_at
|
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
`, [
|
|
id,
|
|
req.userId,
|
|
career_path_id,
|
|
milestone_type,
|
|
title,
|
|
description || '',
|
|
date,
|
|
progress || 0,
|
|
status || 'planned',
|
|
new_salary || null,
|
|
is_universal ? 1 : 0,
|
|
now,
|
|
now
|
|
]);
|
|
|
|
// Return the newly created single milestone object
|
|
const newMilestone = {
|
|
id,
|
|
user_id: req.userId,
|
|
career_path_id,
|
|
milestone_type,
|
|
title,
|
|
description: description || '',
|
|
date,
|
|
progress: progress || 0,
|
|
status: status || 'planned',
|
|
new_salary: new_salary || null,
|
|
is_universal: is_universal ? 1 : 0,
|
|
tasks: []
|
|
};
|
|
|
|
res.status(201).json(newMilestone);
|
|
} catch (err) {
|
|
console.error('Error creating milestone(s):', err);
|
|
res.status(500).json({ error: 'Failed to create milestone(s).' });
|
|
}
|
|
});
|
|
|
|
// UPDATE an existing milestone
|
|
app.put('/api/premium/milestones/:milestoneId', authenticatePremiumUser, async (req, res) => {
|
|
try {
|
|
const { milestoneId } = req.params;
|
|
const {
|
|
milestone_type,
|
|
title,
|
|
description,
|
|
date,
|
|
career_path_id,
|
|
progress,
|
|
status,
|
|
new_salary,
|
|
is_universal
|
|
} = req.body;
|
|
|
|
// Check if milestone exists and belongs to user
|
|
const existing = await db.get(`
|
|
SELECT *
|
|
FROM milestones
|
|
WHERE id = ?
|
|
AND user_id = ?
|
|
`, [milestoneId, req.userId]);
|
|
|
|
if (!existing) {
|
|
return res.status(404).json({ error: 'Milestone not found or not yours.' });
|
|
}
|
|
|
|
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;
|
|
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;
|
|
|
|
// Update row
|
|
await db.run(`
|
|
UPDATE milestones
|
|
SET
|
|
milestone_type = ?,
|
|
title = ?,
|
|
description = ?,
|
|
date = ?,
|
|
career_path_id = ?,
|
|
progress = ?,
|
|
status = ?,
|
|
new_salary = ?,
|
|
is_universal = ?,
|
|
updated_at = ?
|
|
WHERE id = ?
|
|
AND user_id = ?
|
|
`, [
|
|
finalMilestoneType,
|
|
finalTitle,
|
|
finalDesc,
|
|
finalDate,
|
|
finalCareerPath,
|
|
finalProgress,
|
|
finalStatus,
|
|
finalSalary,
|
|
finalIsUniversal,
|
|
now,
|
|
milestoneId,
|
|
req.userId
|
|
]);
|
|
|
|
// Return the updated record 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
|
|
WHERE milestone_id = ?
|
|
`, [milestoneId]);
|
|
|
|
const updatedMilestone = {
|
|
...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
|
|
app.get('/api/premium/milestones', authenticatePremiumUser, async (req, res) => {
|
|
const { careerPathId } = req.query;
|
|
|
|
try {
|
|
// if user wants universal=1 only, e.g. careerPathId=universal
|
|
if (careerPathId === 'universal') {
|
|
// For example, fetch all is_universal=1 for the user:
|
|
const universalRows = await db.all(`
|
|
SELECT *
|
|
FROM milestones
|
|
WHERE user_id = ?
|
|
AND is_universal = 1
|
|
`, [req.userId]);
|
|
|
|
// attach tasks if needed
|
|
const milestoneIds = universalRows.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);
|
|
tasksByMilestone = tasks.reduce((acc, t) => {
|
|
if (!acc[t.milestone_id]) acc[t.milestone_id] = [];
|
|
acc[t.milestone_id].push(t);
|
|
return acc;
|
|
}, {});
|
|
}
|
|
const uniMils = universalRows.map(m => ({
|
|
...m,
|
|
tasks: tasksByMilestone[m.id] || []
|
|
}));
|
|
return res.json({ milestones: uniMils });
|
|
}
|
|
|
|
// else fetch by careerPathId
|
|
const milestones = await db.all(`
|
|
SELECT *
|
|
FROM milestones
|
|
WHERE user_id = ?
|
|
AND career_path_id = ?
|
|
`, [req.userId, careerPathId]);
|
|
|
|
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);
|
|
|
|
tasksByMilestone = tasks.reduce((acc, t) => {
|
|
if (!acc[t.milestone_id]) acc[t.milestone_id] = [];
|
|
acc[t.milestone_id].push(t);
|
|
return acc;
|
|
}, {});
|
|
}
|
|
|
|
const milestonesWithTasks = milestones.map(m => ({
|
|
...m,
|
|
tasks: tasksByMilestone[m.id] || []
|
|
}));
|
|
|
|
res.json({ milestones: milestonesWithTasks });
|
|
} catch (err) {
|
|
console.error('Error fetching milestones with tasks:', err);
|
|
res.status(500).json({ error: 'Failed to fetch milestones.' });
|
|
}
|
|
});
|
|
|
|
// COPY an existing milestone to other scenarios
|
|
app.post('/api/premium/milestone/copy', authenticatePremiumUser, async (req, res) => {
|
|
try {
|
|
const { milestoneId, scenarioIds } = req.body;
|
|
if (!milestoneId || !Array.isArray(scenarioIds) || scenarioIds.length === 0) {
|
|
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
|
|
SET is_universal = 1
|
|
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(`
|
|
UPDATE milestones
|
|
SET origin_milestone_id = ?
|
|
WHERE id = ?
|
|
AND user_id = ?
|
|
`, [ originId, milestoneId, req.userId ]);
|
|
}
|
|
|
|
// 4) fetch tasks & impacts
|
|
const tasks = await db.all(`
|
|
SELECT *
|
|
FROM tasks
|
|
WHERE milestone_id = ?
|
|
`, [milestoneId]);
|
|
|
|
const impacts = await db.all(`
|
|
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;
|
|
}
|
|
|
|
const newMilestoneId = uuidv4();
|
|
|
|
// Always set isUniversal=1 on copies
|
|
const isUniversal = 1;
|
|
|
|
await db.run(`
|
|
INSERT INTO milestones (
|
|
id,
|
|
user_id,
|
|
career_path_id,
|
|
milestone_type,
|
|
title,
|
|
description,
|
|
date,
|
|
progress,
|
|
status,
|
|
new_salary,
|
|
is_universal,
|
|
origin_milestone_id,
|
|
created_at,
|
|
updated_at
|
|
)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
`, [
|
|
newMilestoneId,
|
|
req.userId,
|
|
scenarioId,
|
|
original.milestone_type,
|
|
original.title,
|
|
original.description,
|
|
original.date,
|
|
original.progress,
|
|
original.status,
|
|
original.new_salary,
|
|
isUniversal,
|
|
originId,
|
|
now,
|
|
now
|
|
]);
|
|
|
|
// copy tasks
|
|
for (let t of tasks) {
|
|
const newTaskId = uuidv4();
|
|
await db.run(`
|
|
INSERT INTO tasks (
|
|
id,
|
|
milestone_id,
|
|
user_id,
|
|
title,
|
|
description,
|
|
due_date,
|
|
status,
|
|
created_at,
|
|
updated_at
|
|
)
|
|
VALUES (?, ?, ?, ?, ?, ?, 'not_started', ?, ?)
|
|
`, [
|
|
newTaskId,
|
|
newMilestoneId,
|
|
req.userId,
|
|
t.title,
|
|
t.description,
|
|
t.due_date || null,
|
|
now,
|
|
now
|
|
]);
|
|
}
|
|
|
|
// copy impacts
|
|
for (let imp of impacts) {
|
|
const newImpactId = uuidv4();
|
|
await db.run(`
|
|
INSERT INTO milestone_impacts (
|
|
id,
|
|
milestone_id,
|
|
impact_type,
|
|
direction,
|
|
amount,
|
|
start_date,
|
|
end_date,
|
|
created_at,
|
|
updated_at
|
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
`, [
|
|
newImpactId,
|
|
newMilestoneId,
|
|
imp.impact_type,
|
|
imp.direction,
|
|
imp.amount,
|
|
imp.start_date || null,
|
|
imp.end_date || null,
|
|
now,
|
|
now
|
|
]);
|
|
}
|
|
|
|
copiesCreated.push(newMilestoneId);
|
|
}
|
|
|
|
return res.json({
|
|
originalId: milestoneId,
|
|
origin_milestone_id: originId,
|
|
copiesCreated
|
|
});
|
|
} catch (err) {
|
|
console.error('Error copying milestone:', err);
|
|
res.status(500).json({ error: 'Failed to copy milestone.' });
|
|
}
|
|
});
|
|
|
|
// 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(`
|
|
SELECT id, user_id, origin_milestone_id
|
|
FROM milestones
|
|
WHERE id = ?
|
|
AND user_id = ?
|
|
`, [milestoneId, req.userId]);
|
|
|
|
if (!existing) {
|
|
return res.status(404).json({ error: 'Milestone not found or not owned by user.' });
|
|
}
|
|
|
|
const originId = existing.origin_milestone_id || existing.id;
|
|
|
|
// 2) Delete all copies referencing that origin
|
|
await db.run(`
|
|
DELETE FROM milestones
|
|
WHERE user_id = ?
|
|
AND origin_milestone_id = ?
|
|
`, [req.userId, 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]);
|
|
|
|
res.json({ message: 'Deleted from all scenarios' });
|
|
} catch (err) {
|
|
console.error('Error deleting milestone from all scenarios:', err);
|
|
res.status(500).json({ error: 'Failed to delete milestone from all scenarios.' });
|
|
}
|
|
});
|
|
|
|
// 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(`
|
|
SELECT id, user_id
|
|
FROM milestones
|
|
WHERE id = ?
|
|
AND user_id = ?
|
|
`, [milestoneId, req.userId]);
|
|
|
|
if (!existing) {
|
|
return res.status(404).json({ error: 'Milestone not found or not owned by user.' });
|
|
}
|
|
|
|
// 2) Delete the single row
|
|
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);
|
|
res.status(500).json({ error: 'Failed to delete milestone.' });
|
|
}
|
|
});
|
|
|
|
|
|
/* ------------------------------------------------------------------
|
|
FINANCIAL PROFILES (Renamed emergency_contribution)
|
|
------------------------------------------------------------------ */
|
|
|
|
app.get('/api/premium/financial-profile', authenticatePremiumUser, async (req, res) => {
|
|
try {
|
|
const row = await db.get(`
|
|
SELECT *
|
|
FROM financial_profiles
|
|
WHERE user_id = ?
|
|
`, [req.userId]);
|
|
|
|
res.json(row || {});
|
|
} catch (error) {
|
|
console.error('Error fetching financial profile:', error);
|
|
res.status(500).json({ error: 'Failed to fetch financial profile' });
|
|
}
|
|
});
|
|
|
|
app.post('/api/premium/financial-profile', authenticatePremiumUser, async (req, res) => {
|
|
const {
|
|
current_salary,
|
|
additional_income,
|
|
monthly_expenses,
|
|
monthly_debt_payments,
|
|
retirement_savings,
|
|
retirement_contribution,
|
|
emergency_fund,
|
|
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
|
|
WHERE user_id = ?
|
|
`, [req.userId]);
|
|
|
|
if (!existing) {
|
|
// Insert new row
|
|
await db.run(`
|
|
INSERT INTO financial_profiles (
|
|
user_id,
|
|
current_salary,
|
|
additional_income,
|
|
monthly_expenses,
|
|
monthly_debt_payments,
|
|
retirement_savings,
|
|
emergency_fund,
|
|
retirement_contribution,
|
|
emergency_contribution,
|
|
extra_cash_emergency_pct,
|
|
extra_cash_retirement_pct,
|
|
created_at,
|
|
updated_at
|
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
|
`, [
|
|
req.userId,
|
|
current_salary || 0,
|
|
additional_income || 0,
|
|
monthly_expenses || 0,
|
|
monthly_debt_payments || 0,
|
|
retirement_savings || 0,
|
|
emergency_fund || 0,
|
|
retirement_contribution || 0,
|
|
emergency_contribution || 0, // store new field
|
|
extra_cash_emergency_pct || 0,
|
|
extra_cash_retirement_pct || 0
|
|
]);
|
|
} else {
|
|
// Update existing
|
|
await db.run(`
|
|
UPDATE financial_profiles
|
|
SET
|
|
current_salary = ?,
|
|
additional_income = ?,
|
|
monthly_expenses = ?,
|
|
monthly_debt_payments = ?,
|
|
retirement_savings = ?,
|
|
emergency_fund = ?,
|
|
retirement_contribution = ?,
|
|
emergency_contribution = ?,
|
|
extra_cash_emergency_pct = ?,
|
|
extra_cash_retirement_pct = ?,
|
|
updated_at = CURRENT_TIMESTAMP
|
|
WHERE user_id = ?
|
|
`, [
|
|
current_salary || 0,
|
|
additional_income || 0,
|
|
monthly_expenses || 0,
|
|
monthly_debt_payments || 0,
|
|
retirement_savings || 0,
|
|
emergency_fund || 0,
|
|
retirement_contribution || 0,
|
|
emergency_contribution || 0, // updated field
|
|
extra_cash_emergency_pct || 0,
|
|
extra_cash_retirement_pct || 0,
|
|
req.userId
|
|
]);
|
|
}
|
|
|
|
res.json({ message: 'Financial profile saved/updated.' });
|
|
} catch (error) {
|
|
console.error('Error saving financial profile:', error);
|
|
res.status(500).json({ error: 'Failed to save financial profile.' });
|
|
}
|
|
});
|
|
|
|
/* ------------------------------------------------------------------
|
|
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;
|
|
|
|
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.' });
|
|
}
|
|
});
|
|
|
|
|
|
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 || {});
|
|
});
|
|
|
|
/* ------------------------------------------------------------------
|
|
FINANCIAL PROJECTIONS
|
|
------------------------------------------------------------------ */
|
|
app.post('/api/premium/financial-projection/:careerPathId', authenticatePremiumUser, async (req, res) => {
|
|
const { careerPathId } = req.params;
|
|
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,
|
|
loan_paid_off_month, final_emergency_savings,
|
|
final_retirement_savings, final_loan_balance,
|
|
created_at, updated_at
|
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
|
`, [
|
|
projectionId,
|
|
req.userId,
|
|
careerPathId,
|
|
JSON.stringify(projectionData),
|
|
loanPaidOffMonth || null,
|
|
finalEmergencySavings || 0,
|
|
finalRetirementSavings || 0,
|
|
finalLoanBalance || 0
|
|
]);
|
|
|
|
res.status(201).json({ message: 'Financial projection saved.', projectionId });
|
|
} catch (error) {
|
|
console.error('Error saving financial projection:', error);
|
|
res.status(500).json({ error: 'Failed to save financial projection.' });
|
|
}
|
|
});
|
|
|
|
app.get('/api/premium/financial-projection/:careerPathId', authenticatePremiumUser, async (req, res) => {
|
|
const { careerPathId } = req.params;
|
|
try {
|
|
const row = await db.get(`
|
|
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 = ?
|
|
ORDER BY created_at DESC
|
|
LIMIT 1
|
|
`, [req.userId, careerPathId]);
|
|
|
|
if (!row) {
|
|
return res.status(404).json({ error: 'Projection not found.' });
|
|
}
|
|
|
|
const parsedProjectionData = JSON.parse(row.projection_data);
|
|
res.status(200).json({
|
|
projectionData: parsedProjectionData,
|
|
loanPaidOffMonth: row.loan_paid_off_month,
|
|
finalEmergencySavings: row.final_emergency_savings,
|
|
finalRetirementSavings: row.final_retirement_savings,
|
|
finalLoanBalance: row.final_loan_balance
|
|
});
|
|
} catch (error) {
|
|
console.error('Error fetching financial projection:', error);
|
|
res.status(500).json({ error: 'Failed to fetch financial projection.' });
|
|
}
|
|
});
|
|
|
|
|
|
// POST create a new task
|
|
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',
|
|
details: { milestone_id, title }
|
|
});
|
|
}
|
|
|
|
// Confirm milestone is owned by this user
|
|
const milestone = await db.get(`
|
|
SELECT user_id
|
|
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.' });
|
|
}
|
|
|
|
const taskId = uuidv4();
|
|
const now = new Date().toISOString();
|
|
|
|
// Insert the new task
|
|
await db.run(`
|
|
INSERT INTO tasks (
|
|
id,
|
|
milestone_id,
|
|
user_id,
|
|
title,
|
|
description,
|
|
due_date,
|
|
status,
|
|
created_at,
|
|
updated_at
|
|
) VALUES (?, ?, ?, ?, ?, ?, 'not_started', ?, ?)
|
|
`, [
|
|
taskId,
|
|
milestone_id,
|
|
req.userId,
|
|
title,
|
|
description || '',
|
|
due_date || null,
|
|
now,
|
|
now
|
|
]);
|
|
|
|
// Return the newly created task as JSON
|
|
const newTask = {
|
|
id: taskId,
|
|
milestone_id,
|
|
user_id: req.userId,
|
|
title,
|
|
description: description || '',
|
|
due_date: due_date || null,
|
|
status: 'not_started'
|
|
};
|
|
|
|
res.status(201).json(newTask);
|
|
} catch (err) {
|
|
console.error('Error creating task:', err);
|
|
res.status(500).json({ error: 'Failed to create task.' });
|
|
}
|
|
});
|
|
|
|
// GET tasks for a milestone
|
|
app.get('/api/premium/milestones', authenticatePremiumUser, async (req, res) => {
|
|
const { careerPathId } = req.query;
|
|
|
|
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]);
|
|
|
|
// 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);
|
|
|
|
// 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;
|
|
}, {});
|
|
}
|
|
|
|
// 3. Attach tasks to each milestone object
|
|
const milestonesWithTasks = milestones.map(m => ({
|
|
...m,
|
|
tasks: tasksByMilestone[m.id] || []
|
|
}));
|
|
|
|
res.json({ milestones: milestonesWithTasks });
|
|
} catch (err) {
|
|
console.error('Error fetching milestones with tasks:', err);
|
|
res.status(500).json({ error: 'Failed to fetch milestones.' });
|
|
}
|
|
});
|
|
|
|
/************************************************************************
|
|
* 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.' });
|
|
}
|
|
|
|
// Verify the milestone belongs to this user
|
|
const milestoneRow = await db.get(`
|
|
SELECT 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.' });
|
|
}
|
|
|
|
// Fetch all impacts for that milestone
|
|
const impacts = await db.all(`
|
|
SELECT
|
|
id,
|
|
milestone_id,
|
|
impact_type,
|
|
direction,
|
|
amount,
|
|
start_date,
|
|
end_date,
|
|
created_at,
|
|
updated_at
|
|
FROM milestone_impacts
|
|
WHERE milestone_id = ?
|
|
ORDER BY created_at ASC
|
|
`, [milestone_id]);
|
|
|
|
res.json({ impacts });
|
|
} catch (err) {
|
|
console.error('Error fetching milestone impacts:', err);
|
|
res.status(500).json({ error: 'Failed to fetch milestone impacts.' });
|
|
}
|
|
});
|
|
|
|
app.post('/api/premium/milestone-impacts', authenticatePremiumUser, async (req, res) => {
|
|
try {
|
|
const {
|
|
milestone_id,
|
|
impact_type,
|
|
direction = 'subtract',
|
|
amount = 0,
|
|
start_date = null,
|
|
end_date = null,
|
|
created_at,
|
|
updated_at
|
|
} = req.body;
|
|
|
|
// Basic checks
|
|
if (!milestone_id || !impact_type) {
|
|
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
|
|
FROM milestones
|
|
WHERE id = ?
|
|
`, [milestone_id]);
|
|
if (!milestoneRow || milestoneRow.user_id !== req.userId) {
|
|
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,
|
|
milestone_id,
|
|
impact_type,
|
|
direction,
|
|
amount,
|
|
start_date,
|
|
end_date,
|
|
created_at,
|
|
updated_at
|
|
)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
`, [
|
|
newUUID,
|
|
milestone_id,
|
|
impact_type,
|
|
direction,
|
|
amount,
|
|
start_date,
|
|
end_date,
|
|
finalCreated,
|
|
finalUpdated
|
|
]);
|
|
|
|
// Fetch & return the inserted row
|
|
const insertedRow = await db.get(`
|
|
SELECT
|
|
id,
|
|
milestone_id,
|
|
impact_type,
|
|
direction,
|
|
amount,
|
|
start_date,
|
|
end_date,
|
|
created_at,
|
|
updated_at
|
|
FROM milestone_impacts
|
|
WHERE id = ?
|
|
`, [newUUID]);
|
|
|
|
return res.status(201).json(insertedRow);
|
|
} catch (err) {
|
|
console.error('Error creating milestone impact:', err);
|
|
return res.status(500).json({ error: 'Failed to create milestone impact.' });
|
|
}
|
|
});
|
|
|
|
/************************************************************************
|
|
* UPDATE an existing milestone impact (PUT)
|
|
************************************************************************/
|
|
app.put('/api/premium/milestone-impacts/:impactId', authenticatePremiumUser, async (req, res) => {
|
|
try {
|
|
const { impactId } = req.params;
|
|
const {
|
|
milestone_id,
|
|
impact_type,
|
|
direction = 'subtract',
|
|
amount = 0,
|
|
start_date = null,
|
|
end_date = null
|
|
} = req.body;
|
|
|
|
// 1) Check this impact belongs to user
|
|
const existing = await db.get(`
|
|
SELECT mi.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.' });
|
|
}
|
|
|
|
const now = new Date().toISOString();
|
|
|
|
// 2) Update
|
|
await db.run(`
|
|
UPDATE milestone_impacts
|
|
SET
|
|
milestone_id = ?,
|
|
impact_type = ?,
|
|
direction = ?,
|
|
amount = ?,
|
|
start_date = ?,
|
|
end_date = ?,
|
|
updated_at = ?
|
|
WHERE id = ?
|
|
`, [
|
|
milestone_id,
|
|
impact_type,
|
|
direction,
|
|
amount,
|
|
start_date,
|
|
end_date,
|
|
now,
|
|
impactId
|
|
]);
|
|
|
|
// 3) Return updated
|
|
const updatedRow = await db.get(`
|
|
SELECT
|
|
id,
|
|
milestone_id,
|
|
impact_type,
|
|
direction,
|
|
amount,
|
|
start_date,
|
|
end_date,
|
|
created_at,
|
|
updated_at
|
|
FROM milestone_impacts
|
|
WHERE id = ?
|
|
`, [impactId]);
|
|
|
|
res.json(updatedRow);
|
|
} catch (err) {
|
|
console.error('Error updating milestone impact:', err);
|
|
res.status(500).json({ error: 'Failed to update 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
|
|
const existing = await db.get(`
|
|
SELECT mi.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.' });
|
|
}
|
|
|
|
// 2) Delete
|
|
await db.run(`
|
|
DELETE FROM milestone_impacts
|
|
WHERE id = ?
|
|
`, [impactId]);
|
|
|
|
res.json({ message: 'Impact deleted successfully.' });
|
|
} catch (err) {
|
|
console.error('Error deleting milestone impact:', err);
|
|
res.status(500).json({ error: 'Failed to delete milestone impact.' });
|
|
}
|
|
});
|
|
|
|
app.use((req, res) => {
|
|
console.warn(`No route matched for ${req.method} ${req.originalUrl}`);
|
|
res.status(404).json({ error: 'Not found' });
|
|
});
|
|
|
|
app.listen(PORT, () => {
|
|
console.log(`Premium server running on http://localhost:${PORT}`);
|
|
});
|