diff --git a/src/public/scripts/assetManager.js b/src/public/scripts/assetManager.js new file mode 100644 index 0000000..4f89c24 --- /dev/null +++ b/src/public/scripts/assetManager.js @@ -0,0 +1,90 @@ +const assetForms = [ + "create-other-asset-form", + "create-saving-asset-form", + "create-stock-asset-form" +]; + +/** + * selectAssetForm changes which form is displayed + * to create specified assets. + * @param {string} assetFormId + */ +function selectAssetForm(assetFormId) { + for (let i = 0; i < assetForms.length; i++) { + if (assetForms[i] == assetFormId) { + document.getElementById(assetFormId).style.display = "block"; + } else { + document.getElementById(assetForms[i]).style.display = "none"; + } + } +} + +/** + * resetRadio to default to other asset type. + */ +function resetRadio() { + document.getElementById("asset-other").checked = true; + document.getElementById("asset-saving").checked = false; + document.getElementById("asset-stock").checked = false; +} + +const assetKeys = { + other: [ + "name", + "value", + "description", + "purchaseDate", + ], + saving: [ + "name", + "value", + ], + stock: [ + "ticker", + "price", + "quantity", + "purchaseDate", + ], +} + +/** + * lockAsset prevents edits to asset view modal + * @param {string} assetId + */ +function lockAsset(assetId) { + document.getElementById(`${assetId}-form`).reset(); + + const type = document.getElementById(`type-${assetId}`).value; + + assetKeys[type].forEach((key) => { + document.getElementById(`${key}-${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"); + + document.getElementById(`edit-${assetId}`).innerHTML = "Edit"; + document.getElementById(`edit-${assetId}`).onclick = () => { unlockAsset(assetId) }; +} + +/** + * unlockAsset allows edits to asset view modal + * @param {string} assetId + */ +function unlockAsset(assetId) { + const type = document.getElementById(`type-${assetId}`).value; + + assetKeys[type].forEach((key) => { + document.getElementById(`${key}-${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"); + + document.getElementById(`edit-${assetId}`).innerHTML = "Cancel changes"; + document.getElementById(`edit-${assetId}`).onclick = () => { lockAsset(assetId) }; +} + +resetRadio(); diff --git a/src/public/scripts/selectAssetForm.js b/src/public/scripts/selectAssetForm.js deleted file mode 100644 index b40a946..0000000 --- a/src/public/scripts/selectAssetForm.js +++ /dev/null @@ -1,31 +0,0 @@ -const assetForms = [ - "create-other-asset-form", - "create-saving-asset-form", - "create-stock-asset-form" -]; - -/** - * selectAssetForm changes which form is displayed - * to create specified assets. - * @param {string} assetFormId - */ -function selectAssetForm(assetFormId) { - for (let i = 0; i < assetForms.length; i++) { - if (assetForms[i] == assetFormId) { - document.getElementById(assetFormId).style.display = "block"; - } else { - document.getElementById(assetForms[i]).style.display = "none"; - } - } -} - -/** - * resetRadio to default to other asset type. - */ -function resetRadio() { - document.getElementById("asset-other").checked = true; - document.getElementById("asset-saving").checked = false; - document.getElementById("asset-stock").checked = false; -} - -resetRadio(); diff --git a/src/router/user.js b/src/router/user.js index 8310630..893ad4b 100644 --- a/src/router/user.js +++ b/src/router/user.js @@ -1,8 +1,57 @@ const status = require("../util/statuses"); -const bcrypt = require('bcrypt'); +const ObjectId = require('mongodb').ObjectId; +const bcrypt = require("bcrypt"); const joi = require("joi"); const salt = 12; +/** + * getAssetSchema returns correct joi object + * to validate req.body depending on asset type + * @param {string} type of asset + * @returns {joi.object} schema + */ +const getAssetSchema = (type) => { + let assetSchema; + + switch (type) { + case "other": + assetSchema = joi.object({ + ownerId: joi.string().alphanum().required(), + type: joi.string().valid("other", "stock", "saving").required(), + 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), + id: joi.string().alphanum(), // May be passed when updating existing asset + }); + break; + case "saving": + assetSchema = joi.object({ + ownerId: joi.string().alphanum().required(), + type: joi.string().valid("other", "stock", "saving").required(), + 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 + }); + break; + case "stock": + assetSchema = joi.object({ + ownerId: joi.string().alphanum().required(), + type: joi.string().valid("other", "stock", "saving").required(), + ticker: joi.string().alphanum().min(3).max(5).required(), + price: joi.number().min(0).required(), + quantity: joi.number().min(1).required(), + purchaseDate: joi.date().required(), + id: joi.string().alphanum(), // May be passed when updating existing asset + }); + break; + default: + return null; + }; + + return assetSchema; +} + /** * @param {function} middleware handler * @param {MongoClient.collection} users db collection @@ -169,43 +218,14 @@ module.exports = (middleware, users, assets) => { router.post("/createAsset", async (req, res) => { // Create asset, each asset has different data structure based on type - const type = req.body.assetType; - let assetSchema; + const type = req.body.type; + const assetSchema = getAssetSchema(type); - switch (type) { - case "other": - assetSchema = joi.object({ - ownerId: joi.string().alphanum().required(), - type: joi.string().valid("other", "stock", "saving").required(), - 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), - }); - break; - case "saving": - assetSchema = joi.object({ - ownerId: joi.string().alphanum().required(), - type: joi.string().valid("other", "stock", "saving").required(), - name: joi.string().alphanum().min(3).max(30).required(), - value: joi.number().min(0).required(), - }); - break; - case "stock": - assetSchema = joi.object({ - ownerId: joi.string().alphanum().required(), - type: joi.string().valid("other", "stock", "saving").required(), - ticker: joi.string().alphanum().min(3).max(5).required(), - price: joi.number().min(0).required(), - quantity: joi.number().min(1).required(), - purchaseDate: joi.date().required(), - }); - break; - default: - console.error("Modified asset type, rejected"); - req.session.errMessage = "Invalid input"; - return res.status(status.BadRequest).redirect("/assets"); - }; + if (!assetSchema) { + console.error("Modified asset type, rejected"); + req.session.errMessage = "Invalid input"; + return res.status(status.BadRequest).redirect("/assets"); + } const valid = assetSchema.validate(req.body); @@ -227,9 +247,8 @@ module.exports = (middleware, users, assets) => { newAsset.name = `${newAsset.ticker} Stock`; } newAsset.value = parseFloat(newAsset.value); - newAsset.type = type; - assets.insertOne(newAsset, (err, result) => { + assets.insertOne(newAsset, (err, _) => { if (err) { console.error("Error creating asset: ", err); req.session.errMessage = "Internal server error"; @@ -238,7 +257,92 @@ module.exports = (middleware, users, assets) => { }); req.session.errMessage = ""; - return res.status(status.Ok).redirect("/assets"); + return res.status(status.Ok).redirect("/assets"); + }); + + router.post("/updateAsset", async (req, res) => { + const type = req.body.type; + const assetSchema = getAssetSchema(type); + + if (!assetSchema) { + console.error("Modified asset type, rejected"); + req.session.errMessage = "Invalid input"; + return res.status(status.BadRequest).redirect("/assets"); + } + + const valid = assetSchema.validate(req.body); + + if (valid.err) { + req.session.errMessage = "Invalid input", + res.status(status.BadRequest); + return res.redirect("/assets"); + } + + if (req.body.ownerId != req.session.user._id) { + req.session.errMessage = "Cannot change asset owner", + res.status(status.BadRequest); + return res.redirect("/assets"); + } + + let update = { ...req.body, updatedAt: new Date() }; + if (type == "stock") { + update.quantity = parseInt(update.quantity); + update.price = parseFloat(update.price) + update.value = update.quantity * update.price; + update.name = `${update.ticker} Stock`; + } + update.value = parseFloat(update.value); + delete update.id + + assets.updateOne( + { "_id": new ObjectId(req.body.id) }, + { $set: update }, + ).then((result) => { + if (result.matchedCount === 0) { + console.log(`Asset not found: ${req.body.id}`); + req.session.errMessage = "Unable to update asset"; + return res.status(status.NotFound).redirect("/assets"); + } + if (result.modifiedCount === 0 && result.matchedCount === 1) { + console.log(`Asset data unchanged (already up-to-date): ${req.body.id}`); + } + + req.session.errMessage = ""; + return res.status(status.Ok).redirect("/assets"); + + }).catch((err) => { + console.error("Error updating asset: ", err); + req.session.errMessage = "An error occurred while saving your information. Please try again."; + return res.status(status.InternalServerError).redirect("/assets"); + }); + }); + + router.post("/deleteAsset", (req, res) => { + const id = new ObjectId(req.body.id); + + assets.deleteOne( + { "_id": id } + ).then((result) => { + if (result.deletedCount === 0) { + console.log(`Asset not found: ${req.body.id}`); + req.session.errMessage = "Unable to delete asset"; + return res.status(status.NotFound).redirect("/assets"); + } + + if (!result.acknowledged) { + console.error("Error updating asset: ", err); + req.session.errMessage = "An error occurred while saving your information. Please try again."; + return res.status(status.InternalServerError).redirect("/assets"); + } + + req.session.errMessage = ""; + return res.status(status.Ok).redirect("/assets"); + + }).catch((err) => { + console.error("Error updating asset: ", err); + req.session.errMessage = "An error occurred while saving your information. Please try again."; + return res.status(status.InternalServerError).redirect("/assets"); + }); }); return router; diff --git a/src/views/assets.ejs b/src/views/assets.ejs index 4d4417b..004509d 100644 --- a/src/views/assets.ejs +++ b/src/views/assets.ejs @@ -8,7 +8,11 @@

Create New Asset

-
@@ -18,15 +22,15 @@
@@ -35,7 +39,7 @@
- + @@ -53,10 +57,14 @@ -
@@ -65,7 +73,7 @@ - + @@ -77,10 +85,14 @@ - @@ -89,7 +101,7 @@ - + @@ -107,16 +119,21 @@ - +
@@ -147,33 +164,182 @@
+
<% if (assets.length == 0) { %> - -
- You have no assets, create a new asset above. -
-
+ <% } %> <% assets.forEach((asset) => { %> - -
+ +
+ +
+ + + +
+ +
+
+ +
+ +

Are you sure you want to delete this item?

+
+
+
+ +
+
+
+
+ + +
+
+
-
+ + + + +
+
+
+ +
+
+
+ +
+
+
+ +
+
+ ICON +
+
+ +
+
+ +
+ + + + + <% if (asset.type != "stock") { %> + + + <% } else { %> + + + <% } %> + +
+ + <% if (asset.type != "stock") { %> + + + <% } else { %> + + + + + + <% } %> + + <% if (asset.type == "other") { %> + + + <% } %> + + <% if (asset.type != "saving") { %> + + + <% } %> + +
+ Last Modified: <%= asset.updatedAt %> +
+ +
+ +
+
+
<% }) %>
- +
+ + + <%- include("./partials/navBar") %> <%- include("./partials/footer") %> \ No newline at end of file