From de99301738486197cfc78d9c0059180ac19fdd9a Mon Sep 17 00:00:00 2001 From: Joaquin Date: Tue, 20 May 2025 11:16:55 -0700 Subject: [PATCH 01/36] fix/fixed background of password reset --- src/views/forgotPass.ejs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/views/forgotPass.ejs b/src/views/forgotPass.ejs index a7a656c..e18ae3c 100644 --- a/src/views/forgotPass.ejs +++ b/src/views/forgotPass.ejs @@ -1,7 +1,7 @@ <%- include("./partials/fileHeader") %> <%- include("./partials/headerStart") %> -
+
From ef858bcf00cb45c19114bd4ac9399631e6cb658c Mon Sep 17 00:00:00 2001 From: knighthawk4227 Date: Tue, 20 May 2025 12:57:17 -0700 Subject: [PATCH 02/36] no plan form/ check --- src/auth/forgotPass.js | 13 ++++++++++--- src/public/images/wallet.png | Bin 0 -> 38081 bytes src/views/dashboard.ejs | 20 +++++++++++++++++++- 3 files changed, 29 insertions(+), 4 deletions(-) create mode 100644 src/public/images/wallet.png diff --git a/src/auth/forgotPass.js b/src/auth/forgotPass.js index 424c633..c78aa80 100644 --- a/src/auth/forgotPass.js +++ b/src/auth/forgotPass.js @@ -3,7 +3,9 @@ const crypto = require('crypto'); const joi = require('joi'); const nodeMail = require('nodemailer'); const bcrypt = require('bcrypt'); +const path = require('path'); require('dotenv').config(); +const PATH = path.join(__dirname, '..', 'public', 'images', 'wallet.png'); const PORT = process.env.PORT; @@ -40,7 +42,7 @@ module.exports = (users) => { const token = crypto.randomBytes(32).toString('hex'); console.log(`The reset token is ${token}`) - const expiration = Date.now() + 360000; + const expiration = Date.now() + 3600000; await users.updateOne({ email }, { $set: { resetToken: token, resetTokenExpires: expiration } @@ -52,8 +54,13 @@ module.exports = (users) => { from: process.env.EMAIL_USER, to: email, subject: 'Password reset', - text: `reset your password here ${resetUrl} this link will expire within 1 hour`, - + text: `reset your password here ${resetUrl} this link will expire within 1 hour, \n Do not share this link with anyone + \n \n Thankyou, The RCalculator team.`, + html: ` +

Reset your password here. This link will expire in 1 hour.

+

wallet icon

+

Thank you,
The RCalculator team

`, + }; try { diff --git a/src/public/images/wallet.png b/src/public/images/wallet.png new file mode 100644 index 0000000000000000000000000000000000000000..054652bdb3f67e4b573dd43ac415d0c9de93eb43 GIT binary patch literal 38081 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 literal 0 HcmV?d00001 diff --git a/src/views/dashboard.ejs b/src/views/dashboard.ejs index d306208..a8956e6 100644 --- a/src/views/dashboard.ejs +++ b/src/views/dashboard.ejs @@ -13,9 +13,27 @@
- <% plans.forEach(plan=> { %> + + <% if (plans.length === 0) { %> + + + <% } %> + <% plans.forEach(plan=> { %> <%- include('./partials/dashboardBox.ejs', {thing: plan}) %> <% }); %> +
From 50bd21b164fca494ee0c70077669f636305295af Mon Sep 17 00:00:00 2001 From: Joaquin Date: Tue, 20 May 2025 14:05:34 -0700 Subject: [PATCH 03/36] fix/user info is now kept on form reset --- app.js | 11 +++++++++++ src/auth/authentication.js | 4 ++-- src/views/signup.ejs | 4 ++-- 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/app.js b/app.js index 1e3c914..926be77 100644 --- a/app.js +++ b/app.js @@ -60,6 +60,17 @@ app.get('/signup', (req, res) => { return res.status(status.Ok); }); +app.get('/signup/:name/:email', (req, res) => { + const ignore = ["User not found", "Incorrect password"]; + if (ignore.includes(req.session.errMessage)) req.session.errMessage = ""; + res.render('signup', { + errMessage: req.session.errMessage, + name: req.params.name, + email: req.params.email + }); + return res.status(status.Ok); +}); + app.get('/login', (req, res) => { if (req.session.authenticated) { res.redirect("/home"); diff --git a/src/auth/authentication.js b/src/auth/authentication.js index f70635b..2d0ff1f 100644 --- a/src/auth/authentication.js +++ b/src/auth/authentication.js @@ -96,7 +96,7 @@ module.exports = (users) => { if (req.body.password != req.body.repassword) { req.session.errMessage = "Passwords must match"; res.status(status.BadRequest); - return res.redirect("/signup"); + return res.redirect(`/signup/${req.body.name}/${req.body.email}`); } let strength = passwordStrength(req.body.password); @@ -104,7 +104,7 @@ module.exports = (users) => { if (strength.id < 2) { req.session.errMessage = `Password ${strength.value}`; res.status(status.BadRequest); - return res.redirect("/signup"); + return res.redirect(`/signup/${req.body.name}/${req.body.email}`); } let hashedPassword = await bcrypt.hashSync(req.body.password, salt); diff --git a/src/views/signup.ejs b/src/views/signup.ejs index 212f2a8..d79c1f9 100644 --- a/src/views/signup.ejs +++ b/src/views/signup.ejs @@ -11,13 +11,13 @@ + placeholder="Enter Name" value="<%= typeof name != 'undefined' ? name : '' %>"/>
+ placeholder="Enter Email" value="<%= typeof email != 'undefined' ? email : '' %>"/>
From f1afcbd785a2cbe4e2ac683ccae79df70496c671 Mon Sep 17 00:00:00 2001 From: nicoagostini Date: Tue, 20 May 2025 14:25:27 -0700 Subject: [PATCH 04/36] 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 %>

+ + +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ + + +
+ + Cancel + + +
+ +
+
+ + + +<%- 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") %> - -
-
-

Retirement Plan

-
-
- - -
-
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- -
-
-
-
- -<%- 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 @@

My Retirement Plans

-
New Plan
+ +
<% plans.forEach(plan => { %> @@ -16,9 +17,131 @@ <% }) %>
- + + + + +
+

Create New Retirement Plan

+
+ +
+
+ + +
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ + +
+ + +
+
+
+
<%- include("./partials/navBar") %> <%- include("./partials/scriptLoader") %> + <%- include("./partials/footer") %> From 436e242cf190f47813660b2032597ec366f5f486 Mon Sep 17 00:00:00 2001 From: knighthawk4227 Date: Tue, 20 May 2025 14:25:48 -0700 Subject: [PATCH 05/36] message if no plans --- app.js | 7 +++++-- src/auth/forgotPass.js | 14 ++++++++++---- src/views/dashboard.ejs | 21 +++++++++++++++++++-- src/views/partials/dashboardBox.ejs | 6 +++--- 4 files changed, 37 insertions(+), 11 deletions(-) diff --git a/app.js b/app.js index 926be77..831e82d 100644 --- a/app.js +++ b/app.js @@ -54,9 +54,12 @@ app.get('/', (req, res) => { }); app.get('/signup', (req, res) => { + let error = req.session.errMessage; + delete req.session.errMessage; + const ignore = ["User not found", "Incorrect password"]; - if (ignore.includes(req.session.errMessage)) req.session.errMessage = ""; - res.render('signup', { errMessage: req.session.errMessage }); + if (ignore.includes(error)) error = ""; + res.render('signup', { errMessage: error }); return res.status(status.Ok); }); diff --git a/src/auth/forgotPass.js b/src/auth/forgotPass.js index 424c633..fa97700 100644 --- a/src/auth/forgotPass.js +++ b/src/auth/forgotPass.js @@ -2,8 +2,9 @@ const express = require('express'); const crypto = require('crypto'); const joi = require('joi'); const nodeMail = require('nodemailer'); -const bcrypt = require('bcrypt'); +const path = require('path'); require('dotenv').config(); +const PATH = path.join(__dirname, '..', 'public', 'images', 'wallet.png'); const PORT = process.env.PORT; @@ -40,7 +41,7 @@ module.exports = (users) => { const token = crypto.randomBytes(32).toString('hex'); console.log(`The reset token is ${token}`) - const expiration = Date.now() + 360000; + const expiration = Date.now() + 3600000; await users.updateOne({ email }, { $set: { resetToken: token, resetTokenExpires: expiration } @@ -52,8 +53,13 @@ module.exports = (users) => { from: process.env.EMAIL_USER, to: email, subject: 'Password reset', - text: `reset your password here ${resetUrl} this link will expire within 1 hour`, - + text: `reset your password here ${resetUrl} this link will expire within 1 hour, \n Do not share this link with anyone + \n \n Thankyou, The RCalculator team.`, + html: ` +

Reset your password here. This link will expire in 1 hour.

+

wallet icon

+

Thank you,
The RCalculator team

`, + }; try { diff --git a/src/views/dashboard.ejs b/src/views/dashboard.ejs index d306208..151341f 100644 --- a/src/views/dashboard.ejs +++ b/src/views/dashboard.ejs @@ -13,9 +13,26 @@
- <% plans.forEach(plan=> { %> + <% if (plans.length === 0) { %> +
+
+

Don't have any retirement plans?

+
+
+ Make one +
+
+
+
+ + <% } else %> + <% plans.forEach(plan=> { %> <%- include('./partials/dashboardBox.ejs', {thing: plan}) %> - <% }); %> + <% }); %> +
diff --git a/src/views/partials/dashboardBox.ejs b/src/views/partials/dashboardBox.ejs index 6e9fc0c..29f4b1c 100644 --- a/src/views/partials/dashboardBox.ejs +++ b/src/views/partials/dashboardBox.ejs @@ -5,10 +5,10 @@ class="relative flex flex-col bg-white text-gray-700 shadow-md rounded-xl w-full h-full overflow-hidden min-h-[175px]">
-

+

<%= thing.name %>

-

+

<%= thing.retirementAssets %>

@@ -17,7 +17,7 @@
-

+

Progress:

From 08397a1c8911ebf1f13430019545c5cc0a672cc0 Mon Sep 17 00:00:00 2001 From: nicoagostini Date: Tue, 20 May 2025 14:56:03 -0700 Subject: [PATCH 06/36] refactor/edit plan 'reset' button added --- src/views/editPlan.ejs | 33 +++++++++++++++++++++++++++++---- 1 file changed, 29 insertions(+), 4 deletions(-) diff --git a/src/views/editPlan.ejs b/src/views/editPlan.ejs index ed59a4b..6132efe 100644 --- a/src/views/editPlan.ejs +++ b/src/views/editPlan.ejs @@ -38,14 +38,17 @@

-
- - Cancel - +
+ + + Cancel +
@@ -57,6 +60,28 @@ const form = document.getElementById('editPlanForm'); const errorMessagesDiv = document.getElementById('formErrorMessages'); + // Store initial form values + const initialValues = { + name: form.name.value, + retirementAge: form.retirementAge.value, + retirementExpenses: form.retirementExpenses.value, + retirementAssets: form.retirementAssets.value, + retirementLiabilities: form.retirementLiabilities.value + }; + + const resetButton = document.getElementById('resetButton'); + resetButton.addEventListener('click', function() { + form.name.value = initialValues.name; + form.retirementAge.value = initialValues.retirementAge; + form.retirementExpenses.value = initialValues.retirementExpenses; + form.retirementAssets.value = initialValues.retirementAssets; + form.retirementLiabilities.value = initialValues.retirementLiabilities; + if (errorMessagesDiv) { + errorMessagesDiv.style.display = 'none'; // Hide error message on reset + errorMessagesDiv.textContent = ''; + } + }); + if (form) { form.addEventListener('submit', async function(event) { event.preventDefault(); // Prevent traditional form submission From c0174c2b228a4a8e39c002112b987dfcbee0b251 Mon Sep 17 00:00:00 2001 From: nicoagostini Date: Tue, 20 May 2025 16:45:49 -0700 Subject: [PATCH 07/36] fix/ now assets display the location currency OR CAD as default --- src/views/assets.ejs | 2 +- src/views/partials/assetPreview.ejs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/views/assets.ejs b/src/views/assets.ejs index 5397a5f..691e64b 100644 --- a/src/views/assets.ejs +++ b/src/views/assets.ejs @@ -19,7 +19,7 @@

Total Value:

- $<%= assets.reduce((total, e) => total + e.value, 0).toLocaleString(undefined, {minimumFractionDigits: 2, maximumFractionDigits: 2}) %> + <%- new Intl.NumberFormat('en-US', { style: 'currency', currency: geoData.currency ? geoData.currency : 'CAD' }).format(assets.reduce((total, e) => total + e.value, 0)) %>

diff --git a/src/views/partials/assetPreview.ejs b/src/views/partials/assetPreview.ejs index 4d8c588..13c9996 100644 --- a/src/views/partials/assetPreview.ejs +++ b/src/views/partials/assetPreview.ejs @@ -10,7 +10,7 @@

- $<%= asset.value.toLocaleString(undefined, {minimumFractionDigits: 2, maximumFractionDigits: 2}) %> + <%- new Intl.NumberFormat('en-US', { style: 'currency', currency: geoData.currency ? geoData.currency : 'CAD' }).format(asset.value) %>

From 07b8325a14620ab842a6fdf05eece298b2c7fe79 Mon Sep 17 00:00:00 2001 From: nicoagostini Date: Tue, 20 May 2025 20:12:28 -0700 Subject: [PATCH 08/36] fix/required fields are marked --- src/views/editPlan.ejs | 11 ++++++----- src/views/planDetail.ejs | 2 +- src/views/plans.ejs | 11 ++++++----- src/views/questionnaire.ejs | 16 ++++++++-------- 4 files changed, 21 insertions(+), 19 deletions(-) diff --git a/src/views/editPlan.ejs b/src/views/editPlan.ejs index 6132efe..63ae3a7 100644 --- a/src/views/editPlan.ejs +++ b/src/views/editPlan.ejs @@ -4,34 +4,35 @@

Edit Plan: <%= plan.name %>

+

* Indicates a required field

- +
- +
- +
- +
- +
diff --git a/src/views/planDetail.ejs b/src/views/planDetail.ejs index b1fc808..7d8477b 100644 --- a/src/views/planDetail.ejs +++ b/src/views/planDetail.ejs @@ -48,7 +48,7 @@

<%= plan.retirementAge %>

- +

<%- new Intl.NumberFormat('en-US', { style: 'currency', currency: geoData.currency ? geoData.currency : 'CAD' }).format(plan.retirementExpenses) %>

diff --git a/src/views/plans.ejs b/src/views/plans.ejs index 071a6f1..3c1ab89 100644 --- a/src/views/plans.ejs +++ b/src/views/plans.ejs @@ -31,24 +31,25 @@
+

* Indicates a required field

- +
- +
- +
- +
- +
diff --git a/src/views/questionnaire.ejs b/src/views/questionnaire.ejs index 8cf4cd7..1adacb3 100644 --- a/src/views/questionnaire.ejs +++ b/src/views/questionnaire.ejs @@ -2,16 +2,16 @@ <%- include("./partials/header") %>
-

Welcome: <%= user.name %>

Financial Questionnaire

+

* Indicates a required field

- +
- +
- +
- +
- +
- +
- +
From 5b3d5cdce1df2a6490d9c89d90ecda72e510c437 Mon Sep 17 00:00:00 2001 From: Joaquin Date: Wed, 21 May 2025 09:39:13 -0700 Subject: [PATCH 09/36] fix/improved user info retention on signup/login forms --- app.js | 34 ++++++++++++++++++++-------------- src/auth/authentication.js | 8 ++++---- src/views/login.ejs | 2 +- 3 files changed, 25 insertions(+), 19 deletions(-) diff --git a/app.js b/app.js index 831e82d..b1935f5 100644 --- a/app.js +++ b/app.js @@ -56,21 +56,19 @@ app.get('/', (req, res) => { app.get('/signup', (req, res) => { let error = req.session.errMessage; delete req.session.errMessage; - + const ignore = ["User not found", "Incorrect password"]; if (ignore.includes(error)) error = ""; - res.render('signup', { errMessage: error }); - return res.status(status.Ok); -}); - -app.get('/signup/:name/:email', (req, res) => { - const ignore = ["User not found", "Incorrect password"]; - if (ignore.includes(req.session.errMessage)) req.session.errMessage = ""; - res.render('signup', { - errMessage: req.session.errMessage, - name: req.params.name, - email: req.params.email - }); + + if (req.query.name && req.query.email) { + res.render('signup', { + errMessage: error, + name: req.query.name, + email: req.query.email + }); + } else { + res.render('signup', { errMessage: error }); + } return res.status(status.Ok); }); @@ -79,7 +77,15 @@ app.get('/login', (req, res) => { res.redirect("/home"); return res.status(status.Ok); } - res.render('login', { errMessage: req.session.errMessage }); + + if (req.query.email) { + res.render('login', { + errMessage: req.session.errMessage, + email: req.query.email + }); + } else { + res.render('login', { errMessage: req.session.errMessage }); + } return res.status(status.Ok); }); diff --git a/src/auth/authentication.js b/src/auth/authentication.js index 2d0ff1f..d4a8c5e 100644 --- a/src/auth/authentication.js +++ b/src/auth/authentication.js @@ -43,13 +43,13 @@ module.exports = (users) => { if (!user) { req.session.errMessage = "User not found"; res.status(status.NotFound); - return res.redirect("/login"); + return res.redirect(`/login/?email=${req.body.email}`); } if (!bcrypt.compareSync(req.body.password, user.password)) { req.session.errMessage = "Incorrect password"; res.status(status.Unauthorized); - return res.redirect("/login"); + return res.redirect(`/login/?email=${req.body.email}`); } req.session.authenticated = true; @@ -96,7 +96,7 @@ module.exports = (users) => { if (req.body.password != req.body.repassword) { req.session.errMessage = "Passwords must match"; res.status(status.BadRequest); - return res.redirect(`/signup/${req.body.name}/${req.body.email}`); + return res.redirect(`/signup/?name=${req.body.name}&email=${req.body.email}`); } let strength = passwordStrength(req.body.password); @@ -104,7 +104,7 @@ module.exports = (users) => { if (strength.id < 2) { req.session.errMessage = `Password ${strength.value}`; res.status(status.BadRequest); - return res.redirect(`/signup/${req.body.name}/${req.body.email}`); + return res.redirect(`/signup/?name=${req.body.name}&email=${req.body.email}`); } let hashedPassword = await bcrypt.hashSync(req.body.password, salt); diff --git a/src/views/login.ejs b/src/views/login.ejs index e0c99d0..1b41753 100644 --- a/src/views/login.ejs +++ b/src/views/login.ejs @@ -11,7 +11,7 @@ + placeholder="Enter Email" value="<%= typeof email != 'undefined' ? email : '' %>"/>
From 3dc1faaed55d82865c56690adcafffc1cf2ac7ab Mon Sep 17 00:00:00 2001 From: SowinskiBraeden Date: Wed, 21 May 2025 10:47:27 -0700 Subject: [PATCH 10/36] fix/password reset --- src/auth/forgotPass.js | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/auth/forgotPass.js b/src/auth/forgotPass.js index fa97700..9756945 100644 --- a/src/auth/forgotPass.js +++ b/src/auth/forgotPass.js @@ -1,10 +1,10 @@ +const { passwordStrength } = require("check-password-strength"); +const nodeMail = require('nodemailer'); const express = require('express'); +const bcrypt = require('bcrypt'); const crypto = require('crypto'); const joi = require('joi'); -const nodeMail = require('nodemailer'); -const path = require('path'); require('dotenv').config(); -const PATH = path.join(__dirname, '..', 'public', 'images', 'wallet.png'); const PORT = process.env.PORT; @@ -103,6 +103,13 @@ module.exports = (users) => { req.session.error = 'Reset link is invalid.'; return res.redirect(`/reset`); } + + let strength = passwordStrength(password); + if (strength.id < 2) { + req.session.errMessage = `Password ${strength.value}`; + return res.redirect(`/reset/${token}`); + } + const hashPassword = await bcrypt.hash(password, 12); await users.updateOne( From 796dc245b2bbc58eedd5e51a339379f4e4fa3bba Mon Sep 17 00:00:00 2001 From: SowinskiBraeden Date: Wed, 21 May 2025 10:53:34 -0700 Subject: [PATCH 11/36] fix/password reset email body --- src/auth/forgotPass.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/auth/forgotPass.js b/src/auth/forgotPass.js index 9756945..bcf2ea5 100644 --- a/src/auth/forgotPass.js +++ b/src/auth/forgotPass.js @@ -53,13 +53,14 @@ module.exports = (users) => { from: process.env.EMAIL_USER, to: email, subject: 'Password reset', - text: `reset your password here ${resetUrl} this link will expire within 1 hour, \n Do not share this link with anyone + text: `Hi ${user.name},\nA request was sent to reset your password. If this wasn't you, please ignore this email.\nIf you sent the request, reset your password here ${resetUrl} this link will expire within 1 hour, \n Do not share this link with anyone. \n \n Thankyou, The RCalculator team.`, html: ` -

Reset your password here. This link will expire in 1 hour.

+

Hi ${user.name}

+

A request was sent to reset your password. If this wasn't you, please ignore this email.

+

If you sent the request, reset your password here. This link will expire in 1 hour.

wallet icon

Thank you,
The RCalculator team

`, - }; try { From 231aca9ec562279fa721d445a3e6023f9112286e Mon Sep 17 00:00:00 2001 From: Joaquin Date: Wed, 21 May 2025 11:04:08 -0700 Subject: [PATCH 12/36] fix/added required tooltips to some field forms --- src/views/login.ejs | 7 ++++--- src/views/partials/createAsset.ejs | 7 ++++--- src/views/plans.ejs | 4 ++-- src/views/questionnaire.ejs | 2 +- src/views/signup.ejs | 11 ++++++----- 5 files changed, 17 insertions(+), 14 deletions(-) diff --git a/src/views/login.ejs b/src/views/login.ejs index 1b41753..b725df5 100644 --- a/src/views/login.ejs +++ b/src/views/login.ejs @@ -6,15 +6,16 @@

Login

-
+
+

* Indicates a required field

- +
- + diff --git a/src/views/partials/createAsset.ejs b/src/views/partials/createAsset.ejs index b6624c7..4ae30fc 100644 --- a/src/views/partials/createAsset.ejs +++ b/src/views/partials/createAsset.ejs @@ -12,6 +12,7 @@
+

* Indicates a required field

@@ -59,13 +60,13 @@
- + - + - + diff --git a/src/views/plans.ejs b/src/views/plans.ejs index 3c1ab89..fa48b64 100644 --- a/src/views/plans.ejs +++ b/src/views/plans.ejs @@ -21,7 +21,7 @@ -
+

Create New Retirement Plan

@@ -31,7 +31,7 @@
-

* Indicates a required field

+

* Indicates a required field

diff --git a/src/views/questionnaire.ejs b/src/views/questionnaire.ejs index 1adacb3..4370a47 100644 --- a/src/views/questionnaire.ejs +++ b/src/views/questionnaire.ejs @@ -4,7 +4,7 @@

Financial Questionnaire

-

* Indicates a required field

+

* Indicates a required field

diff --git a/src/views/signup.ejs b/src/views/signup.ejs index d79c1f9..1cc34a0 100644 --- a/src/views/signup.ejs +++ b/src/views/signup.ejs @@ -6,25 +6,26 @@

Signup

-
+
+

* Indicates a required field

- +
- +
- + - + From dd9a0a10270bebc18b79b33400df9aa2203a2ca4 Mon Sep 17 00:00:00 2001 From: SowinskiBraeden Date: Wed, 21 May 2025 11:04:42 -0700 Subject: [PATCH 13/36] fix/allow decimal values in assets --- src/views/partials/assetModify.ejs | 4 ++-- src/views/partials/createAsset.ejs | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/views/partials/assetModify.ejs b/src/views/partials/assetModify.ejs index 82059c8..2f3eb96 100644 --- a/src/views/partials/assetModify.ejs +++ b/src/views/partials/assetModify.ejs @@ -87,10 +87,10 @@ <% if (asset.type != "stock") { %> - + <% } else { %> - + diff --git a/src/views/partials/createAsset.ejs b/src/views/partials/createAsset.ejs index b6624c7..7970d6e 100644 --- a/src/views/partials/createAsset.ejs +++ b/src/views/partials/createAsset.ejs @@ -63,7 +63,7 @@ - + @@ -96,7 +96,7 @@ - +
diff --git a/src/views/partials/assetModify.ejs b/src/views/partials/assetModify.ejs index 2f3eb96..6428b1a 100644 --- a/src/views/partials/assetModify.ejs +++ b/src/views/partials/assetModify.ejs @@ -30,7 +30,13 @@
<% if (asset.type == "other") { %>
-
<% } else { %> - <% } %> @@ -70,40 +80,94 @@
- - + + - + <% if (asset.type != "stock") { %> - + <% } else { %> - + <% } %>
<% if (asset.type != "stock") { %> - + <% } else { %> - + - + <% } %> <% if (asset.type == "other") { %> - + <% } %> <% if (asset.type != "saving") { %> - + <% } %>
@@ -111,7 +175,12 @@
-
diff --git a/src/views/partials/assetPreview.ejs b/src/views/partials/assetPreview.ejs index 13c9996..831b45e 100644 --- a/src/views/partials/assetPreview.ejs +++ b/src/views/partials/assetPreview.ejs @@ -1,6 +1,10 @@
- -
\ No newline at end of file +
diff --git a/src/views/partials/createAsset.ejs b/src/views/partials/createAsset.ejs index 7970d6e..0b0434c 100644 --- a/src/views/partials/createAsset.ejs +++ b/src/views/partials/createAsset.ejs @@ -17,15 +17,37 @@
@@ -38,7 +60,12 @@
-
- + - + - + - +
-
\ No newline at end of file + From 5bc26e334eec40bcd4d071b1fde2d30740a0ff20 Mon Sep 17 00:00:00 2001 From: Joaquin Date: Wed, 21 May 2025 11:22:36 -0700 Subject: [PATCH 15/36] fix/fixed input fields on login/signup not being required --- src/views/login.ejs | 4 ++-- src/views/plans.ejs | 6 +++--- src/views/signup.ejs | 8 ++++---- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/views/login.ejs b/src/views/login.ejs index b725df5..7d0e7b2 100644 --- a/src/views/login.ejs +++ b/src/views/login.ejs @@ -10,13 +10,13 @@

* Indicates a required field

-
-
diff --git a/src/views/plans.ejs b/src/views/plans.ejs index fa48b64..c828e8c 100644 --- a/src/views/plans.ejs +++ b/src/views/plans.ejs @@ -42,15 +42,15 @@
- +
- +
- +
diff --git a/src/views/signup.ejs b/src/views/signup.ejs index 1cc34a0..dc50168 100644 --- a/src/views/signup.ejs +++ b/src/views/signup.ejs @@ -10,23 +10,23 @@

* Indicates a required field

-
-
- -
From e50047b26a0b7cc6ed4646ab3409888497f6cd46 Mon Sep 17 00:00:00 2001 From: SowinskiBraeden Date: Wed, 21 May 2025 11:33:26 -0700 Subject: [PATCH 16/36] refactor/add headers to client side scripts --- src/public/scripts/calcExchange.js | 3 -- src/public/scripts/dollarFormat.js | 8 +-- src/public/scripts/editPlanModals.js | 65 ++++++++++++++++++++++ src/public/scripts/footer.js | 1 + src/public/scripts/geolocation.js | 24 +++++++++ src/public/scripts/planManager.js | 10 +++- src/public/scripts/planModals.js | 78 +++++++++++++++++++++++++++ src/views/editPlan.ejs | 68 +---------------------- src/views/plans.ejs | 81 +--------------------------- 9 files changed, 183 insertions(+), 155 deletions(-) delete mode 100644 src/public/scripts/calcExchange.js create mode 100644 src/public/scripts/editPlanModals.js create mode 100644 src/public/scripts/planModals.js diff --git a/src/public/scripts/calcExchange.js b/src/public/scripts/calcExchange.js deleted file mode 100644 index 29956e8..0000000 --- a/src/public/scripts/calcExchange.js +++ /dev/null @@ -1,3 +0,0 @@ -let rate = document.getElementsByClassName("countryButton").value; - -document.getElementById("exchange").innerHTML \ No newline at end of file diff --git a/src/public/scripts/dollarFormat.js b/src/public/scripts/dollarFormat.js index a22f2c5..e962411 100644 --- a/src/public/scripts/dollarFormat.js +++ b/src/public/scripts/dollarFormat.js @@ -1,12 +1,12 @@ + /** * Function takes in a number and formats it to currency - * @param {integer} - * @returns nuber formatted in currency + * @param {number} value + * @returns {string} number formatted in currency */ - document.addEventListener("DOMContentLoaded", () => { const number = document.querySelectorAll(".planGoal"); - number.forEach(value => { + number.forEach((value) => { num = parseFloat(value.textContent); if (num >= 999999) { value.textContent = "$" + (num /= 1000000) + "M"; diff --git a/src/public/scripts/editPlanModals.js b/src/public/scripts/editPlanModals.js new file mode 100644 index 0000000..55f9909 --- /dev/null +++ b/src/public/scripts/editPlanModals.js @@ -0,0 +1,65 @@ +// Basic client-side form submission handler to process JSON response +document.addEventListener('DOMContentLoaded', () => { + const form = document.getElementById('editPlanForm'); + const errorMessagesDiv = document.getElementById('formErrorMessages'); + + // Store initial form values + const initialValues = { + name: form.name.value, + retirementAge: form.retirementAge.value, + retirementExpenses: form.retirementExpenses.value, + retirementAssets: form.retirementAssets.value, + retirementLiabilities: form.retirementLiabilities.value + }; + + const resetButton = document.getElementById('resetButton'); + resetButton.addEventListener('click', function() { + form.name.value = initialValues.name; + form.retirementAge.value = initialValues.retirementAge; + form.retirementExpenses.value = initialValues.retirementExpenses; + form.retirementAssets.value = initialValues.retirementAssets; + form.retirementLiabilities.value = initialValues.retirementLiabilities; + if (errorMessagesDiv) { + errorMessagesDiv.style.display = 'none'; // Hide error message on reset + errorMessagesDiv.textContent = ''; + } + }); + + if (form) { + form.addEventListener('submit', async function(event) { + event.preventDefault(); // Prevent traditional form submission + errorMessagesDiv.textContent = ''; + errorMessagesDiv.style.display = 'none'; + + const formData = new FormData(form); + const data = {}; + formData.forEach((value, key) => { + data[key] = value; + }); + + try { + const response = await fetch(form.action, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(data), + }); + + const result = await response.json(); + + if (response.ok && result.success) { + // alert('Plan updated successfully!'); // Optional: show an alert + window.location.href = `/plans/<%= plan._id %>`; // Redirect to plan detail page + } else { + errorMessagesDiv.textContent = result.message || 'An error occurred while updating the plan.'; + errorMessagesDiv.style.display = 'block'; + } + } catch (error) { + console.error('Error submitting form:', error); + errorMessagesDiv.textContent = 'A network error occurred. Please try again.'; + errorMessagesDiv.style.display = 'block'; + } + }); + } +}); \ No newline at end of file diff --git a/src/public/scripts/footer.js b/src/public/scripts/footer.js index e63f58a..03e9328 100644 --- a/src/public/scripts/footer.js +++ b/src/public/scripts/footer.js @@ -13,6 +13,7 @@ if(page !== "login" && page !== "signup" && page !== "forgotPassword" && page != factButton.addEventListener("click", () => { factMenu.classList.toggle("hidden"); }); + factSubmitButton.addEventListener("click", () => { fetch("/fact", { method: "POST", diff --git a/src/public/scripts/geolocation.js b/src/public/scripts/geolocation.js index 94c9c66..15d4bbb 100644 --- a/src/public/scripts/geolocation.js +++ b/src/public/scripts/geolocation.js @@ -1,9 +1,17 @@ +/** + * getLocation gets the geolocation of user on page load. + */ function getLocation() { if (navigator.geolocation) { navigator.geolocation.getCurrentPosition(getLatestExchange, error); } } +/** + * update the currency exchange rate dropdown + * or display error + * @param {object} data + */ function update(data) { if (data.data.message != "error") { document.getElementById("loading").style = "display: none"; @@ -48,6 +56,10 @@ function update(data) { } } +/** + * switchButton changes the selected exchange rate icon + * @param {element} clickedButton + */ function switchButton(clickedButton) { document.getElementById("dropdown-country-button").value = clickedButton.value; updateExchange(document.getElementById("dropdown-country-button").value); @@ -60,10 +72,18 @@ function switchButton(clickedButton) { `; } +/** + * updateExchange updates the exchange rate + * @param {number} exRate + */ function updateExchange(exRate) { document.getElementById("exchange").innerHTML = "$1.00 = $" +`${(1 * exRate).toFixed(2)}`; } +/** + * getLatestExchange gets rates from API from a given location. + * @param {object} position coordinates + */ async function getLatestExchange(position) { let lat = position.coords.latitude; let lon = position.coords.longitude; @@ -73,6 +93,10 @@ async function getLatestExchange(position) { update(data); } +/** + * error displays error message + * @param {string} err + */ function error(err) { const data = { data: { diff --git a/src/public/scripts/planManager.js b/src/public/scripts/planManager.js index a1d4bec..9f0b1ac 100644 --- a/src/public/scripts/planManager.js +++ b/src/public/scripts/planManager.js @@ -1,3 +1,7 @@ +/** + * editPlan redirect + * @param {string} planId + */ function editPlan(planId) { if (planId) { window.location.href = `/plans/${planId}/edit`; @@ -7,6 +11,10 @@ function editPlan(planId) { } } +/** + * deletePlan handler + * @param {string} planId + */ async function deletePlan(planId) { if (!planId) { console.error('deletePlan called without a planId'); @@ -36,4 +44,4 @@ async function deletePlan(planId) { 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/public/scripts/planModals.js b/src/public/scripts/planModals.js new file mode 100644 index 0000000..07a8dbe --- /dev/null +++ b/src/public/scripts/planModals.js @@ -0,0 +1,78 @@ +document.addEventListener('DOMContentLoaded', () => { + const openModalBtn = document.getElementById('openNewPlanModalBtn'); + const newPlanDialog = document.getElementById('newPlanDialog'); + const newPlanForm = document.getElementById('newPlanForm'); + const submitNewPlanBtn = document.getElementById('submitNewPlanBtn'); + const newPlanError = document.getElementById('newPlanError'); + const resetNewPlanFormBtn = document.getElementById('resetNewPlanFormBtn'); + + openModalBtn.addEventListener('click', () => { + newPlanDialog.showModal(); + // Error reset is now handled by 'close' event, but good to clear on open too + newPlanError.style.display = 'none'; + newPlanError.textContent = ''; + }); + + // Handles form reset and error clearing when dialog is closed by any means (Esc, Cancel button, backdrop click, successful submit) + newPlanDialog.addEventListener('close', () => { + newPlanForm.reset(); + newPlanError.style.display = 'none'; + newPlanError.textContent = ''; + }); + + // Close dialog if user clicks on the backdrop + newPlanDialog.addEventListener('click', (event) => { + if (event.target === newPlanDialog) { + newPlanDialog.close(); // This will trigger the 'close' event listener above + } + }); + + resetNewPlanFormBtn.addEventListener('click', () => { + newPlanForm.reset(); + newPlanError.style.display = 'none'; + newPlanError.textContent = ''; + }); + + newPlanForm.addEventListener('submit', async (event) => { + event.preventDefault(); + submitNewPlanBtn.disabled = true; + submitNewPlanBtn.textContent = 'Saving...'; + newPlanError.style.display = 'none'; + + const formData = new FormData(newPlanForm); + const data = Object.fromEntries(formData.entries()); + + try { + const response = await fetch('/newPlan', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(data), + }); + + if (response.ok) { + const result = await response.json(); + if (result.success) { + newPlanDialog.close(); // This will trigger the 'close' event listener + window.location.reload(); // Reload to see the new plan + } else { + newPlanError.textContent = result.message || 'Failed to create plan. Please try again.'; + newPlanError.style.display = 'block'; + } + } else { + const errorData = await response.json(); + newPlanError.textContent = errorData.message || `Error: ${response.status} - ${response.statusText}`; + newPlanError.style.display = 'block'; + } + } catch (error) { + console.error('Error submitting new plan:', error); + newPlanError.textContent = 'An unexpected error occurred. Please try again.'; + newPlanError.style.display = 'block'; + } + finally { + submitNewPlanBtn.disabled = false; + submitNewPlanBtn.textContent = 'Save Plan'; + } + }); +}); \ No newline at end of file diff --git a/src/views/editPlan.ejs b/src/views/editPlan.ejs index 63ae3a7..44862c7 100644 --- a/src/views/editPlan.ejs +++ b/src/views/editPlan.ejs @@ -55,73 +55,7 @@
- + <%- include('./partials/navBar') %> <%- include('./partials/scriptLoader') %> diff --git a/src/views/plans.ejs b/src/views/plans.ejs index fa48b64..1f47031 100644 --- a/src/views/plans.ejs +++ b/src/views/plans.ejs @@ -65,84 +65,5 @@ <%- include("./partials/navBar") %> <%- include("./partials/scriptLoader") %> - + <%- include("./partials/footer") %> From 9a61d362be9b3b98b91ffff7ccd1ab5cf13e5650 Mon Sep 17 00:00:00 2001 From: SowinskiBraeden Date: Wed, 21 May 2025 11:38:16 -0700 Subject: [PATCH 17/36] update/project structure --- README.md | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index aa462a5..89c3b30 100755 --- a/README.md +++ b/README.md @@ -53,16 +53,22 @@ Example: ``` retirementCalculator/ ├── src/ -│ ├── views/ -│ │ └── partials/ -│ ├── css/ -│ ├── images/ -│ ├── scripts/ -│ └── utils/ +| ├── auth/ +| ├── database/ +│ ├── public/ +│ │ ├── images/ +│ │ ├── scripts/ +│ │ └── svgs/ +| | +│ ├── router/ +│ ├── utils/ +│ └── views/ +│ └── partials/ │ -├── app.js ├── .env.example ├── .gitignore +├── app.js +├── CONTRIBUTING.md ├── LICENSE ├── package-lock.json ├── package.json From 163cb73b9d311d2731c6bf5db8acf3c5dbf16cd9 Mon Sep 17 00:00:00 2001 From: SowinskiBraeden Date: Wed, 21 May 2025 12:06:57 -0700 Subject: [PATCH 18/36] fix/validation error messages --- src/auth/authentication.js | 15 ++++++++------- src/auth/forgotPass.js | 4 ++-- src/router/user.js | 20 ++++++++++---------- 3 files changed, 20 insertions(+), 19 deletions(-) diff --git a/src/auth/authentication.js b/src/auth/authentication.js index d4a8c5e..5729ae2 100644 --- a/src/auth/authentication.js +++ b/src/auth/authentication.js @@ -27,14 +27,14 @@ module.exports = (users) => { router.post("/login", async (req, res) => { const credentialSchema = joi.object({ - email: joi.string().email().required(), + email: joi.string().email({ minDomainSegments: 2, tlds: { allow: true } }).required(), password: joi.string().alphanum().max(20).required(), }); const valid = credentialSchema.validate(req.body); - if (valid.err) { - req.session.errMessage = "Invalid input"; + if (valid.error) { + req.session.errMessage = "Invalid input:" + valid.error.details.map(d => d.message.replace(/"/g, '')).join(', '); res.status(status.BadRequest); return res.redirect("/login"); } @@ -72,16 +72,16 @@ module.exports = (users) => { router.post("/signup", async (req, res) => { const userSchema = joi.object({ - email: joi.string().email().required(), + email: joi.string().email({ minDomainSegments: 2, tlds: { allow: true } }).required(), name: joi.string().pattern(new RegExp('^[a-zA-Z]+$')).max(20).required(), password: joi.string().alphanum().max(20).min(8).required(), repassword: joi.string().alphanum().max(20).min(8).required(), }); const valid = userSchema.validate(req.body); - - if (valid.err) { - req.session.errMessage = "Invalid input", + + if (valid.error) { + req.session.errMessage = valid.error.details[0].message, res.status(status.BadRequest); return res.redirect("/signup"); } @@ -95,6 +95,7 @@ module.exports = (users) => { if (req.body.password != req.body.repassword) { req.session.errMessage = "Passwords must match"; + res.status(status.BadRequest); return res.redirect(`/signup/?name=${req.body.name}&email=${req.body.email}`); } diff --git a/src/auth/forgotPass.js b/src/auth/forgotPass.js index bcf2ea5..2dce041 100644 --- a/src/auth/forgotPass.js +++ b/src/auth/forgotPass.js @@ -21,7 +21,7 @@ module.exports = (users) => { router.post('/auth/resetPass', async (req, res) => { const resetSchema = joi.object({ - email: joi.string().email().required(), + email: joi.string().email({ minDomainSegments: 2, tlds: { allow: true } }).required(), }); req.session.error = ''; req.session.reset = ''; @@ -83,7 +83,7 @@ module.exports = (users) => { const valid = passwordSchema.validate({ password, confirmPassword }); if (valid.error) { console.log("houston we have a problem"); // nice - req.session.error = 'Invalid input'; + req.session.error = "Invalid input:" + valid.error.details.map(d => d.message.replace(/"/g, '')).join(', ');; res.status(status.BadRequest); return res.redirect(`/reset/${token}`); } diff --git a/src/router/user.js b/src/router/user.js index 3f95a73..e4421ce 100644 --- a/src/router/user.js +++ b/src/router/user.js @@ -449,7 +449,7 @@ module.exports = (middleware, users, plans, assets) => { router.post("/updateAccount", async (req, res) => { const accountSchema = joi.object({ - email: joi.string().email(), + email: joi.string().email({ minDomainSegments: 2, tlds: { allow: true } }), name: joi.string().pattern(new RegExp('^[a-zA-Z]+$')).max(20), password: joi.string().alphanum().max(20).min(8), repassword: joi.string().alphanum().max(20).min(8), @@ -457,9 +457,9 @@ module.exports = (middleware, users, plans, assets) => { const valid = accountSchema.validate(req.body); - if (valid.err) { - req.session.errMessage = "Invalid input", - res.status(status.BadRequest); + if (valid.error) { + req.session.errMessage = "Invalid input:" + valid.error.details.map(d => d.message.replace(/"/g, '')).join(', '); + res.status(status.BadRequest); return res.redirect("/profile"); } @@ -606,9 +606,9 @@ module.exports = (middleware, users, plans, assets) => { const valid = assetSchema.validate(req.body); - if (valid.err) { - req.session.errMessage = "Invalid input", - res.status(status.BadRequest); + if (valid.error) { + req.session.errMessage = "Invalid input:" + valid.error.details.map(d => d.message.replace(/"/g, '')).join(', '); + res.status(status.BadRequest); return res.redirect("/assets"); } @@ -651,9 +651,9 @@ module.exports = (middleware, users, plans, assets) => { const valid = assetSchema.validate(req.body); - if (valid.err) { - req.session.errMessage = "Invalid input", - res.status(status.BadRequest); + if (valid.error) { + req.session.errMessage = "Invalid input:" + valid.error.details.map(d => d.message.replace(/"/g, '')).join(', '); + res.status(status.BadRequest); return res.redirect("/assets"); } From 7fd7fa28ff535e85b6c7f71f7e93ade379f39e14 Mon Sep 17 00:00:00 2001 From: nicoagostini Date: Wed, 21 May 2025 13:56:56 -0700 Subject: [PATCH 19/36] fix/Router after editing fixed to plan/PlanId --- src/public/scripts/editPlanModals.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/public/scripts/editPlanModals.js b/src/public/scripts/editPlanModals.js index 55f9909..5c25837 100644 --- a/src/public/scripts/editPlanModals.js +++ b/src/public/scripts/editPlanModals.js @@ -49,8 +49,10 @@ document.addEventListener('DOMContentLoaded', () => { const result = await response.json(); if (response.ok && result.success) { - // alert('Plan updated successfully!'); // Optional: show an alert - window.location.href = `/plans/<%= plan._id %>`; // Redirect to plan detail page + const actionUrl = form.action; + const urlParts = actionUrl.split('/'); + const planId = urlParts[urlParts.length - 2]; + window.location.href = `/plans/${planId}`; } else { errorMessagesDiv.textContent = result.message || 'An error occurred while updating the plan.'; errorMessagesDiv.style.display = 'block'; From 7fe94e1934aaea5a86a8bf29763b0dc1a7c7c888 Mon Sep 17 00:00:00 2001 From: SowinskiBraeden Date: Wed, 21 May 2025 13:58:39 -0700 Subject: [PATCH 20/36] fix/password --- src/auth/authentication.js | 6 +++--- src/router/user.js | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/auth/authentication.js b/src/auth/authentication.js index 5729ae2..f89fc41 100644 --- a/src/auth/authentication.js +++ b/src/auth/authentication.js @@ -28,7 +28,7 @@ module.exports = (users) => { const credentialSchema = joi.object({ email: joi.string().email({ minDomainSegments: 2, tlds: { allow: true } }).required(), - password: joi.string().alphanum().max(20).required(), + password: joi.string().max(20).required(), }); const valid = credentialSchema.validate(req.body); @@ -74,8 +74,8 @@ module.exports = (users) => { const userSchema = joi.object({ email: joi.string().email({ minDomainSegments: 2, tlds: { allow: true } }).required(), name: joi.string().pattern(new RegExp('^[a-zA-Z]+$')).max(20).required(), - password: joi.string().alphanum().max(20).min(8).required(), - repassword: joi.string().alphanum().max(20).min(8).required(), + password: joi.string().max(20).min(8).required(), + repassword: joi.string().max(20).min(8).required(), }); const valid = userSchema.validate(req.body); diff --git a/src/router/user.js b/src/router/user.js index e4421ce..f3cb614 100644 --- a/src/router/user.js +++ b/src/router/user.js @@ -451,8 +451,8 @@ module.exports = (middleware, users, plans, assets) => { const accountSchema = joi.object({ email: joi.string().email({ minDomainSegments: 2, tlds: { allow: true } }), name: joi.string().pattern(new RegExp('^[a-zA-Z]+$')).max(20), - password: joi.string().alphanum().max(20).min(8), - repassword: joi.string().alphanum().max(20).min(8), + password: joi.string().max(20).min(8), + repassword: joi.string().max(20).min(8), }); const valid = accountSchema.validate(req.body); From 17e3bdf4f41378bcdd9a98cd76547672e916501c Mon Sep 17 00:00:00 2001 From: nicoagostini Date: Wed, 21 May 2025 14:40:08 -0700 Subject: [PATCH 21/36] fix/margin top on assets.ejs changed to match the application --- src/views/assets.ejs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/views/assets.ejs b/src/views/assets.ejs index 691e64b..ae793ae 100644 --- a/src/views/assets.ejs +++ b/src/views/assets.ejs @@ -10,14 +10,14 @@
-
+
-

Assets:

-

<%= assets.length %>

+

Assets:

+

<%= assets.length %>

-

Total Value:

-

+

Total Value:

+

<%- new Intl.NumberFormat('en-US', { style: 'currency', currency: geoData.currency ? geoData.currency : 'CAD' }).format(assets.reduce((total, e) => total + e.value, 0)) %>

From 7c9e92345d86a7bbf2241b3a280dbbea875bd8b1 Mon Sep 17 00:00:00 2001 From: SowinskiBraeden Date: Wed, 21 May 2025 15:03:26 -0700 Subject: [PATCH 22/36] fix/assets empty description --- src/router/user.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/router/user.js b/src/router/user.js index f3cb614..d5d70c3 100644 --- a/src/router/user.js +++ b/src/router/user.js @@ -27,8 +27,9 @@ const getAssetSchema = (type) => { name: joi.string().alphanum().min(3).max(30).required(), value: joi.number().min(0).required(), purchaseDate: joi.date().required(), - description: joi.string().alphanum().max(240), + description: joi.string().max(240).min(0), id: joi.string().alphanum(), // May be passed when updating existing asset + userId: joi.string().alphanum(), }); break; case "saving": @@ -37,6 +38,7 @@ const getAssetSchema = (type) => { name: joi.string().alphanum().min(3).max(30).required(), value: joi.number().min(0).required(), id: joi.string().alphanum(), // May be passed when updating existing asset + userId: joi.string().alphanum(), }); break; case "stock": @@ -47,6 +49,7 @@ const getAssetSchema = (type) => { quantity: joi.number().min(1).required(), purchaseDate: joi.date().required(), id: joi.string().alphanum(), // May be passed when updating existing asset + userId: joi.string().alphanum(), }); break; default: From 7c09df09240c557e1afc6e179d60fc6d29ee67ba Mon Sep 17 00:00:00 2001 From: SowinskiBraeden Date: Wed, 21 May 2025 15:13:09 -0700 Subject: [PATCH 23/36] fix/asset modified date + des whitespace --- src/views/partials/assetModify.ejs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/views/partials/assetModify.ejs b/src/views/partials/assetModify.ejs index 285d905..4e4ffbf 100644 --- a/src/views/partials/assetModify.ejs +++ b/src/views/partials/assetModify.ejs @@ -153,9 +153,7 @@ rows="4" cols="50" class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm disabled:border-gray-200 disabled:bg-gray-50 disabled:text-gray-500 disabled:shadow-none" - > - <%= asset.description %> - + ><%= asset.description %> <% } %> <% if (asset.type != "saving") { %> @@ -171,7 +169,8 @@ <% } %>
- Last Modified: <%= asset.updatedAt %> + <% let options = { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' }; %> + Last Modified: <%= asset.updatedAt.toLocaleDateString("en-US", options); %>
From b12d4bebe1bca247349ea6107f34a116c9b27cca Mon Sep 17 00:00:00 2001 From: SowinskiBraeden Date: Wed, 21 May 2025 15:40:29 -0700 Subject: [PATCH 24/36] update/add year to car assets --- src/public/scripts/assetManager.js | 30 ++++++++++++++++++++++++++++++ src/router/user.js | 3 +++ src/views/partials/assetModify.ejs | 21 +++++++++++++++++++++ src/views/partials/createAsset.ejs | 15 +++++++++++++++ 4 files changed, 69 insertions(+) diff --git a/src/public/scripts/assetManager.js b/src/public/scripts/assetManager.js index 16bbc37..b0eee0b 100644 --- a/src/public/scripts/assetManager.js +++ b/src/public/scripts/assetManager.js @@ -85,6 +85,8 @@ function lockAsset(assetId, icon) { document.getElementById(`${key}-${assetId}`).disabled = true; }); + if (icon === "Car" || icon === "Motorcycle") document.getElementById(`year-${assetId}`).disabled = true; + document.getElementById(`save-${assetId}`).disabled = true; document.getElementById(`save-${assetId}`).classList.remove("cursor-pointer"); document.getElementById(`save-${assetId}`).classList.add("cursor-not-allowed"); @@ -105,6 +107,8 @@ function unlockAsset(assetId, icon) { document.getElementById(`${key}-${assetId}`).disabled = false; }); + if (icon === "Car" || icon === "Motorcycle") document.getElementById(`year-${assetId}`).disabled = false; + document.getElementById(`save-${assetId}`).disabled = false; document.getElementById(`save-${assetId}`).classList.remove("cursor-not-allowed"); document.getElementById(`save-${assetId}`).classList.add("cursor-pointer"); @@ -133,6 +137,8 @@ function autoOpenCreate() { * @param {string} assetId */ function selectIcon(selectedIcon, assetId="") { + toggleYear(selectedIcon.value, assetId); + document.getElementById(`dropdown-icon-button${assetId != "" ? "-" : ""}${assetId}`).value = selectedIcon.value; document.getElementById(`icon${assetId != "" ? "-" : ""}${assetId}`).value = selectedIcon.value @@ -144,5 +150,29 @@ function selectIcon(selectedIcon, assetId="") { `; } +/** + * toggleYear shows or hides year input + * @param {string} type of asset + */ +function toggleYear(type, assetId="") { + if (type === "Car" || type === "Motorcycle") { + if (assetId) { + document.getElementById(`year-${assetId}`).disabled = false; + document.getElementById(`year-modify-${assetId}`).style.display = 'block'; + } else { + document.getElementById('year-input').disabled = false; + document.getElementById('year-create').style.display = 'block'; + } + } else { + if (assetId) { + document.getElementById(`year-${assetId}`).disabled = true; + document.getElementById(`year-modify-${assetId}`).style.display = 'none'; + } else { + document.getElementById('year-input').disabled = true; + document.getElementById('year-create').style.display = 'none'; + } + } +} + resetRadio(); autoOpenCreate(); diff --git a/src/router/user.js b/src/router/user.js index d5d70c3..4665a7e 100644 --- a/src/router/user.js +++ b/src/router/user.js @@ -26,6 +26,7 @@ const getAssetSchema = (type) => { icon: joi.string().alphanum().required(), name: joi.string().alphanum().min(3).max(30).required(), value: joi.number().min(0).required(), + year: joi.number().min(1900).max(new Date().getFullYear()), purchaseDate: joi.date().required(), description: joi.string().max(240).min(0), id: joi.string().alphanum(), // May be passed when updating existing asset @@ -627,6 +628,7 @@ module.exports = (middleware, users, plans, assets) => { newAsset.value = newAsset.quantity * newAsset.price; newAsset.name = `${newAsset.ticker} Stock`; } + if (type == "other" && newAsset.year != "") newAsset.year = parseInt(newAsset.year); newAsset.value = parseFloat(newAsset.value); newAsset.icon = type == "stock" ? "Stock" : type == "saving" ? "Coins" : newAsset.icon; @@ -673,6 +675,7 @@ module.exports = (middleware, users, plans, assets) => { update.value = update.quantity * update.price; update.name = `${update.ticker} Stock`; } + if (type == "other" && update.year != "") update.year = parseInt(update.year); update.value = parseFloat(update.value); delete update.id; delete update.userId; diff --git a/src/views/partials/assetModify.ejs b/src/views/partials/assetModify.ejs index 4e4ffbf..b455aa1 100644 --- a/src/views/partials/assetModify.ejs +++ b/src/views/partials/assetModify.ejs @@ -120,6 +120,27 @@ value="<%= asset.value %>" class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm disabled:border-gray-200 disabled:bg-gray-50 disabled:text-gray-500 disabled:shadow-none" > + +
+ style="display: none;" + <% } %> + > + + +
<% } else { %> +
+ + +
+ Date: Wed, 21 May 2025 16:16:29 -0700 Subject: [PATCH 25/36] buttons blue --- src/views/assets.ejs | 4 +-- src/views/dashboard.ejs | 6 ++-- src/views/editPlan.ejs | 16 +++++------ src/views/forgotPass.ejs | 4 +-- src/views/login.ejs | 12 ++++---- src/views/partials/assetModify.ejs | 26 +++++++++--------- src/views/partials/createAsset.ejs | 44 +++++++++++++++--------------- src/views/planDetail.ejs | 2 +- src/views/plans.ejs | 16 +++++------ src/views/profile.ejs | 38 +++++++++++++------------- src/views/questionnaire.ejs | 22 +++++++-------- src/views/resetPass.ejs | 2 +- src/views/signup.ejs | 12 ++++---- 13 files changed, 102 insertions(+), 102 deletions(-) diff --git a/src/views/assets.ejs b/src/views/assets.ejs index ae793ae..41fb3cd 100644 --- a/src/views/assets.ejs +++ b/src/views/assets.ejs @@ -25,11 +25,11 @@
- + Back to Dashboard
diff --git a/src/views/editPlan.ejs b/src/views/editPlan.ejs index 44862c7..18043e9 100644 --- a/src/views/editPlan.ejs +++ b/src/views/editPlan.ejs @@ -10,44 +10,44 @@
+ class="mt-1 block w-full px-4 py-2 border border-gray-600 rounded-md shadow-sm bg-gray-700 text-white focus:ring-blue-500 focus:border-blue-500 sm:text-sm placeholder-gray-400">
+ class="mt-1 block w-full px-4 py-2 border border-gray-600 rounded-md shadow-sm bg-gray-700 text-white focus:ring-blue-500 focus:border-blue-500 sm:text-sm placeholder-gray-400">
+ class="mt-1 block w-full px-4 py-2 border border-gray-600 rounded-md shadow-sm bg-gray-700 text-white focus:ring-blue-500 focus:border-blue-500 sm:text-sm placeholder-gray-400">
+ class="mt-1 block w-full px-4 py-2 border border-gray-600 rounded-md shadow-sm bg-gray-700 text-white focus:ring-blue-500 focus:border-blue-500 sm:text-sm placeholder-gray-400">
+ class="mt-1 block w-full px-4 py-2 border border-gray-600 rounded-md shadow-sm bg-gray-700 text-white focus:ring-blue-500 focus:border-blue-500 sm:text-sm placeholder-gray-400">
- - + Cancel
diff --git a/src/views/forgotPass.ejs b/src/views/forgotPass.ejs index e18ae3c..af3707f 100644 --- a/src/views/forgotPass.ejs +++ b/src/views/forgotPass.ejs @@ -12,7 +12,7 @@
<% } else { %> @@ -31,7 +31,7 @@ <% if (!reset) { %>
+ class="border-2 border-blue-600 bg-blue-600 text-white py-2 w-full rounded-md hover:bg-blue-700 hover:border-blue-700 font-semibold transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2">Send Reset Link
<% } %>
diff --git a/src/views/login.ejs b/src/views/login.ejs index 7d0e7b2..b929c0a 100644 --- a/src/views/login.ejs +++ b/src/views/login.ejs @@ -11,13 +11,13 @@
<% if (errMessage && errMessage.length > 0) { %> @@ -27,20 +27,20 @@ <% } %>
+ class="border-2 border-blue-600 bg-blue-600 text-white py-2 w-full rounded-md hover:bg-blue-700 hover:border-blue-700 font-semibold transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2">Login
Don't have an account? - Register + Register
diff --git a/src/views/partials/assetModify.ejs b/src/views/partials/assetModify.ejs index b455aa1..dd0a852 100644 --- a/src/views/partials/assetModify.ejs +++ b/src/views/partials/assetModify.ejs @@ -8,7 +8,7 @@ style="width: 100%;" onclick="lockAsset('<%= asset._id %>', '<%= asset.icon %>')" type="submit" - class="text-center w-sm py-2 py-2 mx-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 cursor-pointer" + class="text-center w-sm py-2 py-2 mx-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 cursor-pointer" > Close @@ -34,7 +34,7 @@ id="dropdown-icon-button-<%= asset._id %>" value="" data-dropdown-toggle="dropdown-icons-<%= asset._id %>" - class="shrink-0 z-10 inline-flex items-center py-2.5 px-4 text-sm font-medium text-center border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm disabled:border-gray-200 disabled:bg-gray-50 disabled:text-gray-500 disabled:shadow-none" + class="shrink-0 z-10 inline-flex items-center py-2.5 px-4 text-sm font-medium text-center border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm disabled:border-gray-200 disabled:bg-gray-50 disabled:text-gray-500 disabled:shadow-none" type="button" > <%= asset.icon %> <%= asset.icon %> @@ -59,7 +59,7 @@ <% } else { %> @@ -93,7 +93,7 @@ type="text" name="name" value="<%= asset.name %>" - class="mt-1 mb-2 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm disabled:border-gray-200 disabled:bg-gray-50 disabled:text-gray-500 disabled:shadow-none" + class="mt-1 mb-2 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm disabled:border-gray-200 disabled:bg-gray-50 disabled:text-gray-500 disabled:shadow-none" > <% } else { %> @@ -103,7 +103,7 @@ type="text" name="ticker" value="<%= asset.ticker %>" - class="mt-1 mb-2 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm disabled:border-gray-200 disabled:bg-gray-50 disabled:text-gray-500 disabled:shadow-none" + class="mt-1 mb-2 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm disabled:border-gray-200 disabled:bg-gray-50 disabled:text-gray-500 disabled:shadow-none" > <% } %> @@ -118,7 +118,7 @@ step=".01" name="value" value="<%= asset.value %>" - class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm disabled:border-gray-200 disabled:bg-gray-50 disabled:text-gray-500 disabled:shadow-none" + class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm disabled:border-gray-200 disabled:bg-gray-50 disabled:text-gray-500 disabled:shadow-none" >
@@ -150,7 +150,7 @@ step=".01" name="price" value="<%= asset.price %>" - class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm disabled:border-gray-200 disabled:bg-gray-50 disabled:text-gray-500 disabled:shadow-none" + class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm disabled:border-gray-200 disabled:bg-gray-50 disabled:text-gray-500 disabled:shadow-none" > @@ -160,7 +160,7 @@ type="number" name="quantity" value="<%= asset.quantity %>" - class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm disabled:border-gray-200 disabled:bg-gray-50 disabled:text-gray-500 disabled:shadow-none" + class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm disabled:border-gray-200 disabled:bg-gray-50 disabled:text-gray-500 disabled:shadow-none" > <% } %> @@ -173,7 +173,7 @@ name="description" rows="4" cols="50" - class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm disabled:border-gray-200 disabled:bg-gray-50 disabled:text-gray-500 disabled:shadow-none" + class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm disabled:border-gray-200 disabled:bg-gray-50 disabled:text-gray-500 disabled:shadow-none" ><%= asset.description %> <% } %> @@ -185,7 +185,7 @@ type="date" name="purchaseDate" value="<%= asset.purchaseDate %>" - class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm disabled:border-gray-200 disabled:bg-gray-50 disabled:text-gray-500 disabled:shadow-none" + class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm disabled:border-gray-200 disabled:bg-gray-50 disabled:text-gray-500 disabled:shadow-none" > <% } %> @@ -199,7 +199,7 @@ id="save-<%= asset._id %>" disabled type="submit" - class="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 cursor-not-allowed" + class="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 cursor-not-allowed" > Save diff --git a/src/views/partials/createAsset.ejs b/src/views/partials/createAsset.ejs index 9f78a5f..2e98c58 100644 --- a/src/views/partials/createAsset.ejs +++ b/src/views/partials/createAsset.ejs @@ -6,7 +6,7 @@ @@ -25,7 +25,7 @@ type="radio" name="type" value="other" - class="form-radio h-4 w-4 text-indigo-600 border-gray-300 focus:ring-indigo-500 cursor-pointer" + class="form-radio h-4 w-4 text-blue-600 border-gray-300 focus:ring-blue-500 cursor-pointer" > Other Assets @@ -36,7 +36,7 @@ type="radio" name="type" value="saving" - class="form-radio h-4 w-4 text-indigo-600 border-gray-300 focus:ring-indigo-500 cursor-pointer" + class="form-radio h-4 w-4 text-blue-600 border-gray-300 focus:ring-blue-500 cursor-pointer" > Savings @@ -47,7 +47,7 @@ type="radio" name="type" value="stock" - class="form-radio h-4 w-4 text-indigo-600 border-gray-300 focus:ring-indigo-500 cursor-pointer" + class="form-radio h-4 w-4 text-blue-600 border-gray-300 focus:ring-blue-500 cursor-pointer" > Stocks @@ -64,7 +64,7 @@ @@ -179,7 +179,7 @@ name="name" minlength="3" maxlength="30" - class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm" + class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm" > @@ -191,20 +191,20 @@ id="value" type="number" name="value" - class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm" + class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm" >
@@ -223,7 +223,7 @@ name="ticker" minlength="3" maxlength="5" - class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm" + class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm" > @@ -235,7 +235,7 @@ id="price" type="number" name="price" - class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm" + class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm" > @@ -245,7 +245,7 @@ min="1" id="quantity" name="quantity" - class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm" + class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm" > @@ -254,20 +254,20 @@ type="date" id="purchaseDate" name="purchaseDate" - class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm" + class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm" >
diff --git a/src/views/planDetail.ejs b/src/views/planDetail.ejs index 7d8477b..95c2a7d 100644 --- a/src/views/planDetail.ejs +++ b/src/views/planDetail.ejs @@ -88,7 +88,7 @@
- +
diff --git a/src/views/plans.ejs b/src/views/plans.ejs index e51f305..ac8488e 100644 --- a/src/views/plans.ejs +++ b/src/views/plans.ejs @@ -24,7 +24,7 @@

Create New Retirement Plan

- +
@@ -34,29 +34,29 @@

* Indicates a required field

- +
- +
- +
- +
- +
- - + +
diff --git a/src/views/profile.ejs b/src/views/profile.ejs index 3be7f54..acfac2f 100644 --- a/src/views/profile.ejs +++ b/src/views/profile.ejs @@ -51,24 +51,24 @@

Account settings

- +
- + - + - + - +
- +
@@ -78,7 +78,7 @@

Personal information

<% if (user.financialData) { %> - + <% } %>
@@ -91,7 +91,7 @@ id="dob" type="date" name="dob" - class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm disabled:border-gray-200 disabled:bg-gray-50 disabled:text-gray-500 disabled:shadow-none" + class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm disabled:border-gray-200 disabled:bg-gray-50 disabled:text-gray-500 disabled:shadow-none" <% let year = typeof(user.dob) == "object" ? user.dob.getFullYear() : user.dob.split('-')[0]; %> <% let month = typeof(user.dob) == "object" ? user.dob.getMonth() : user.dob.split('-')[1] - 1; %> <% let day = typeof(user.dob) == "object" ? user.dob.getDate() + 1 : user.dob.split('-')[2].split('T')[0];%> @@ -101,7 +101,7 @@
- @@ -112,19 +112,19 @@
@@ -132,31 +132,31 @@
- +
- +
- +
- +
- +
<% } else { %> <% } %> diff --git a/src/views/questionnaire.ejs b/src/views/questionnaire.ejs index 4370a47..8304efa 100644 --- a/src/views/questionnaire.ejs +++ b/src/views/questionnaire.ejs @@ -8,11 +8,11 @@
- +
- @@ -23,19 +23,19 @@
@@ -43,26 +43,26 @@
- +
- +
- +
- +
- +
diff --git a/src/views/resetPass.ejs b/src/views/resetPass.ejs index b608e5a..8e7745d 100644 --- a/src/views/resetPass.ejs +++ b/src/views/resetPass.ejs @@ -26,7 +26,7 @@ <% } %>
diff --git a/src/views/signup.ejs b/src/views/signup.ejs index dc50168..9183107 100644 --- a/src/views/signup.ejs +++ b/src/views/signup.ejs @@ -11,23 +11,23 @@
<% if (errMessage && errMessage.length > 0) { %> @@ -37,11 +37,11 @@ <% } %>
+ class="border-2 border-blue-600 bg-blue-600 text-white py-2 w-full rounded-md hover:bg-blue-700 hover:border-blue-700 font-semibold transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2">Signup
Already have an account? - Login + Login
From cf3d3738f11522ac7fa0bcf307c357e63dd29ae7 Mon Sep 17 00:00:00 2001 From: SowinskiBraeden Date: Wed, 21 May 2025 16:37:48 -0700 Subject: [PATCH 26/36] fix/default hide year on asset create --- src/views/partials/createAsset.ejs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/views/partials/createAsset.ejs b/src/views/partials/createAsset.ejs index 9f78a5f..e1953ef 100644 --- a/src/views/partials/createAsset.ejs +++ b/src/views/partials/createAsset.ejs @@ -104,7 +104,7 @@ class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm" > -
+
diff --git a/src/views/planDetail.ejs b/src/views/planDetail.ejs index 7d8477b..e9d13e0 100644 --- a/src/views/planDetail.ejs +++ b/src/views/planDetail.ejs @@ -34,7 +34,7 @@
-

<%- new Intl.NumberFormat('en-US', { style: 'currency', currency: geoData.currency ? geoData.currency : 'CAD' }).format(assets.reduce((total, asset) => total + asset.value, 0)) %>

+

<%- new Intl.NumberFormat('en-US', { style: 'currency', currency: geoData.currency ? geoData.currency : 'CAD' }).format(totalUserAssetValue) %>

From d2e69f230f2b7b7261eb6f4fb8c497677c0cc723 Mon Sep 17 00:00:00 2001 From: knighthawk4227 Date: Wed, 21 May 2025 16:54:17 -0700 Subject: [PATCH 28/36] fixing conflicts --- src/views/assets.ejs | 7 +++++-- src/views/dashboard.ejs | 7 +++---- src/views/partials/assetModify.ejs | 5 +++-- src/views/partials/createAsset.ejs | 2 +- 4 files changed, 12 insertions(+), 9 deletions(-) diff --git a/src/views/assets.ejs b/src/views/assets.ejs index 4e95d18..cd1e212 100644 --- a/src/views/assets.ejs +++ b/src/views/assets.ejs @@ -25,11 +25,14 @@
- + Back to Dashboard
diff --git a/src/views/partials/assetModify.ejs b/src/views/partials/assetModify.ejs index dd0a852..af5fde1 100644 --- a/src/views/partials/assetModify.ejs +++ b/src/views/partials/assetModify.ejs @@ -8,7 +8,8 @@ style="width: 100%;" onclick="lockAsset('<%= asset._id %>', '<%= asset.icon %>')" type="submit" - class="text-center w-sm py-2 py-2 mx-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 cursor-pointer" + class="text-center w-sm py-2 py-2 mx-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white + bg-blue-600 hover:bg-blue-700 focus:outline-none " > Close @@ -71,7 +72,7 @@ diff --git a/src/views/partials/createAsset.ejs b/src/views/partials/createAsset.ejs index d24705f..d69543f 100644 --- a/src/views/partials/createAsset.ejs +++ b/src/views/partials/createAsset.ejs @@ -6,7 +6,7 @@ From 05fa5558e3d3b246f333d0ba0d4d49e013a010d8 Mon Sep 17 00:00:00 2001 From: knighthawk4227 Date: Wed, 21 May 2025 17:02:35 -0700 Subject: [PATCH 29/36] open modal of plan after pressing create new plan --- src/public/scripts/planModals.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/public/scripts/planModals.js b/src/public/scripts/planModals.js index 07a8dbe..4eca1f2 100644 --- a/src/public/scripts/planModals.js +++ b/src/public/scripts/planModals.js @@ -6,6 +6,13 @@ document.addEventListener('DOMContentLoaded', () => { const newPlanError = document.getElementById('newPlanError'); const resetNewPlanFormBtn = document.getElementById('resetNewPlanFormBtn'); + let url = new URLSearchParams(window.location.search); + let modalValue = url.get('openModal'); + + if (modalValue === 'true') { + newPlanDialog.showModal(); + } + openModalBtn.addEventListener('click', () => { newPlanDialog.showModal(); // Error reset is now handled by 'close' event, but good to clear on open too From c1adb31657fd1ebd28fd52d5773dac410d4e458c Mon Sep 17 00:00:00 2001 From: knighthawk4227 Date: Thu, 22 May 2025 00:21:35 -0700 Subject: [PATCH 30/36] fixing indigo button --- src/views/partials/assetModify.ejs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/views/partials/assetModify.ejs b/src/views/partials/assetModify.ejs index af5fde1..a35aca4 100644 --- a/src/views/partials/assetModify.ejs +++ b/src/views/partials/assetModify.ejs @@ -72,7 +72,7 @@ From ec309fbafd8c7dad0e59bddb124ed640ffd81474 Mon Sep 17 00:00:00 2001 From: nicoagostini Date: Thu, 22 May 2025 10:33:13 -0700 Subject: [PATCH 31/36] fix/ Reinforce user to complete the financial questionnaire --- src/router/user.js | 38 ++++++++++++++++++++++++++++---------- 1 file changed, 28 insertions(+), 10 deletions(-) diff --git a/src/router/user.js b/src/router/user.js index c370716..ad16184 100644 --- a/src/router/user.js +++ b/src/router/user.js @@ -109,6 +109,12 @@ module.exports = (middleware, users, plans, assets) => { }); router.get('/assets', async (req, res) => { + + if (!req.session.user.financialData || !req.session.user) { + req.session.errMessage = "Please complete your financial data before creating a plan."; + return res.status(status.Unauthorized).redirect('/questionnaire'); + } + let userAssets = await assets.find({ userId: new ObjectId(req.session.userId) }).toArray(); let totalUserAssetValue = await calculateTotalAssetValue(userAssets); @@ -124,6 +130,12 @@ module.exports = (middleware, users, plans, assets) => { }); router.get('/plans', async (req, res) => { + + if (!req.session.user.financialData || !req.session.user) { + req.session.errMessage = "Please complete your financial data before creating a plan."; + return res.status(status.Unauthorized).redirect('/questionnaire'); + } + try { const updatedUserPlans = await updateProgress(plans, assets, users, req.session.user._id); @@ -177,21 +189,27 @@ module.exports = (middleware, users, plans, assets) => { } }); - router.get('/newPlan', (req, res) => { + // router.get('/newPlan', (req, res) => { + // if (!req.session.user.financialData || !req.session.user) { + // req.session.errMessage = "Please complete your financial data before creating a plan."; + // return res.status(status.Unauthorized).redirect('/questionnaire'); + // } + // const errMessage = req.session.errMessage; + // req.session.errMessage = ""; + // res.render('newPlan', { + // user: req.session.user, + // errMessage: errMessage, + // geoData: req.session.geoData + // }); + // }); + + router.post('/newPlan', async (req, res) => { + if (!req.session.user.financialData || !req.session.user) { req.session.errMessage = "Please complete your financial data before creating a plan."; return res.status(status.Unauthorized).redirect('/questionnaire'); } - const errMessage = req.session.errMessage; - req.session.errMessage = ""; - res.render('newPlan', { - user: req.session.user, - errMessage: errMessage, - geoData: req.session.geoData - }); - }); - router.post('/newPlan', async (req, res) => { const planSchema = joi.object({ name: joi.string().min(3).max(100).required(), retirementAge: joi.number().min(18).max(120).required(), From 3d55a2b0d031814ad0e92cd64abadbca15057087 Mon Sep 17 00:00:00 2001 From: nicoagostini Date: Thu, 22 May 2025 10:37:05 -0700 Subject: [PATCH 32/36] fix/ vehicle depreciation capped at one thousand --- src/util/calculations.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/util/calculations.js b/src/util/calculations.js index 81ff6bf..979de45 100644 --- a/src/util/calculations.js +++ b/src/util/calculations.js @@ -55,6 +55,9 @@ async function calculateTotalAssetValue(assets) { asset.year = new Date(asset.year).getFullYear(); let age = today.getFullYear() - asset.year; let value = asset.value * Math.pow(0.85, age); + if (value < 1000) { + value = 1000; + } totalAssetValue += value; } else { totalAssetValue += asset.value; From c0e2d01ea63725bc767688c398d0c6cf5282e490 Mon Sep 17 00:00:00 2001 From: SowinskiBraeden Date: Thu, 22 May 2025 10:38:22 -0700 Subject: [PATCH 33/36] fix/asset name aphanum restriction --- src/router/user.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/router/user.js b/src/router/user.js index c370716..0506f16 100644 --- a/src/router/user.js +++ b/src/router/user.js @@ -24,7 +24,7 @@ const getAssetSchema = (type) => { assetSchema = joi.object({ type: joi.string().valid("other", "stock", "saving").required(), icon: joi.string().alphanum().required(), - name: joi.string().alphanum().min(3).max(30).required(), + name: joi.string().min(3).max(30).required(), value: joi.number().min(0).required(), year: joi.number().min(1900).max(new Date().getFullYear()), purchaseDate: joi.date().required(), @@ -36,7 +36,7 @@ const getAssetSchema = (type) => { case "saving": assetSchema = joi.object({ type: joi.string().valid("other", "stock", "saving").required(), - name: joi.string().alphanum().min(3).max(30).required(), + name: joi.string().min(3).max(30).required(), value: joi.number().min(0).required(), id: joi.string().alphanum(), // May be passed when updating existing asset userId: joi.string().alphanum(), @@ -45,7 +45,7 @@ const getAssetSchema = (type) => { case "stock": assetSchema = joi.object({ type: joi.string().valid("other", "stock", "saving").required(), - ticker: joi.string().alphanum().min(3).max(5).required(), + ticker: joi.string().min(3).max(5).required(), price: joi.number().min(0).required(), quantity: joi.number().min(1).required(), purchaseDate: joi.date().required(), From 5b6ce3b09de300a87621459cc73cd31e9d30a8a3 Mon Sep 17 00:00:00 2001 From: nicoagostini Date: Thu, 22 May 2025 10:42:53 -0700 Subject: [PATCH 34/36] fix/Depreciation fixed, if asset value < 1000, use the input value --- src/util/calculations.js | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/src/util/calculations.js b/src/util/calculations.js index 979de45..e521786 100644 --- a/src/util/calculations.js +++ b/src/util/calculations.js @@ -45,28 +45,35 @@ async function updatePlanProgressInDB(planId, percentage, plans) { async function calculateTotalAssetValue(assets) { if (!assets || typeof assets.find !== 'function') { console.error("Error with the assets collection"); - return; + return 0; } try { let totalAssetValue = 0; + const today = new Date(); + for (const asset of assets) { - if(asset.icon == "Motorcycle" || asset.icon == "Car"){ - today = new Date(); - asset.year = new Date(asset.year).getFullYear(); - let age = today.getFullYear() - asset.year; - let value = asset.value * Math.pow(0.85, age); - if (value < 1000) { - value = 1000; + if (asset.icon === "Motorcycle" || asset.icon === "Car") { + if (asset.value < 1000) { + totalAssetValue += asset.value; + } else { + const assetYear = new Date(asset.year).getFullYear(); + const age = today.getFullYear() - assetYear; + let depreciatedValue = asset.value * Math.pow(0.85, age); + + if (depreciatedValue < 1000) { + totalAssetValue += 1000; + } else { + totalAssetValue += depreciatedValue; + } } - totalAssetValue += value; } else { - totalAssetValue += asset.value; + totalAssetValue += asset.value; } } return totalAssetValue; } catch (err) { console.error("Error in calculateTotalAssetValue:", err); - return 0; + return 0; } } From 16f267bcd4a211807c446fcfa18dd9ab2667028b Mon Sep 17 00:00:00 2001 From: nicoagostini Date: Thu, 22 May 2025 11:09:15 -0700 Subject: [PATCH 35/36] fix/calculations capped if you try to retire today --- src/util/calculations.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/util/calculations.js b/src/util/calculations.js index e521786..148b319 100644 --- a/src/util/calculations.js +++ b/src/util/calculations.js @@ -128,6 +128,10 @@ async function calculateProgress(plan, assets, users, userId) { const monthlyInvestment = (totalUserPlanValue - totalUserAssetValue + totalCostOfRetirement) / monthsUntilRetirement; const percentageCalculated = (totalUserAssetValue / (totalUserPlanValue + totalCostOfRetirement)) * 100; + + if (monthsUntilRetirement <= 0) { + return { monthlyInvestment: 0, totalCostOfRetirement: totalCostOfRetirement, monthsUntilRetirement: 0, yearsRetired: yearsRetired, yearsUntilRetirement: 0, percentage: 0 }; + } const progress = {}; From 72698145014accc5564065540d9e82e92dbde31f Mon Sep 17 00:00:00 2001 From: SowinskiBraeden Date: Thu, 22 May 2025 11:12:36 -0700 Subject: [PATCH 36/36] fix/limit age --- src/views/profile.ejs | 2 ++ src/views/questionnaire.ejs | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/views/profile.ejs b/src/views/profile.ejs index acfac2f..ee1ce2b 100644 --- a/src/views/profile.ejs +++ b/src/views/profile.ejs @@ -97,6 +97,8 @@ <% let day = typeof(user.dob) == "object" ? user.dob.getDate() + 1 : user.dob.split('-')[2].split('T')[0];%> <% let d = new Date(parseInt(year), parseInt(month), parseInt(day)); %> value="<%= d.getFullYear() + "-" + ("0"+(d.getMonth()+1)).slice(-2) + "-" + ("0" + d.getDate()).slice(-2); %>" + <% let d1 = new Date(); %> + max="<%= (d1.getFullYear() - 18) + "-" +("0"+(d1.getMonth()+1)).slice(-2) + "-" + ("0" + d1.getDate()).slice(-2); %>" >
diff --git a/src/views/questionnaire.ejs b/src/views/questionnaire.ejs index 8304efa..ea34367 100644 --- a/src/views/questionnaire.ejs +++ b/src/views/questionnaire.ejs @@ -8,7 +8,8 @@
- + <% let d = new Date(); %> + " type="date" name="dob" class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm">