diff --git a/.env.example b/.env.example index 518df8a..255daf9 100755 --- a/.env.example +++ b/.env.example @@ -1,4 +1,7 @@ -mongoURI='mongodb://localhost:27017/' -database='nameOfDatabase' +MONGO_URI='mongodb://localhost:27017/' +DATABASE='nameOfDatabase' PORT=8000 -secret='123456789' \ No newline at end of file +SECRET='123456789' +GEOLOCATION_API='api_key' +EMAIL_USER=mail@example.com +EMAIL_PASS='password' diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index c8a7ed4..0000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "editor.fontFamily": "Monocraft", - "editor.fontLigatures": true -} \ No newline at end of file diff --git a/about.html b/about.html deleted file mode 100644 index 7c27b52..0000000 --- a/about.html +++ /dev/null @@ -1,12 +0,0 @@ - - - Team Name: BBY-14 - Team Members: - - - diff --git a/app.js b/app.js index 91045f6..ff449a4 100644 --- a/app.js +++ b/app.js @@ -10,9 +10,9 @@ require('dotenv').config(); const app = express(); const port = process.env.PORT || 3000; -const mongoURI = process.env.mongoURI; -const database = process.env.database; // Database name -const secret = process.env.secret || "123-secret-xyz"; +const mongoURI = process.env.MONGO_URI; +const database = process.env.DATABASE; // Database name +const secret = process.env.SECRET || "123-secret-xyz"; /*** Sessions ***/ app.use(session({ @@ -165,6 +165,7 @@ app.post('/api/location', async (req, res) => { }); // 404 handler - keep the actual notFound route please +// REALLY DONT DELETE THIS app.get('/notFound', (req, res) => { res.render('notFound'); return res.status(status.NotFound); diff --git a/src/auth/authentication.js b/src/auth/authentication.js index a540751..727f4e7 100644 --- a/src/auth/authentication.js +++ b/src/auth/authentication.js @@ -84,7 +84,7 @@ module.exports = (users) => { financialData: false, }).then((results, err) => { if (err) { - console.error(err); + console.error("Error creating user on signup: ", err); res.session.errMessage = "Internal server error"; return res.status(status.InternalServerError).redirect("/signup"); } diff --git a/src/auth/middleware.js b/src/auth/middleware.js index 3c12f61..5c24807 100644 --- a/src/auth/middleware.js +++ b/src/auth/middleware.js @@ -1,7 +1,7 @@ const status = require("../util/statuses"); const session = require("express-session"); -// Get all user routes names +// Get all names of user routes let userRouter = require("../router/user")((req, res, next) => next(), null, null, null); userRouter.stack.shift(); const userRoutes = userRouter.stack.map((layer) => layer.route.path.split("/")[1]); @@ -12,8 +12,9 @@ const userRoutes = userRouter.stack.map((layer) => layer.route.path.split("/")[1 */ const createMiddleware = (users) => { return async (req, res, next) => { - // Redirect not found pages to 404 page - if (!userRoutes.includes(req.url.split("/")[1])) { + // Check if incoming request route exists in user routes + // redirect to 404 if not + if (!userRoutes.includes(req.url.split("/")[1].split("?")[0])) { return res.status(status.NotFound).redirect("/notFound"); } diff --git a/src/public/scripts/assetManager.js b/src/public/scripts/assetManager.js index 86ddd08..16bbc37 100644 --- a/src/public/scripts/assetManager.js +++ b/src/public/scripts/assetManager.js @@ -8,6 +8,12 @@ const assetForms = [ * resetAll asset creation forms */ function resetAll() { + document.getElementById("dropdown-icon-button").innerHTML = ` + Other Other + + `; document.getElementById("create-other-asset-form").reset(); document.getElementById("create-saving-asset-form").reset(); document.getElementById("create-stock-asset-form").reset(); @@ -40,6 +46,7 @@ function resetRadio() { const assetKeys = { other: [ "name", + "dropdown-icon-button", "value", "description", "purchaseDate", @@ -59,8 +66,17 @@ const assetKeys = { /** * lockAsset prevents edits to asset view modal * @param {string} assetId + * @param {string} icon to defualt to */ -function lockAsset(assetId) { +function lockAsset(assetId, icon) { + let dropdown = document.getElementById(`dropdown-icon-button-${assetId}`); + if (dropdown) dropdown.innerHTML = ` + ${icon} ${icon} + + `; + document.getElementById(`${assetId}-form`).reset(); const type = document.getElementById(`type-${assetId}`).value; @@ -74,14 +90,15 @@ function lockAsset(assetId) { document.getElementById(`save-${assetId}`).classList.add("cursor-not-allowed"); document.getElementById(`edit-${assetId}`).innerHTML = "Edit"; - document.getElementById(`edit-${assetId}`).onclick = () => { unlockAsset(assetId) }; + document.getElementById(`edit-${assetId}`).onclick = () => { unlockAsset(assetId, icon) }; } /** * unlockAsset allows edits to asset view modal * @param {string} assetId + * @param {string} icon to defualt to */ -function unlockAsset(assetId) { +function unlockAsset(assetId, icon) { const type = document.getElementById(`type-${assetId}`).value; assetKeys[type].forEach((key) => { @@ -92,8 +109,40 @@ function unlockAsset(assetId) { document.getElementById(`save-${assetId}`).classList.remove("cursor-not-allowed"); document.getElementById(`save-${assetId}`).classList.add("cursor-pointer"); - document.getElementById(`edit-${assetId}`).innerHTML = "Cancel changes"; - document.getElementById(`edit-${assetId}`).onclick = () => { lockAsset(assetId) }; + document.getElementById(`edit-${assetId}`).innerHTML = "Cancel"; + document.getElementById(`edit-${assetId}`).onclick = () => { lockAsset(assetId, icon) }; +} + +/** + * autoOpenCreate checks if popup param + * in url to auto open create popup + */ +function autoOpenCreate() { + const query = window.location.search; + const params = new URLSearchParams(query); + + if (params.has("popup")) { + document.getElementById('create-asset-modal').showModal(); + } +} + +/** + * selectIcon updated selected icon while creating + * or modifying assets. + * @param {button element} selectedIcon + * @param {string} assetId + */ +function selectIcon(selectedIcon, assetId="") { + document.getElementById(`dropdown-icon-button${assetId != "" ? "-" : ""}${assetId}`).value = selectedIcon.value; + document.getElementById(`icon${assetId != "" ? "-" : ""}${assetId}`).value = selectedIcon.value + + document.getElementById(`dropdown-icon-button${assetId != "" ? "-" : ""}${assetId}`).innerHTML = selectedIcon.innerHTML + + ` + + `; } resetRadio(); +autoOpenCreate(); diff --git a/src/public/scripts/geolocation.js b/src/public/scripts/geolocation.js index 26f2e1a..7fce450 100644 --- a/src/public/scripts/geolocation.js +++ b/src/public/scripts/geolocation.js @@ -37,8 +37,6 @@ function update(data) { dropdown.appendChild(listItem); }); - - console.log(data); } function switchButton(clickedButton) { diff --git a/src/public/svgs/icons/Bike.svg b/src/public/svgs/icons/Bike.svg new file mode 100644 index 0000000..c31ed29 --- /dev/null +++ b/src/public/svgs/icons/Bike.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/public/svgs/icons/Book.svg b/src/public/svgs/icons/Book.svg new file mode 100644 index 0000000..d1cd59b --- /dev/null +++ b/src/public/svgs/icons/Book.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/public/svgs/icons/Camera.svg b/src/public/svgs/icons/Camera.svg new file mode 100644 index 0000000..4fda7d9 --- /dev/null +++ b/src/public/svgs/icons/Camera.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/public/svgs/icons/Car.svg b/src/public/svgs/icons/Car.svg new file mode 100644 index 0000000..63014de --- /dev/null +++ b/src/public/svgs/icons/Car.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/public/svgs/icons/Coins.svg b/src/public/svgs/icons/Coins.svg new file mode 100644 index 0000000..17fee69 --- /dev/null +++ b/src/public/svgs/icons/Coins.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/public/svgs/icons/Console.svg b/src/public/svgs/icons/Console.svg new file mode 100644 index 0000000..932964d --- /dev/null +++ b/src/public/svgs/icons/Console.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/public/svgs/icons/Desktop.svg b/src/public/svgs/icons/Desktop.svg new file mode 100644 index 0000000..2ad4fbd --- /dev/null +++ b/src/public/svgs/icons/Desktop.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/public/svgs/icons/Device.svg b/src/public/svgs/icons/Device.svg new file mode 100644 index 0000000..ea62d89 --- /dev/null +++ b/src/public/svgs/icons/Device.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/public/svgs/icons/Electronic.svg b/src/public/svgs/icons/Electronic.svg new file mode 100644 index 0000000..7f34f13 --- /dev/null +++ b/src/public/svgs/icons/Electronic.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/public/svgs/icons/Home.svg b/src/public/svgs/icons/Home.svg new file mode 100644 index 0000000..230751a --- /dev/null +++ b/src/public/svgs/icons/Home.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/public/svgs/icons/Laptop.svg b/src/public/svgs/icons/Laptop.svg new file mode 100644 index 0000000..eeb9595 --- /dev/null +++ b/src/public/svgs/icons/Laptop.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/public/svgs/icons/Money.svg b/src/public/svgs/icons/Money.svg new file mode 100644 index 0000000..1476d84 --- /dev/null +++ b/src/public/svgs/icons/Money.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/public/svgs/icons/Motorcycle.svg b/src/public/svgs/icons/Motorcycle.svg new file mode 100644 index 0000000..bd3776e --- /dev/null +++ b/src/public/svgs/icons/Motorcycle.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/public/svgs/icons/Other.svg b/src/public/svgs/icons/Other.svg new file mode 100644 index 0000000..31bfd17 --- /dev/null +++ b/src/public/svgs/icons/Other.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/public/svgs/icons/Phone.svg b/src/public/svgs/icons/Phone.svg new file mode 100644 index 0000000..b34a759 --- /dev/null +++ b/src/public/svgs/icons/Phone.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/public/svgs/icons/Stock.svg b/src/public/svgs/icons/Stock.svg new file mode 100644 index 0000000..3caf5b6 --- /dev/null +++ b/src/public/svgs/icons/Stock.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/router/user.js b/src/router/user.js index c5c603f..c8f67f6 100644 --- a/src/router/user.js +++ b/src/router/user.js @@ -1,9 +1,12 @@ const getRates = require("../util/exchangeRate"); +const { calculatePlanProgress, updatePlanProgressInDB } = require("../util/calculations"); const status = require("../util/statuses"); const ObjectId = require('mongodb').ObjectId; const session = require("express-session"); const bcrypt = require("bcrypt"); +const path = require("path"); const joi = require("joi"); +const fs = require("fs"); const salt = 12; /** @@ -19,6 +22,7 @@ const getAssetSchema = (type) => { case "other": 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(), value: joi.number().min(0).required(), purchaseDate: joi.date().required(), @@ -60,6 +64,10 @@ const getAssetSchema = (type) => { module.exports = (middleware, users, plans, assets) => { const router = require("express").Router(); + // Create list of icon filenames + let icons = fs.readdirSync(path.join(__dirname, "../public/svgs/icons")); + icons = icons.map((icon) => icon.split(".")[0]); + router.use(middleware); router.get('/home', async (req, res) => { @@ -82,19 +90,28 @@ module.exports = (middleware, users, plans, assets) => { user: req.session.user, errMessage: req.session.errMessage, assets: userAssets, - geoData: req.session.geoData + geoData: req.session.geoData, + icons: icons, }); return res.status(status.Ok); }); router.get('/plans', async (req, res) => { try { - // console.log(new ObjectId(req.session.user._id)); - const userPlans = await plans.find({ userId: new ObjectId(req.session.user._id) }).toArray(); - // console.log(userPlans); + const userPlansFromDB = await plans.find({ userId: new ObjectId(req.session.user._id) }).toArray(); + + // Use a for...of loop for proper async/await behavior in series for updates + for (const plan of userPlansFromDB) { + const percentage = await calculatePlanProgress(plan, assets, req.session.user._id); + await updatePlanProgressInDB(plan._id, percentage, plans); // Pass the 'plans' collection + } + + // Re-fetch plans to get updated progress for rendering + const updatedUserPlans = await plans.find({ userId: new ObjectId(req.session.user._id) }).toArray(); + res.render('plans', { user: req.session.user, - plans: userPlans, + plans: updatedUserPlans, // Send the most up-to-date plans geoData: req.session.geoData }); } catch (err) { @@ -108,6 +125,7 @@ module.exports = (middleware, users, plans, assets) => { try { const planId = req.params.id; + let userAssets = await assets.find({ userId: new ObjectId(req.session.user._id) }).toArray(); if (!ObjectId.isValid(planId)) { @@ -122,11 +140,19 @@ module.exports = (middleware, users, plans, assets) => { req.session.errMessage = "Plan not found or you do not have permission to view it."; return res.status(status.NotFound).redirect('/plans'); } - // console.log("Found plan:", plan); + + // The plan.progress should be up-to-date from the database as it was updated in the /plans route + // or when assets/plans are modified. If an immediate recalculation for this specific view is absolutely needed, + // (e.g., if assets were modified without an immediate plan progress update elsewhere), + // you could do it here: + // const currentProgress = await calculatePlanProgress(plan, assets, req.session.user._id); + // plan.progress = currentProgress; // This would only update the 'plan' object for this render, not in DB + res.render('planDetail', { user: req.session.user, - plan: plan, - geoData: req.session.geoData + plan: plan, // This plan object will have the progress from the database + geoData: req.session.geoData, + assets: userAssets, }); } catch (err) { @@ -175,7 +201,7 @@ module.exports = (middleware, users, plans, assets) => { retirementExpenses: value.retirementExpenses, retirementAssets: value.retirementAssets, retirementLiabilities: value.retirementLiabilities, - progress: "0%" + progress: "0" }; try { @@ -226,8 +252,6 @@ module.exports = (middleware, users, plans, assets) => { }); router.post('/questionnaire', (req, res) => { - // console.log("Questionnaire POST body:", req.body); - const questionnaireSchema = joi.object({ dob: joi.date().required(), education: joi.string().valid('primary', 'secondary', 'tertiary', 'postgraduate').required(), @@ -368,6 +392,7 @@ module.exports = (middleware, users, plans, assets) => { newAsset.name = `${newAsset.ticker} Stock`; } newAsset.value = parseFloat(newAsset.value); + newAsset.icon = type == "stock" ? "Stock" : type == "saving" ? "Coins" : newAsset.icon; assets.insertOne(newAsset, (err, _) => { if (err) { @@ -413,7 +438,8 @@ module.exports = (middleware, users, plans, assets) => { update.name = `${update.ticker} Stock`; } update.value = parseFloat(update.value); - delete update.id + delete update.id; + delete update.userId; assets.updateOne( { "_id": new ObjectId(req.body.id) }, @@ -504,9 +530,10 @@ module.exports = (middleware, users, plans, assets) => { return res.status(status.InternalServerError).redirect("/profile"); }); }); + router.get("/exRates/:lat/:lon", async (req, res) => { if (!req.session.geoData.country) { - const response = await fetch(`https://maps.googleapis.com/maps/api/geocode/json?latlng=${req.params.lat},${req.params.lon}&result_type=country&key=${process.env.geolocation_api}`); + const response = await fetch(`https://maps.googleapis.com/maps/api/geocode/json?latlng=${req.params.lat},${req.params.lon}&result_type=country&key=${process.env.GEOLOCATION_API}`); const data = await response.json(); country = data.results[0].formatted_address; diff --git a/src/util/calculations.js b/src/util/calculations.js new file mode 100644 index 0000000..25f5d33 --- /dev/null +++ b/src/util/calculations.js @@ -0,0 +1,45 @@ +const ObjectId = require('mongodb').ObjectId; + +async function calculatePlanProgress(plans, assets, userId) { + if (!plans || typeof plans.retirementAssets === 'undefined') { + console.error("Invalid plan document provided to calculatePlanProgress:", plans); + return 0; + } + if (!assets || typeof assets.find !== 'function') { + console.error("Invalid assetsCollection provided to calculatePlanProgress"); + return 0; + } + + try { + const userAssets = await assets.find({ userId: new ObjectId(userId) }).toArray(); + const totalUserAssetValue = userAssets.reduce((total, asset) => total + asset.value, 0); + let percentage = 0; + + if (plans.retirementAssets > 0) { + percentage = (totalUserAssetValue / plans.retirementAssets) * 100; + } + return percentage; + } catch (err) { + console.error("Error in calculatePlanProgress:", err); + return 0; + } +} + +async function updatePlanProgressInDB(planId, percentage, plans) { + if (!plans || typeof plans.updateOne !== 'function') { + console.error("Error with the plans collection"); + return; + } + if (typeof percentage !== 'number' || isNaN(percentage)) { + console.error(`Error with the percentage: ${percentage}`); + return; + } + + try { + await plans.updateOne({ _id: new ObjectId(planId) }, { $set: { progress: parseFloat(percentage.toFixed(2)) } }); + } catch (err) { + console.error("Error in updatePlanProgressInDB:", err); + } +} + +module.exports = { calculatePlanProgress, updatePlanProgressInDB }; \ No newline at end of file diff --git a/src/views/assets.ejs b/src/views/assets.ejs index cec34b0..0b6eae4 100644 --- a/src/views/assets.ejs +++ b/src/views/assets.ejs @@ -2,7 +2,6 @@ <%- include("./partials/header") %>
-
@@ -38,7 +37,31 @@
- + + + + +
+ + +
@@ -178,7 +201,9 @@ + +
+ <% } else { %> + + <% } %> + +

<%= String(asset.type).charAt(0).toUpperCase() + String(asset.type).slice(1); %> Asset

+
+ <% if (asset.type != "stock") { %> @@ -328,16 +381,12 @@ <%- include("./partials/navBar") %> +<%- include("./partials/scriptLoader") %> <%- include("./partials/footer") %> \ No newline at end of file diff --git a/src/views/dashboard.ejs b/src/views/dashboard.ejs index 214d975..d36b979 100644 --- a/src/views/dashboard.ejs +++ b/src/views/dashboard.ejs @@ -1,15 +1,26 @@ <%- include("./partials/fileHeader") %> <%- include("./partials/header") %> -
- -
- - Create new plan +
+ + + +
+
+ +
+ + + +
+

+ Retirement goal

+

+ $53k

@@ -99,15 +110,6 @@
- - - - - - - <%- include("./partials/navBar") %> - <%- include("./partials/footer") %> +<%- include("./partials/navBar") %> +<%- include("./partials/scriptLoader") %> +<%- include("./partials/footer") %> diff --git a/src/views/more.ejs b/src/views/more.ejs index 59e3acf..934700e 100644 --- a/src/views/more.ejs +++ b/src/views/more.ejs @@ -6,5 +6,6 @@
<%- 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 index a05dd1a..5370290 100644 --- a/src/views/newPlan.ejs +++ b/src/views/newPlan.ejs @@ -37,4 +37,6 @@
+<%- include("./partials/navBar") %> +<%- include("./partials/scriptLoader") %> <%- include("./partials/footer") %> \ No newline at end of file diff --git a/src/views/partials/footer.ejs b/src/views/partials/footer.ejs index 110cbe0..9ee0b91 100644 --- a/src/views/partials/footer.ejs +++ b/src/views/partials/footer.ejs @@ -6,5 +6,6 @@ + - \ No newline at end of file + diff --git a/src/views/partials/scriptLoader.ejs b/src/views/partials/scriptLoader.ejs new file mode 100644 index 0000000..afe997b --- /dev/null +++ b/src/views/partials/scriptLoader.ejs @@ -0,0 +1,8 @@ + + + + diff --git a/src/views/planDetail.ejs b/src/views/planDetail.ejs index 8b430f1..e75b46f 100644 --- a/src/views/planDetail.ejs +++ b/src/views/planDetail.ejs @@ -12,10 +12,10 @@
Progress - <%= plan.progress %> + <%= plan.progress %>%
-
+
@@ -25,11 +25,11 @@
-

16 MOCK

+

<%= assets.length %>

-

$165,000 MOCK

+

<%= assets.reduce((total, asset) => total + asset.value, 0) %>

@@ -71,4 +71,5 @@ <%- include("./partials/navBar") %> +<%- include("./partials/scriptLoader") %> <%- include("./partials/footer") %> diff --git a/src/views/plans.ejs b/src/views/plans.ejs index ce7ca80..5926a22 100644 --- a/src/views/plans.ejs +++ b/src/views/plans.ejs @@ -11,7 +11,7 @@
<%= plan.name %>
-
+

<%= plan.description %>

@@ -21,4 +21,5 @@ <%- include("./partials/navBar") %> +<%- include("./partials/scriptLoader") %> <%- include("./partials/footer") %> diff --git a/src/views/profile.ejs b/src/views/profile.ejs index b1a45f3..b8a8c3d 100644 --- a/src/views/profile.ejs +++ b/src/views/profile.ejs @@ -138,4 +138,5 @@ <%- include("./partials/navBar") %> +<%- include("./partials/scriptLoader") %> <%- include("./partials/footer") %> \ No newline at end of file diff --git a/src/views/questionnaire.ejs b/src/views/questionnaire.ejs index 993a8c1..84420e6 100644 --- a/src/views/questionnaire.ejs +++ b/src/views/questionnaire.ejs @@ -68,4 +68,6 @@ +<%- include("./partials/navBar") %> +<%- include("./partials/scriptLoader") %> <%- include("./partials/footer") %> \ No newline at end of file