From f1afcbd785a2cbe4e2ac683ccae79df70496c671 Mon Sep 17 00:00:00 2001
From: nicoagostini
Date: Tue, 20 May 2025 14:25:27 -0700
Subject: [PATCH] refactor/ new plan as a modal pop up. Deletion and edition on
plan created
---
src/public/scripts/planManager.js | 39 ++++++++
src/router/user.js | 145 +++++++++++++++++++++++++-----
src/util/calculations.js | 27 ++++--
src/views/editPlan.ejs | 102 +++++++++++++++++++++
src/views/newPlan.ejs | 41 ---------
src/views/planDetail.ejs | 6 ++
src/views/plans.ejs | 127 +++++++++++++++++++++++++-
7 files changed, 414 insertions(+), 73 deletions(-)
create mode 100644 src/public/scripts/planManager.js
create mode 100644 src/views/editPlan.ejs
delete mode 100644 src/views/newPlan.ejs
diff --git a/src/public/scripts/planManager.js b/src/public/scripts/planManager.js
new file mode 100644
index 0000000..a1d4bec
--- /dev/null
+++ b/src/public/scripts/planManager.js
@@ -0,0 +1,39 @@
+function editPlan(planId) {
+ if (planId) {
+ window.location.href = `/plans/${planId}/edit`;
+ } else {
+ console.error('editPlan called without a planId');
+ alert('Cannot edit plan: Plan ID is missing.');
+ }
+}
+
+async function deletePlan(planId) {
+ if (!planId) {
+ console.error('deletePlan called without a planId');
+ alert('Cannot delete plan: Plan ID is missing.');
+ return;
+ }
+
+ if (confirm('Are you sure you want to delete this plan? This action cannot be undone.')) {
+ try {
+ const response = await fetch(`/plans/${planId}`, {
+ method: 'DELETE',
+ headers: {
+ 'Content-Type': 'application/json'
+ }
+ });
+
+ const result = await response.json();
+
+ if (response.ok && result.success) {
+ alert('Plan deleted successfully.');
+ window.location.href = '/plans'; // Redirect to the plans list page
+ } else {
+ alert(`Failed to delete plan: ${result.message || 'Unknown error'}`);
+ }
+ } catch (error) {
+ console.error('Error deleting plan:', error);
+ alert('An error occurred while trying to delete the plan. Please check the console for details and ensure the server is running.');
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/router/user.js b/src/router/user.js
index 7a25e91..3f95a73 100644
--- a/src/router/user.js
+++ b/src/router/user.js
@@ -1,5 +1,5 @@
const getRates = require("../util/exchangeRate");
-const { calculateProgress, updatePlanProgressInDB } = require("../util/calculations");
+const { calculateProgress, updatePlanProgressInDB, updateProgress } = require("../util/calculations");
const suggestions = require("../util/suggestions");
const status = require("../util/statuses");
const ObjectId = require('mongodb').ObjectId;
@@ -118,16 +118,9 @@ module.exports = (middleware, users, plans, assets) => {
router.get('/plans', async (req, res) => {
try {
- const userPlansFromDB = await plans.find({ userId: new ObjectId(req.session.userId) }).toArray();
-
- for (const plan of userPlansFromDB) {
- const progress = await calculateProgress(plan, assets, users, req.session.user._id);
- await updatePlanProgressInDB(plan._id, progress.percentage, plans);
- }
-
- const updatedUserPlans = await plans.find({ userId: new ObjectId(req.session.user._id) }).toArray();
-
+ const updatedUserPlans = await updateProgress(plans, assets, users, req.session.user._id);
+
res.render('plans', {
user: req.session.user,
plans: updatedUserPlans,
@@ -150,23 +143,21 @@ module.exports = (middleware, users, plans, assets) => {
req.session.errMessage = "Invalid plan ID format.";
return res.status(status.BadRequest).redirect('/plans');
}
-
const plan = await plans.findOne({ userId: new ObjectId(req.session.userId), _id: new ObjectId(planId) });
if (!plan) {
- console.log(`Plan not found with ID: ${planId} for user: ${req.session.userId}`);
req.session.errMessage = "Plan not found or you do not have permission to view it.";
return res.status(status.NotFound).redirect('/plans');
}
- const progress = await calculateProgress(plan, assets, users, req.session.userId);
+ const progress = await calculateProgress(plan, assets, users, req.session.userId);
res.render('planDetail', {
user: req.session.user,
- plan: plan,
+ plan: plan,
geoData: req.session.geoData,
assets: userAssets,
- progress: progress,
+ progress: progress,
suggestions: await suggestions.generateSuggestions(),
});
@@ -205,29 +196,134 @@ module.exports = (middleware, users, plans, assets) => {
if (error) {
console.error("Plan validation error:", error.details);
- req.session.errMessage = "Invalid input: " + error.details.map(d => d.message.replace(/"/g, '')).join(', ');
- res.status(status.BadRequest).redirect("/newPlan");
+ const errorMessage = "Invalid input: " + error.details.map(d => d.message.replace(/"/g, '')).join(', ');
+ res.status(status.BadRequest).json({ success: false, message: errorMessage });
return;
}
- const newPlan = {
+
+ const planToInsert = {
userId: new ObjectId(req.session.userId),
name: value.name,
retirementAge: value.retirementAge,
retirementExpenses: value.retirementExpenses,
retirementAssets: value.retirementAssets,
retirementLiabilities: value.retirementLiabilities,
- progress: "0"
+ progress: "0"
};
try {
- await plans.insertOne({ userId: new ObjectId(req.session.userId), ...newPlan });
- req.session.errMessage = "";
- res.redirect('/plans');
+ await plans.insertOne(planToInsert);
+ res.status(status.Ok).json({ success: true, message: "Plan created successfully." });
}
catch (err) {
console.error("Error saving plan:", err);
- req.session.errMessage = "An error occurred while saving your plan. Please try again.";
- res.status(status.InternalServerError).redirect("/newPlan");
+ res.status(status.InternalServerError).json({ success: false, message: "An error occurred while saving your plan. Please try again." });
+ }
+ });
+
+ router.delete('/plans/:id', async (req, res) => {
+ const planId = req.params.id;
+ const plan = await plans.findOne({ _id: new ObjectId(planId) });
+ if(!plan) {
+ console.log(`Plan not found with ID: ${planId}`);
+ res.status(status.NotFound).json({ success: false, message: "Plan not found or you do not have permission to delete it." });
+ return;
+ }
+ if(plan.userId.toString() !== req.session.userId) {
+ console.log(`User ${req.session.userId} does not have permission to delete plan ${planId}`);
+ res.status(status.Forbidden).json({ success: false, message: "You do not have permission to delete this plan." });
+ return;
+ }
+ try {
+ await plans.deleteOne({ _id: new ObjectId(planId) });
+ res.status(status.Ok).json({ success: true, message: "Plan deleted successfully." });
+ }
+ catch (err) {
+ console.error("Error deleting plan:", err);
+ res.status(status.InternalServerError).json({ success: false, message: "An error occurred while deleting your plan. Please try again." });
+ }
+ });
+
+ router.get('/plans/:id/edit', async (req, res) => {
+ const planId = req.params.id;
+ const plan = await plans.findOne({ _id: new ObjectId(planId) });
+ if(!plan) {
+ console.log(`Plan not found with ID: ${planId}`);
+ res.status(status.NotFound).json({ success: false, message: "Plan not found or you do not have permission to edit it." });
+ return;
+ }
+ if(plan.userId.toString() !== req.session.userId) {
+ console.log(`User ${req.session.userId} does not have permission to edit plan ${planId}`);
+ res.status(status.Forbidden).json({ success: false, message: "You do not have permission to edit this plan." });
+ return;
+ }
+ res.render('editPlan', {
+ user: req.session.user,
+ plan: plan,
+ geoData: req.session.geoData
+ });
+ });
+
+ router.post('/plans/:id/edit', async (req, res) => {
+ const planId = req.params.id;
+ const plan = await plans.findOne({ _id: new ObjectId(planId) });
+ if(!plan) {
+ console.log(`Plan not found with ID: ${planId}`);
+ res.status(status.NotFound).json({ success: false, message: "Plan not found or you do not have permission to edit it." });
+ return;
+ }
+ if(plan.userId.toString() !== req.session.userId) {
+ console.log(`User ${req.session.userId} does not have permission to edit plan ${planId}`);
+ res.status(status.Forbidden).json({ success: false, message: "You do not have permission to edit this plan." });
+ return;
+ }
+
+ const planSchema = joi.object({
+ name: joi.string().min(3).max(100).required(),
+ retirementAge: joi.number().min(18).max(120).required(),
+ retirementExpenses: joi.number().min(0).required(),
+ retirementAssets: joi.number().min(0).required(),
+ retirementLiabilities: joi.number().min(0).required(),
+ });
+
+ // Object for Joi validation - only fields from req.body
+ const dataToValidate = {
+ name: req.body.name,
+ retirementAge: Number(req.body.retirementAge),
+ retirementExpenses: parseFloat(req.body.retirementExpenses),
+ retirementAssets: parseFloat(req.body.retirementAssets),
+ retirementLiabilities: parseFloat(req.body.retirementLiabilities),
+ };
+
+ const { error, value } = planSchema.validate(dataToValidate);
+ if (error) {
+ console.error("Plan validation error:", error.details);
+ const errorMessage = "Invalid input: " + error.details.map(d => d.message.replace(/"/g, '')).join(', ');
+ res.status(status.BadRequest).json({ success: false, message: errorMessage });
+ return;
+ }
+
+ const progressCalculated = await calculateProgress(value, assets, users, req.session.userId);
+
+ const planToSet = {
+ name: value.name,
+ retirementAge: value.retirementAge,
+ retirementExpenses: value.retirementExpenses,
+ retirementAssets: value.retirementAssets,
+ retirementLiabilities: value.retirementLiabilities,
+ progress: progressCalculated.percentage,
+ };
+ try {
+ await plans.updateOne(
+ { _id: new ObjectId(planId), userId: new ObjectId(req.session.userId) },
+ { $set: planToSet }
+ );
+ await updateProgress(plans, assets, users, req.session.user._id);
+ res.status(status.Ok).json({ success: true, message: "Plan updated successfully." });
+ }
+ catch (err) {
+ console.error("Error updating plan:", err);
+ res.status(status.InternalServerError).json({ success: false, message: "An error occurred while updating your plan. Please try again." });
}
});
@@ -415,6 +511,7 @@ module.exports = (middleware, users, plans, assets) => {
return res.status(status.Ok).redirect("/profile");
});
+
}).catch(err => {
console.error("Error updating account in database:", err);
req.session.errMessage = "An error occurred while saving your information. Please try again.";
diff --git a/src/util/calculations.js b/src/util/calculations.js
index 19d3222..058e017 100644
--- a/src/util/calculations.js
+++ b/src/util/calculations.js
@@ -67,15 +67,17 @@ async function calculateProgress(plan, assets, users, userId) {
const totalUserPlanValue = plan.retirementAssets;
const userDoc = await users.findOne({ _id: new ObjectId(userId) });
+
if (!userDoc || !userDoc.dob) {
- console.error("User document or DOB not found for userId:", userId);
- return { monthlyInvestment: NaN, totalCostOfRetirement: NaN, monthsUntilRetirement: NaN, yearsRetired: NaN, percentage: NaN };
+ console.error("[calculateProgress] User document or DOB not found for userId:", userId);
+ // Ensure a structured return even on error to avoid undefined.progress issues
+ return { monthlyInvestment: NaN, totalCostOfRetirement: NaN, monthsUntilRetirement: NaN, yearsRetired: NaN, yearsUntilRetirement: NaN, percentage: NaN };
}
const userDob = new Date(userDoc.dob);
if (isNaN(userDob.getTime())) {
- console.error("userDob is an invalid date. Aborting calculation.");
- return { monthlyInvestment: NaN, totalCostOfRetirement: NaN, monthsUntilRetirement: NaN, yearsRetired: NaN, percentage: NaN };
+ console.error("[calculateProgress] userDob is an invalid date. Aborting calculation.");
+ return { monthlyInvestment: NaN, totalCostOfRetirement: NaN, monthsUntilRetirement: NaN, yearsRetired: NaN, yearsUntilRetirement: NaN, percentage: NaN };
}
const today = new Date();
@@ -104,10 +106,23 @@ async function calculateProgress(plan, assets, users, userId) {
} catch (err) {
console.error("Error in calculateProgress:", err);
- return;
+ // Ensure a structured return even on error to avoid undefined.progress issues
+ return { monthlyInvestment: NaN, totalCostOfRetirement: NaN, monthsUntilRetirement: NaN, yearsRetired: NaN, yearsUntilRetirement: NaN, percentage: NaN };
}
}
+
+async function updateProgress(plans, assets, users, userId) {
+ const userPlansFromDB = await plans.find({ userId: new ObjectId(userId) }).toArray();
+ for (const plan of userPlansFromDB) {
+ const progress = await calculateProgress(plan, assets, users, userId);
+ await updatePlanProgressInDB(plan._id, progress.percentage, plans);
+ }
+
+ const updatedUserPlans = await plans.find({ userId: new ObjectId(userId) }).toArray();
+ return updatedUserPlans;
+ }
-module.exports = { calculatePlanProgress, updatePlanProgressInDB, calculateProgress};
\ No newline at end of file
+
+module.exports = { calculatePlanProgress, updatePlanProgressInDB, calculateProgress, updateProgress};
\ No newline at end of file
diff --git a/src/views/editPlan.ejs b/src/views/editPlan.ejs
new file mode 100644
index 0000000..ed59a4b
--- /dev/null
+++ b/src/views/editPlan.ejs
@@ -0,0 +1,102 @@
+<%- include('./partials/fileHeader') %>
+<%- include('./partials/header') %>
+
+
+
+
Edit Plan: <%= plan.name %>
+
+
+
+
+
+
+
+<%- include('./partials/navBar') %>
+<%- include('./partials/scriptLoader') %>
+<%- include('./partials/footer') %>
\ No newline at end of file
diff --git a/src/views/newPlan.ejs b/src/views/newPlan.ejs
deleted file mode 100644
index d337639..0000000
--- a/src/views/newPlan.ejs
+++ /dev/null
@@ -1,41 +0,0 @@
-<%- include("./partials/fileHeader") %>
-<%- include("./partials/header") %>
-
-
-
-
-
-<%- include("./partials/navBar") %>
-<%- include("./partials/scriptLoader") %>
-<%- include("./partials/footer") %>
\ No newline at end of file
diff --git a/src/views/planDetail.ejs b/src/views/planDetail.ejs
index 5fbacc8..b1fc808 100644
--- a/src/views/planDetail.ejs
+++ b/src/views/planDetail.ejs
@@ -87,10 +87,16 @@
+
+
+
+
+
+
<%- include("./partials/navBar") %>
<%- include("./partials/scriptLoader") %>
<%- include("./partials/footer") %>
diff --git a/src/views/plans.ejs b/src/views/plans.ejs
index adfc5ee..071a6f1 100644
--- a/src/views/plans.ejs
+++ b/src/views/plans.ejs
@@ -4,7 +4,8 @@
-
+
+
+
<%- include("./partials/navBar") %>
<%- include("./partials/scriptLoader") %>
+
<%- include("./partials/footer") %>