Va and project List API update

This commit is contained in:
=bhagwanSingh 2025-12-05 13:29:04 +05:30
parent 59561f211d
commit 00cb793a7c
13 changed files with 169 additions and 134 deletions

View File

@ -6,7 +6,7 @@
"scripts": { "scripts": {
"start": "nodemon --inspect src/server.js", "start": "nodemon --inspect src/server.js",
"test": "echo \"Error: no test specified\" && exit 1", "test": "echo \"Error: no test specified\" && exit 1",
"deploy": "curl -X POST http://35.162.222.18:9000/hooks/deploy-va-middleware", "deploy": "curl -X POST http://35.95.209.161:9000/hooks/deploy-va-middleware",
"build": "npx webpack --mode production", "build": "npx webpack --mode production",
"dev": "npx webpack --mode development" "dev": "npx webpack --mode development"
}, },

View File

@ -4,21 +4,21 @@ import { updateEntityRelationships } from "../../helper/updateEntityRelationship
// Register a new VA // Register a new VA
async function project_add_sf(req, res) { async function project_add_sf(req, res) {
try { try {
const payload = req.body|| {}; const payload = req.body || {};
const { name, hourlyRate, clientId, vaAssigned, dedicatedHours, vaWorkingScheduleStart, vaWorkingScheduleEnd, isFlexibleAllowedPt, vaWorkingDelayAllowedStart, vaWorkingDelayAllowedEnd } = payload; const { name, hourlyRate, clientId, vaAssigned, dedicatedHours, vaWorkingScheduleStart, vaWorkingScheduleEnd, isFlexibleAllowed, vaWorkingDelayAllowedStart, vaWorkingDelayAllowedEnd } = payload;
// Validate required fields // Validate required fields
if (!name) { if (!name) {
return res.status(400).json({ return res.status(400).json({
success: false, success: false,
message: "Project name is required" message: "Project name is required"
}); });
} }
if (!clientId) { if (!clientId) {
return res.status(400).json({ return res.status(400).json({
success: false, success: false,
message: "Client ID is required" message: "Client ID is required"
}); });
} }
@ -27,53 +27,62 @@ async function project_add_sf(req, res) {
try { try {
clientObjectId = new Types.ObjectId(clientId); clientObjectId = new Types.ObjectId(clientId);
} catch (error) { } catch (error) {
return res.status(400).json({ return res.status(400).json({
success: false, success: false,
message: "Invalid Client ID format" message: "Invalid Client ID format"
}); });
} }
// Validate and convert vaAssigned to array of ObjectIds // Validate and convert vaAssigned to array of ObjectIds
let vaAssignedArray = []; // let vaAssignedArray = [];
if (vaAssigned) { // if (vaAssigned) {
if (Array.isArray(vaAssigned)) { // if (Array.isArray(vaAssigned)) {
try { // try {
vaAssignedArray = vaAssigned.map(vaId => new Types.ObjectId(vaId)); // vaAssignedArray = vaAssigned.map(vaId => {
} catch (error) { // if(typeof vaId === 'object' && "isFlexibleAllowed" in vaId){
return res.status(400).json({ // // Handle Error form SF Side for now - That vaId is coming as object
success: false, // return new Types.ObjectId(vaId.vaId)
message: "Invalid VA ID format in vaAssigned array" // } else {
}); // return new Types.ObjectId(vaId)
} // }
} else if (typeof vaAssigned === 'string') { // });
// Handle single VA ID as string // } catch (error) {
try { // return res.status(400).json({
vaAssignedArray = [new Types.ObjectId(vaAssigned)]; // success: false,
} catch (error) { // message: "Invalid VA ID format in vaAssigned array"
return res.status(400).json({ // });
success: false, // }
message: "Invalid VA ID format" // } else if (typeof vaAssigned === 'string') {
}); // // Handle single VA ID as string
} // try {
} // vaAssignedArray = [new Types.ObjectId(vaAssigned)];
} // } catch (error) {
// return res.status(400).json({
// success: false,
// message: "Invalid VA ID format"
// });
// }
// }
// }
// sanitize/construct allowed fields // sanitize/construct allowed fields
const toCreate = { const toCreate = {
name: name.trim(), name: name.trim(),
hourlyRate: hourlyRate || '0', hourlyRate: hourlyRate || '0',
clientId: clientObjectId, clientId: clientObjectId,
vaAssigned: vaAssignedArray, vaAssigned: vaAssigned,
dedicatedHours: dedicatedHours ? parseFloat(dedicatedHours) : 0, dedicatedHours: dedicatedHours ? parseFloat(dedicatedHours) : 0,
vaWorkingScheduleStart: vaWorkingScheduleStart || null, vaWorkingScheduleStart: vaWorkingScheduleStart || null,
vaWorkingScheduleEnd: vaWorkingScheduleEnd || null, vaWorkingScheduleEnd: vaWorkingScheduleEnd || null,
isFlexibleAllowedPt: isFlexibleAllowedPt || true, isFlexibleAllowed: isFlexibleAllowed || true,
vaWorkingDelayAllowedStart: vaWorkingDelayAllowedStart || null, vaWorkingDelayAllowedStart: vaWorkingDelayAllowedStart || null,
vaWorkingDelayAllowedEnd: vaWorkingDelayAllowedEnd || null vaWorkingDelayAllowedEnd: vaWorkingDelayAllowedEnd || null
}; };
const created = await ProjectModel.create(toCreate); const created = await ProjectModel.create(toCreate);
const vaAssignedArray = vaAssigned ? vaAssigned.map(vaDtl => vaDtl.vaId) : [];
const relationshipUpdate = await updateEntityRelationships({ const relationshipUpdate = await updateEntityRelationships({
project: { project: {
projectId: created._id, projectId: created._id,
@ -87,7 +96,7 @@ async function project_add_sf(req, res) {
} }
res.status(201).json({ success: true, data: created, relationships: relationshipUpdate }); res.status(201).json({ success: true, data: created, relationships: relationshipUpdate });
} catch (error) { } catch (error) {
console.error("project_add_sf error:", error); console.error("project_add_sf error:", error);
return res.status(500).json({ success: false, message: "Internal Server Error" }); return res.status(500).json({ success: false, message: "Internal Server Error" });

View File

@ -10,25 +10,25 @@ async function update_project_sf(req, res) {
try { try {
const { projectId } = req.params; const { projectId } = req.params;
const payload = req.body || {}; const payload = req.body || {};
const { const {
name, name,
hourlyRate, hourlyRate,
vaAssigned, vaAssigned,
dedicatedHours, dedicatedHours,
status, status,
totalLoggedHours , totalLoggedHours,
vaWorkingScheduleStart, vaWorkingScheduleStart,
vaWorkingScheduleEnd, vaWorkingScheduleEnd,
isFlexibleAllowedPt, isFlexibleAllowed,
vaWorkingDelayAllowedStart, vaWorkingDelayAllowedStart,
vaWorkingDelayAllowedEnd vaWorkingDelayAllowedEnd
} = payload; } = payload;
// Validate projectId // Validate projectId
if (!projectId) { if (!projectId) {
return res.status(400).json({ return res.status(400).json({
success: false, success: false,
message: "Project ID is required" message: "Project ID is required"
}); });
} }
@ -36,18 +36,18 @@ async function update_project_sf(req, res) {
try { try {
projectObjectId = new Types.ObjectId(projectId); projectObjectId = new Types.ObjectId(projectId);
} catch (error) { } catch (error) {
return res.status(400).json({ return res.status(400).json({
success: false, success: false,
message: "Invalid Project ID format" message: "Invalid Project ID format"
}); });
} }
// Check if project exists // Check if project exists
const existingProject = await ProjectModel.findById(projectObjectId); const existingProject = await ProjectModel.findById(projectObjectId);
if (!existingProject) { if (!existingProject) {
return res.status(404).json({ return res.status(404).json({
success: false, success: false,
message: "Project not found" message: "Project not found"
}); });
} }
@ -59,9 +59,9 @@ async function update_project_sf(req, res) {
// Update name if provided // Update name if provided
if (name !== undefined) { if (name !== undefined) {
if (!name.trim()) { if (!name.trim()) {
return res.status(400).json({ return res.status(400).json({
success: false, success: false,
message: "Project name cannot be empty" message: "Project name cannot be empty"
}); });
} }
updateData.name = name.trim(); updateData.name = name.trim();
@ -85,9 +85,9 @@ async function update_project_sf(req, res) {
// Update status if provided // Update status if provided
if (status !== undefined) { if (status !== undefined) {
if (!["ACTIVE", "INACTIVE"].includes(status)) { if (!["ACTIVE", "INACTIVE"].includes(status)) {
return res.status(400).json({ return res.status(400).json({
success: false, success: false,
message: "Status must be either ACTIVE or INACTIVE" message: "Status must be either ACTIVE or INACTIVE"
}); });
} }
updateData.status = status; updateData.status = status;
@ -102,8 +102,8 @@ async function update_project_sf(req, res) {
updateData.vaWorkingScheduleEnd = vaWorkingScheduleEnd || null; updateData.vaWorkingScheduleEnd = vaWorkingScheduleEnd || null;
} }
if (isFlexibleAllowedPt !== undefined) { if (isFlexibleAllowed !== undefined) {
updateData.isFlexibleAllowedPt = isFlexibleAllowedPt; updateData.isFlexibleAllowed = isFlexibleAllowed;
} }
if (vaWorkingDelayAllowedStart !== undefined) { if (vaWorkingDelayAllowedStart !== undefined) {
@ -114,34 +114,45 @@ async function update_project_sf(req, res) {
updateData.vaWorkingDelayAllowedEnd = vaWorkingDelayAllowedEnd || null; updateData.vaWorkingDelayAllowedEnd = vaWorkingDelayAllowedEnd || null;
} }
if (vaAssigned !== undefined && vaAssigned !== null && Array.isArray(vaAssigned)) {
updateData.vaAssigned = vaAssigned;
}
// Handle VA assignments - this requires relationship updates // Handle VA assignments - this requires relationship updates
if (vaAssigned !== undefined) { if (vaAssigned !== undefined) {
let newVaAssignedArray = []; let newVaAssignedArray = updateData.vaAssigned.map(vaDtl => vaDtl.vaId);
// Parse and validate VA IDs // Parse and validate VA IDs
if (vaAssigned && vaAssigned.length > 0) { if (vaAssigned && vaAssigned.length > 0) {
if (Array.isArray(vaAssigned)) { if (Array.isArray(vaAssigned)) {
try { try {
newVaAssignedArray = vaAssigned.map(vaId => new Types.ObjectId(vaId)); newVaAssignedArray = vaAssigned.map(vaId => {
if (typeof vaId === 'object' && "isFlexibleAllowed" in vaId) {
// Handle Error form SF Side for now - That vaId is coming as object
return new Types.ObjectId(vaId.vaId)
} else {
return new Types.ObjectId(vaId)
}
});
} catch (error) { } catch (error) {
return res.status(400).json({ return res.status(400).json({
success: false, success: false,
message: "Invalid VA ID format in vaAssigned array" message: "Invalid VA ID format in vaAssigned array"
}); });
} }
} else if (typeof vaAssigned === 'string') { } else if (typeof vaAssigned === 'string') {
try { try {
newVaAssignedArray = [new Types.ObjectId(vaAssigned)]; newVaAssignedArray = [new Types.ObjectId(vaAssigned)];
} catch (error) { } catch (error) {
return res.status(400).json({ return res.status(400).json({
success: false, success: false,
message: "Invalid VA ID format" message: "Invalid VA ID format"
}); });
} }
} }
} }
updateData.vaAssigned = newVaAssignedArray; // updateData.vaAssigned = newVaAssignedArray;
// Calculate VAs to add and remove for relationship updates // Calculate VAs to add and remove for relationship updates
const currentVaIds = existingProject.vaAssigned.map(va => va.toString()); const currentVaIds = existingProject.vaAssigned.map(va => va.toString());
@ -175,9 +186,9 @@ async function update_project_sf(req, res) {
// If no fields to update // If no fields to update
if (Object.keys(updateData).length === 0) { if (Object.keys(updateData).length === 0) {
return res.status(400).json({ return res.status(400).json({
success: false, success: false,
message: "No valid fields to update" message: "No valid fields to update"
}); });
} }
@ -187,7 +198,7 @@ async function update_project_sf(req, res) {
updateData, updateData,
{ new: true, runValidators: true } { new: true, runValidators: true }
).populate('clientId', 'userDetails.name userDetails.email') ).populate('clientId', 'userDetails.name userDetails.email')
.populate('vaAssigned', 'userDetails.firstName userDetails.lastName userDetails.email'); .populate('vaAssigned', 'userDetails.firstName userDetails.lastName userDetails.email');
// If VA assignments were changed but relationship update failed, log warning // If VA assignments were changed but relationship update failed, log warning
if (vaAssigned !== undefined) { if (vaAssigned !== undefined) {
@ -211,33 +222,33 @@ async function update_project_sf(req, res) {
} catch (error) { } catch (error) {
console.error("update_project_sf error:", error); console.error("update_project_sf error:", error);
// MongoDB duplicate key error // MongoDB duplicate key error
if (error.code === 11000) { if (error.code === 11000) {
return res.status(400).json({ return res.status(400).json({
success: false, success: false,
message: "Project with this name already exists" message: "Project with this name already exists"
}); });
} }
// MongoDB validation error // MongoDB validation error
if (error.name === 'ValidationError') { if (error.name === 'ValidationError') {
const errors = Object.values(error.errors).map(err => err.message); const errors = Object.values(error.errors).map(err => err.message);
return res.status(400).json({ return res.status(400).json({
success: false, success: false,
message: "Validation error", message: "Validation error",
errors: errors errors: errors
}); });
} }
return res.status(500).json({ return res.status(500).json({
success: false, success: false,
message: "Internal Server Error", message: "Internal Server Error",
error: error.message error: error.message
}); });
} }
} }
export { export {
update_project_sf update_project_sf
}; };

View File

@ -44,7 +44,7 @@ async function va_remove_function(req, res) {
if (projectIds.length > 0) { if (projectIds.length > 0) {
const projectResult = await ProjectModel.updateMany( const projectResult = await ProjectModel.updateMany(
{ _id: { $in: projectIds } }, { _id: { $in: projectIds } },
{ $pull: { vaAssigned: vaId } } { $pull: { "vaAssigned.vaId": vaId } }
); );
projectsUpdated = projectResult.modifiedCount; projectsUpdated = projectResult.modifiedCount;
console.log(`Removed VA from ${projectsUpdated} projects`); console.log(`Removed VA from ${projectsUpdated} projects`);

View File

@ -105,7 +105,7 @@ function getCurrentUTCTime() {
// console.log(`Schedule Check - Current: ${currentMinutes}, Start: ${scheduleStartMinutes}, End: ${scheduleEndMinutes}, Allowed Start: ${allowedStartMinutes}, Allowed End: ${allowedEndMinutes}`); // console.log(`Schedule Check - Current: ${currentMinutes}, Start: ${scheduleStartMinutes}, End: ${scheduleEndMinutes}, Allowed Start: ${allowedStartMinutes}, Allowed End: ${allowedEndMinutes}`);
// // Check for PARTTIME with flexible schedule // // Check for PARTTIME with flexible schedule
// if (vaStatus === "PARTTIME" && project.isFlexibleAllowedPt) { // if (vaStatus === "PARTTIME" && project.isFlexibleAllowed) {
// return { isCompliant: true, warnings }; // return { isCompliant: true, warnings };
// } // }

View File

@ -72,7 +72,7 @@ async function va_projects_log_list_function(req, res) {
console.log("VA ID:", vaId.toString()); console.log("VA ID:", vaId.toString());
projectList = await ProjectModel.find({ vaAssigned: vaUser._id }); projectList = await ProjectModel.find({ "vaAssigned.vaId": vaUser._id });
} }
if (!projectList.length) { if (!projectList.length) {

View File

@ -34,7 +34,7 @@ async function va_timesheet_adjustment_function(req, res) {
// Validate project exists and VA is assigned to it // Validate project exists and VA is assigned to it
const project = await ProjectModel.findOne({ const project = await ProjectModel.findOne({
_id: projectId, _id: projectId,
vaAssigned: vaId "vaAssigned.vaId": vaId
}); });
if (!project) { if (!project) {

View File

@ -43,7 +43,7 @@ async function va_timesheet_summary_function(req, res) {
// Validate project exists and VA is assigned to it // Validate project exists and VA is assigned to it
const project = await ProjectModel.findOne({ const project = await ProjectModel.findOne({
_id: projectId, _id: projectId,
vaAssigned: vaId "vaAssigned.vaId": vaId
}); });
if (!project) { if (!project) {

View File

@ -20,7 +20,7 @@ async function va_tips_manage_function(req, res) {
clientId, clientId,
status: "Unbilled", status: "Unbilled",
amount: parseFloat(amount).toFixed(2), amount: parseFloat(amount).toFixed(2),
description, description: description || "",
vaName: vaUser.userDetails.firstName ? vaUser.userDetails.firstName : "" + " " + vaUser.userDetails.lastName ? vaUser.userDetails.lastName : "", vaName: vaUser.userDetails.firstName ? vaUser.userDetails.firstName : "" + " " + vaUser.userDetails.lastName ? vaUser.userDetails.lastName : "",
vaId, vaId,
}; };
@ -34,7 +34,7 @@ async function va_tips_manage_function(req, res) {
"vaId": createdTip.vaId.toString(), "vaId": createdTip.vaId.toString(),
"status": "Unbilled", "status": "Unbilled",
"clientId": createdTip.clientId.toString(), "clientId": createdTip.clientId.toString(),
"comment": createdTip.description, "comment": createdTip.description || "",
"tipAmount": Number(parseFloat(createdTip.amount).toFixed(2)), // Convert to cents "tipAmount": Number(parseFloat(createdTip.amount).toFixed(2)), // Convert to cents
} }

View File

@ -69,7 +69,7 @@ async function updateEntityRelationships(data) {
if (vaId && Array.isArray(vaId)) { if (vaId && Array.isArray(vaId)) {
await ProjectModel.findByIdAndUpdate( await ProjectModel.findByIdAndUpdate(
projectId, projectId,
{ $addToSet: { vaAssigned: { $each: vaId } } } { $addToSet: { "vaAssigned.vaId": { $each: vaId } } }
); );
} }
@ -103,7 +103,7 @@ async function updateEntityRelationships(data) {
{ _id: { $in: projectId } }, { _id: { $in: projectId } },
{ {
$addToSet: { $addToSet: {
vaAssigned: vaId "vaAssigned.vaId": vaId
} }
} }
); );
@ -285,7 +285,7 @@ async function removeEntityRelationships(data) {
if (projectId && Array.isArray(projectId)) { if (projectId && Array.isArray(projectId)) {
await ProjectModel.updateMany( await ProjectModel.updateMany(
{ _id: { $in: projectId } }, { _id: { $in: projectId } },
{ $pull: { vaAssigned: vaId } } { $pull: { "vaAssigned.vaId": vaId } }
); );
} }

View File

@ -89,9 +89,16 @@ const projectSchema = new Schema({
}, },
vaAssigned: [ vaAssigned: [
{ {
type: Types.ObjectId, _id: false,
ref: "VA", vaId: {
}, type: Types.ObjectId,
ref: "VA",
},
isFlexibleAllowed: {
type: Boolean,
default: true
}
}
], ],
dedicatedHours: { dedicatedHours: {
type: String, type: String,
@ -110,7 +117,7 @@ const projectSchema = new Schema({
type: String, type: String,
default: null default: null
}, },
isFlexibleAllowedPt: { isFlexibleAllowed: {
type: Boolean, type: Boolean,
default: true default: true
}, },
@ -189,7 +196,7 @@ const clientDetailsSchema = new Schema({
enum: ["CHECKING", "SAVINGS"], enum: ["CHECKING", "SAVINGS"],
type: String, type: String,
}, },
accountHolderName: { accountHolderName: {
type: String, type: String,
}, },
@ -363,36 +370,36 @@ const vaWorkTrackSchema = new Schema({
) )
const passwordResetSchema = new mongoose.Schema({ const passwordResetSchema = new mongoose.Schema({
email: { email: {
type: String, type: String,
required: true, required: true,
lowercase: true, lowercase: true,
index: true index: true
}, },
token: { token: {
type: String, type: String,
required: true, required: true,
unique: true, unique: true,
index: true index: true
}, },
userType: { userType: {
type: String, type: String,
required: true, required: true,
enum: ['va', 'client'], enum: ['va', 'client'],
index: true index: true
}, },
expiresAt: { expiresAt: {
type: Date, type: Date,
required: true, required: true,
index: true, index: true,
expires: 3600 // TTL index for 1 hour expires: 3600 // TTL index for 1 hour
}, },
used: { used: {
type: Boolean, type: Boolean,
default: false default: false
} }
}, { }, {
timestamps: true timestamps: true
}); });
passwordResetSchema.index({ email: 1, used: 1 }); passwordResetSchema.index({ email: 1, used: 1 });

View File

@ -341,7 +341,7 @@ io.on("connection", (socket) => {
const projectVAIds = new Set(); const projectVAIds = new Set();
const populated = await ProjectModel.findById(projectId).populate("vaAssigned"); const populated = await ProjectModel.findById(projectId).populate("vaAssigned");
populated.vaAssigned.forEach((va) => projectVAIds.add(va._id.toString())); populated.vaAssigned.forEach((va) => projectVAIds.add(va.vaId.toString()));
socket.data.allowedVAs = Array.from(projectVAIds); socket.data.allowedVAs = Array.from(projectVAIds);
console.log(`Client ${clientId} can view VAs:`, socket.data.allowedVAs); console.log(`Client ${clientId} can view VAs:`, socket.data.allowedVAs);
@ -498,6 +498,14 @@ io.on("connection", (socket) => {
}); });
app.get("/heath", () => {
if(Math.round(Math.random()) === 1) {
return res.status(200).json({"success": true, "message": "Server is Healthy"})
} else {
return res.status(403).json({"success": false, "message": "Server is not Healthy"})
}
})
app.use((req, res) => { app.use((req, res) => {
res.status(404).render("not_found", { res.status(404).render("not_found", {
title: "404 - Page Not Found" title: "404 - Page Not Found"

View File

@ -184,7 +184,7 @@
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="form-label">Tip Description</label> <label class="form-label">Tip Description</label>
<textarea name="vaTipDescription" class="form-input" id="vaTipDescription"></textarea> <textarea required="true" name="vaTipDescription" class="form-input" id="vaTipDescription"></textarea>
<div class="error-message" id="vaTipDescriptionError"></div> <div class="error-message" id="vaTipDescriptionError"></div>
</div> </div>
<div class="modal-actions"> <div class="modal-actions">