Merge pull request #37 from JoaquinPar/refactor/plans
refactor/ new plan as a modal pop up. Deletion and edition on plan cr…
This commit is contained in:
7 files changed
+414
-73
No files matched your search
@@ -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.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+121
-24
@@ -1,5 +1,5 @@
|
|||||||
const getRates = require("../util/exchangeRate");
|
const getRates = require("../util/exchangeRate");
|
||||||
const { calculateProgress, updatePlanProgressInDB } = require("../util/calculations");
|
const { calculateProgress, updatePlanProgressInDB, updateProgress } = require("../util/calculations");
|
||||||
const suggestions = require("../util/suggestions");
|
const suggestions = require("../util/suggestions");
|
||||||
const status = require("../util/statuses");
|
const status = require("../util/statuses");
|
||||||
const ObjectId = require('mongodb').ObjectId;
|
const ObjectId = require('mongodb').ObjectId;
|
||||||
@@ -118,16 +118,9 @@ module.exports = (middleware, users, plans, assets) => {
|
|||||||
|
|
||||||
router.get('/plans', async (req, res) => {
|
router.get('/plans', async (req, res) => {
|
||||||
try {
|
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', {
|
res.render('plans', {
|
||||||
user: req.session.user,
|
user: req.session.user,
|
||||||
plans: updatedUserPlans,
|
plans: updatedUserPlans,
|
||||||
@@ -150,23 +143,21 @@ module.exports = (middleware, users, plans, assets) => {
|
|||||||
req.session.errMessage = "Invalid plan ID format.";
|
req.session.errMessage = "Invalid plan ID format.";
|
||||||
return res.status(status.BadRequest).redirect('/plans');
|
return res.status(status.BadRequest).redirect('/plans');
|
||||||
}
|
}
|
||||||
|
|
||||||
const plan = await plans.findOne({ userId: new ObjectId(req.session.userId), _id: new ObjectId(planId) });
|
const plan = await plans.findOne({ userId: new ObjectId(req.session.userId), _id: new ObjectId(planId) });
|
||||||
|
|
||||||
if (!plan) {
|
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.";
|
req.session.errMessage = "Plan not found or you do not have permission to view it.";
|
||||||
return res.status(status.NotFound).redirect('/plans');
|
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', {
|
res.render('planDetail', {
|
||||||
user: req.session.user,
|
user: req.session.user,
|
||||||
plan: plan,
|
plan: plan,
|
||||||
geoData: req.session.geoData,
|
geoData: req.session.geoData,
|
||||||
assets: userAssets,
|
assets: userAssets,
|
||||||
progress: progress,
|
progress: progress,
|
||||||
suggestions: await suggestions.generateSuggestions(),
|
suggestions: await suggestions.generateSuggestions(),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -205,29 +196,134 @@ module.exports = (middleware, users, plans, assets) => {
|
|||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
console.error("Plan validation error:", error.details);
|
console.error("Plan validation error:", error.details);
|
||||||
req.session.errMessage = "Invalid input: " + error.details.map(d => d.message.replace(/"/g, '')).join(', ');
|
const errorMessage = "Invalid input: " + error.details.map(d => d.message.replace(/"/g, '')).join(', ');
|
||||||
res.status(status.BadRequest).redirect("/newPlan");
|
res.status(status.BadRequest).json({ success: false, message: errorMessage });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const newPlan = {
|
|
||||||
|
const planToInsert = {
|
||||||
userId: new ObjectId(req.session.userId),
|
userId: new ObjectId(req.session.userId),
|
||||||
name: value.name,
|
name: value.name,
|
||||||
retirementAge: value.retirementAge,
|
retirementAge: value.retirementAge,
|
||||||
retirementExpenses: value.retirementExpenses,
|
retirementExpenses: value.retirementExpenses,
|
||||||
retirementAssets: value.retirementAssets,
|
retirementAssets: value.retirementAssets,
|
||||||
retirementLiabilities: value.retirementLiabilities,
|
retirementLiabilities: value.retirementLiabilities,
|
||||||
progress: "0"
|
progress: "0"
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await plans.insertOne({ userId: new ObjectId(req.session.userId), ...newPlan });
|
await plans.insertOne(planToInsert);
|
||||||
req.session.errMessage = "";
|
res.status(status.Ok).json({ success: true, message: "Plan created successfully." });
|
||||||
res.redirect('/plans');
|
|
||||||
}
|
}
|
||||||
catch (err) {
|
catch (err) {
|
||||||
console.error("Error saving plan:", err);
|
console.error("Error saving plan:", err);
|
||||||
req.session.errMessage = "An error occurred while saving your plan. Please try again.";
|
res.status(status.InternalServerError).json({ success: false, message: "An error occurred while saving your plan. Please try again." });
|
||||||
res.status(status.InternalServerError).redirect("/newPlan");
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
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");
|
return res.status(status.Ok).redirect("/profile");
|
||||||
});
|
});
|
||||||
|
|
||||||
}).catch(err => {
|
}).catch(err => {
|
||||||
console.error("Error updating account in database:", err);
|
console.error("Error updating account in database:", err);
|
||||||
req.session.errMessage = "An error occurred while saving your information. Please try again.";
|
req.session.errMessage = "An error occurred while saving your information. Please try again.";
|
||||||
|
|||||||
@@ -67,15 +67,17 @@ async function calculateProgress(plan, assets, users, userId) {
|
|||||||
const totalUserPlanValue = plan.retirementAssets;
|
const totalUserPlanValue = plan.retirementAssets;
|
||||||
|
|
||||||
const userDoc = await users.findOne({ _id: new ObjectId(userId) });
|
const userDoc = await users.findOne({ _id: new ObjectId(userId) });
|
||||||
|
|
||||||
if (!userDoc || !userDoc.dob) {
|
if (!userDoc || !userDoc.dob) {
|
||||||
console.error("User document or DOB not found for userId:", userId);
|
console.error("[calculateProgress] User document or DOB not found for userId:", userId);
|
||||||
return { monthlyInvestment: NaN, totalCostOfRetirement: NaN, monthsUntilRetirement: NaN, yearsRetired: NaN, percentage: NaN };
|
// 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);
|
const userDob = new Date(userDoc.dob);
|
||||||
|
|
||||||
if (isNaN(userDob.getTime())) {
|
if (isNaN(userDob.getTime())) {
|
||||||
console.error("userDob is an invalid date. Aborting calculation.");
|
console.error("[calculateProgress] userDob is an invalid date. Aborting calculation.");
|
||||||
return { monthlyInvestment: NaN, totalCostOfRetirement: NaN, monthsUntilRetirement: NaN, yearsRetired: NaN, percentage: NaN };
|
return { monthlyInvestment: NaN, totalCostOfRetirement: NaN, monthsUntilRetirement: NaN, yearsRetired: NaN, yearsUntilRetirement: NaN, percentage: NaN };
|
||||||
}
|
}
|
||||||
|
|
||||||
const today = new Date();
|
const today = new Date();
|
||||||
@@ -104,10 +106,23 @@ async function calculateProgress(plan, assets, users, userId) {
|
|||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Error in calculateProgress:", 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};
|
|
||||||
|
module.exports = { calculatePlanProgress, updatePlanProgressInDB, calculateProgress, updateProgress};
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
<%- include('./partials/fileHeader') %>
|
||||||
|
<%- include('./partials/header') %>
|
||||||
|
|
||||||
|
<main class="container mx-auto p-4 pt-28">
|
||||||
|
<div class="max-w-2xl mx-auto bg-gray-800 p-6 sm:p-8 rounded-xl shadow-xl">
|
||||||
|
<h2 class="text-2xl font-bold text-center text-white mb-8">Edit Plan: <%= plan.name %></h2>
|
||||||
|
|
||||||
|
<form id="editPlanForm" action="/plans/<%= plan._id %>/edit" method="POST" class="space-y-6">
|
||||||
|
<div>
|
||||||
|
<label for="name" class="block text-sm font-medium text-gray-300">Plan Name</label>
|
||||||
|
<input type="text" name="name" id="name" value="<%= plan.name %>" required
|
||||||
|
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-indigo-500 focus:border-indigo-500 sm:text-sm placeholder-gray-400">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label for="retirementAge" class="block text-sm font-medium text-gray-300">Desired Retirement Age</label>
|
||||||
|
<input type="number" name="retirementAge" id="retirementAge" value="<%= plan.retirementAge %>" required min="18"
|
||||||
|
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-indigo-500 focus:border-indigo-500 sm:text-sm placeholder-gray-400">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label for="retirementExpenses" class="block text-sm font-medium text-gray-300">Estimated Monthly Expenses at Retirement (<%= geoData.currency || 'CAD' %>)</label>
|
||||||
|
<input type="number" name="retirementExpenses" id="retirementExpenses" value="<%= plan.retirementExpenses %>" required min="0" step="any"
|
||||||
|
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-indigo-500 focus:border-indigo-500 sm:text-sm placeholder-gray-400">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label for="retirementAssets" class="block text-sm font-medium text-gray-300">Target Retirement Assets (<%= geoData.currency || 'CAD' %>)</label>
|
||||||
|
<input type="number" name="retirementAssets" id="retirementAssets" value="<%= plan.retirementAssets %>" required min="0" step="any"
|
||||||
|
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-indigo-500 focus:border-indigo-500 sm:text-sm placeholder-gray-400">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label for="retirementLiabilities" class="block text-sm font-medium text-gray-300">Estimated Monthly Liabilities at Retirement (<%= geoData.currency || 'CAD' %>)</label>
|
||||||
|
<input type="number" name="retirementLiabilities" id="retirementLiabilities" value="<%= plan.retirementLiabilities %>" required min="0" step="any"
|
||||||
|
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-indigo-500 focus:border-indigo-500 sm:text-sm placeholder-gray-400">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="formErrorMessages" class="text-red-400 text-sm my-2" style="display: none;"></div>
|
||||||
|
|
||||||
|
<div class="flex items-center justify-end space-x-4 pt-4">
|
||||||
|
<a href="/plans/<%= plan._id %>" class="inline-flex justify-center py-2 px-4 border border-gray-600 rounded-md shadow-sm bg-gray-600 text-sm font-medium text-white hover:bg-gray-500 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-gray-800 focus:ring-indigo-500">
|
||||||
|
Cancel
|
||||||
|
</a>
|
||||||
|
<button type="submit"
|
||||||
|
class="inline-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-offset-gray-800 focus:ring-indigo-500">
|
||||||
|
Save Changes
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Basic client-side form submission handler to process JSON response
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
const form = document.getElementById('editPlanForm');
|
||||||
|
const errorMessagesDiv = document.getElementById('formErrorMessages');
|
||||||
|
|
||||||
|
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';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<%- include('./partials/navBar') %>
|
||||||
|
<%- include('./partials/scriptLoader') %>
|
||||||
|
<%- include('./partials/footer') %>
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
<%- include("./partials/fileHeader") %>
|
|
||||||
<%- include("./partials/header") %>
|
|
||||||
|
|
||||||
<main class="container mx-auto p-4 pt-30">
|
|
||||||
<div class="max-w-md mx-auto bg-white p-8 mb-10 rounded-lg shadow-md">
|
|
||||||
<h3 class="text-lg font-medium mb-6">Retirement Plan</h3>
|
|
||||||
<form action="/newPlan" method="post" class="space-y-4">
|
|
||||||
<div>
|
|
||||||
<label for="name" class="block text-sm font-medium text-gray-700">Plan Name</label>
|
|
||||||
<input type="text" name="name" placeholder="e.g., Retirement Plan" 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">
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label for="retirementAge" class="block text-sm font-medium text-gray-700">Desired Retirement Age</label>
|
|
||||||
<input type="number" name="retirementAge" placeholder="e.g., 65" min="18" max="120" 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">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label for="retirementExpenses" class="block text-sm font-medium text-gray-700">Estimated Monthly Retirement Expenses</label>
|
|
||||||
<input type="number" name="retirementExpenses" min="0" placeholder="e.g., 3000" 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">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label for="retirementAssets" class="block text-sm font-medium text-gray-700">Estimated Retirement Assets</label>
|
|
||||||
<input type="number" name="retirementAssets" min="0" placeholder="e.g., 500000" 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">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label for="retirementLiabilities" class="block text-sm font-medium text-gray-700">Estimated Retirement Liabilities</label>
|
|
||||||
<input type="number" name="retirementLiabilities" min="0" placeholder="e.g., 50000" 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">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<button 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">Save Plan</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
|
|
||||||
<%- include("./partials/navBar") %>
|
|
||||||
<%- include("./partials/scriptLoader") %>
|
|
||||||
<%- include("./partials/footer") %>
|
|
||||||
@@ -87,10 +87,16 @@
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="flex items-center justify-center space-x-2">
|
||||||
|
<button type="button" class="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-pointer" onclick="editPlan('<%= plan._id %>')">Edit Plan</button>
|
||||||
|
<button type="button" class="py-2 px-4 border border-red-600 rounded-md shadow-sm text-sm font-medium text-white bg-red-600 hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 cursor-pointer" onclick="deletePlan('<%= plan._id %>')">Delete Plan</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
<script src="/static/scripts/planManager.js"></script>
|
||||||
|
|
||||||
<%- include("./partials/navBar") %>
|
<%- include("./partials/navBar") %>
|
||||||
<%- include("./partials/scriptLoader") %>
|
<%- include("./partials/scriptLoader") %>
|
||||||
<%- include("./partials/footer") %>
|
<%- include("./partials/footer") %>
|
||||||
+125
-2
@@ -4,7 +4,8 @@
|
|||||||
<main class="container mx-auto p-4 pt-28">
|
<main class="container mx-auto p-4 pt-28">
|
||||||
<div class="max-w-md mx-auto bg-white p-8 mb-10 rounded-lg shadow-md space-y-4">
|
<div class="max-w-md mx-auto bg-white p-8 mb-10 rounded-lg shadow-md space-y-4">
|
||||||
<h3 class="text-lg font-medium mb-6">My Retirement Plans</h3>
|
<h3 class="text-lg font-medium mb-6">My Retirement Plans</h3>
|
||||||
<a href="/newPlan" class="block max-w-sm p-2"><div class="text-center bg-blue-600 text-white py-1 w-full rounded-md hover:bg-blue-700 hover:text-white font-semibold">New Plan</div></a>
|
<!-- Changed link to a button to open modal -->
|
||||||
|
<button id="openNewPlanModalBtn" class="block w-full max-w-sm p-2"><div class="text-center bg-blue-600 text-white py-1 w-full rounded-md hover:bg-blue-700 hover:text-white font-semibold">New Plan</div></button>
|
||||||
<hr class="mt-3">
|
<hr class="mt-3">
|
||||||
<% plans.forEach(plan => { %>
|
<% plans.forEach(plan => { %>
|
||||||
<a href="/plans/<%= plan._id %>" class="block max-w-sm p-6 border rounded-lg shadow-sm bg-gray-800 border-gray-700 hover:bg-gray-700">
|
<a href="/plans/<%= plan._id %>" class="block max-w-sm p-6 border rounded-lg shadow-sm bg-gray-800 border-gray-700 hover:bg-gray-700">
|
||||||
@@ -16,9 +17,131 @@
|
|||||||
</a>
|
</a>
|
||||||
<% }) %>
|
<% }) %>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- New Plan Modal - Styled like createAsset.ejs -->
|
||||||
|
<dialog id="newPlanDialog" class="w-lg mx-auto bg-white p-8 mt-10 rounded-lg shadow-md">
|
||||||
|
<!-- Header: Title and Cancel button -->
|
||||||
|
<div class="flex flex-row justify-between items-center mb-4">
|
||||||
|
<h3 class="text-lg font-bold text-gray-900">Create New Retirement Plan</h3>
|
||||||
|
<form method="dialog" id="cancelNewPlanDialogForm">
|
||||||
|
<button type="submit" class="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-pointer">Cancel</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Form Content -->
|
||||||
|
<div>
|
||||||
|
<form id="newPlanForm" class="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label for="name" class="block text-sm font-medium text-gray-700 text-left">Plan Name</label>
|
||||||
|
<input type="text" name="name" id="planName" placeholder="e.g., My Awesome Plan" 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" required>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="retirementAge" class="block text-sm font-medium text-gray-700 text-left">Desired Retirement Age</label>
|
||||||
|
<input type="number" name="retirementAge" id="retirementAge" placeholder="e.g., 65" min="18" max="120" 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" required>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="retirementExpenses" class="block text-sm font-medium text-gray-700 text-left">Estimated Monthly Retirement Expenses</label>
|
||||||
|
<input type="number" name="retirementExpenses" id="retirementExpenses" min="0" placeholder="e.g., 3000" 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" required>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="retirementAssets" class="block text-sm font-medium text-gray-700 text-left">Estimated Retirement Assets</label>
|
||||||
|
<input type="number" name="retirementAssets" id="retirementAssets" min="0" placeholder="e.g., 500000" 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" required>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="retirementLiabilities" class="block text-sm font-medium text-gray-700 text-left">Estimated Retirement Liabilities</label>
|
||||||
|
<input type="number" name="retirementLiabilities" id="retirementLiabilities" min="0" placeholder="e.g., 50000" 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" required>
|
||||||
|
</div>
|
||||||
|
<div id="newPlanError" class="text-red-500 text-sm mt-2" style="display: none;"></div>
|
||||||
|
<!-- Form Buttons: Save Plan and Reset -->
|
||||||
|
<div class="mt-6 flex flex-row justify-between">
|
||||||
|
<button type="button" id="resetNewPlanFormBtn" class="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">Reset</button>
|
||||||
|
<button id="submitNewPlanBtn" type="submit" class="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">Save Plan</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</dialog>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<%- include("./partials/navBar") %>
|
<%- include("./partials/navBar") %>
|
||||||
<%- include("./partials/scriptLoader") %>
|
<%- include("./partials/scriptLoader") %>
|
||||||
|
<script>
|
||||||
|
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';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
<%- include("./partials/footer") %>
|
<%- include("./partials/footer") %>
|
||||||
Reference in new issue
Block a user