diff --git a/src/controllers/va/vaProfile/vaRegister.controller.js b/src/controllers/va/vaProfile/vaRegister.controller.js index 5746119..47b0442 100644 --- a/src/controllers/va/vaProfile/vaRegister.controller.js +++ b/src/controllers/va/vaProfile/vaRegister.controller.js @@ -74,7 +74,7 @@ async function va_register(req, res) { res.status(201).json({ success: true, data: created, SyncSalesforce: result }); } catch (error) { console.error("❌ Failed to sync with Salesforce:", error.message); - + await VAModel.findOneAndDelete({_id: created.userDetails._id}); // Create a custom error to distinguish Salesforce errors const salesforceError = new Error(`Salesforce sync failed: ${error.message}`); salesforceError.isSalesforceError = true; diff --git a/src/controllers/va/vaTimesheetAdjustment/vaAllTimesheetAdjustmentReuqest.controller.js b/src/controllers/va/vaTimesheetAdjustment/vaAllTimesheetAdjustmentReuqest.controller.js index 4ba9488..5c710de 100644 --- a/src/controllers/va/vaTimesheetAdjustment/vaAllTimesheetAdjustmentReuqest.controller.js +++ b/src/controllers/va/vaTimesheetAdjustment/vaAllTimesheetAdjustmentReuqest.controller.js @@ -1,15 +1,24 @@ +import mongoose from "mongoose"; import { AdjustedHoursModel } from "../../../models/vaModel.js"; // Get all adjustment requests (with filtering options) async function get_adjustment_requests_list(req, res) { try { - const { status, projectId, vaId } = req.params; - + const { status, projectId, vaId } = req.query; + // Build filter object const filter = {}; - if (status) filter.status = status; - if (projectId) filter.projectId = projectId; - if (vaId) filter.vaId = vaId; + // if (status) filter.status = status; + // if (projectId) filter.projectId = projectId; + // if (vaId) filter.vaId = vaId; + + if (status && status !== "undefined") filter.status = status; + if (projectId && mongoose.Types.ObjectId.isValid(projectId)) { + filter.projectId = new mongoose.Types.ObjectId(projectId); + } + if (vaId && mongoose.Types.ObjectId.isValid(vaId)) { + filter.vaId = new mongoose.Types.ObjectId(vaId); + } const adjustments = await AdjustedHoursModel.find(filter) .sort({ createdAt: -1 }) @@ -54,9 +63,9 @@ async function get_adjustment_requests_list(req, res) { } catch (error) { console.error("Get adjustment requests error:", error); - return res.status(500).json({ - success: false, - message: "Internal Server Error" + return res.status(500).json({ + success: false, + message: "Internal Server Error" }); } } diff --git a/src/controllers/va/vaTips/vaTipsManage.controller.js b/src/controllers/va/vaTips/vaTipsManage.controller.js new file mode 100644 index 0000000..010ae35 --- /dev/null +++ b/src/controllers/va/vaTips/vaTipsManage.controller.js @@ -0,0 +1,143 @@ +import { VAModel, TipsModel, ClientModel } from "../../../models/vaModel.js"; + +// Approve or reject leave +async function va_tips_manage_function(req, res) { + + const { clientId, amount, description, vaId } = req.body; + + try { + const vaUser = await VAModel.findById(vaId); + if (!vaUser) { + return res.status(404).json({ success: false, message: "Virtual Assistant not found" }); + } + const clUser = await ClientModel.findById(clientId); + if (!clUser) { + return res.status(404).json({ success: false, message: "Client not found" }); + } + + // Create new tip entry + const newTip = { + clientId, + status: "Unbilled", + amount: parseFloat(amount).toFixed(2), + description, + vaName: vaUser.userDetails.firstName ? vaUser.userDetails.firstName : "" + " " + vaUser.userDetails.lastName ? vaUser.userDetails.lastName : "", + vaId, + }; + + const createdTip = await TipsModel.create(newTip); + + const salesforceUrl = `${process.env.SF_DOCTRACKER_API_URL}services/apexrest/docva/api/va-tips`; + + const sfData = { + "id": createdTip._id.toString(), + "vaId": createdTip.vaId.toString(), + "status": "Unbilled", + "clientId": createdTip.clientId.toString(), + "comment": createdTip.description, + "tipAmount": Number(parseFloat(createdTip.amount).toFixed(2)), // Convert to cents + } + + const response = await fetch(salesforceUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + // Add any required headers for Salesforce authentication + // 'Authorization': 'Bearer ' + process.env.SF_ACCESS_TOKEN + }, + body: JSON.stringify({ ...sfData }) + }); + + if (response.ok) { + console.log("✅ Successfully synced Tip data to Salesforce"); + return res.status(201).json({ success: true, data: createdTip }); + } else { + TipsModel.findByIdAndDelete(createdTip._id); + const errorText = await response.text(); + throw new Error(`Salesforce API responded with status ${response.status}: ${errorText}`); + } + + } catch (error) { + console.log(error); + res.status(500).json({ success: false, message: "Internal server error" }); + } +} + +async function va_tips_get(req, res) { + + const { clientId } = req.body; + + try { + const clUser = await ClientModel.findById(clientId); + if (!clUser) { + return res.status(404).json({ success: false, message: "Client not found" }); + } + const data = await TipsModel.find({ clientId: clientId, status: "Unbilled" }); + return res.status(200).json({ success: true, data: data }); + } catch (error) { + console.log(error); + res.status(500).json({ success: false, message: "Internal server error" }); + } +} + +async function va_tips_update(req, res) { + const { tipId } = req.params; + const { update } = req.body; + + try { + const tipData = await TipsModel.findById(tipId); + if (!tipData) { + return res.status(404).json({ success: false, message: "Tip not found" }); + } + + await TipsModel.updateOne({ _id: tipId }, update); + return res.status(200).json({ success: true, message: "Tip updated successfully" }); + + } catch (error) { + console.log(error); + res.status(500).json({ success: false, message: "Internal server error" }); + } +} + +async function va_tips_delete(req, res) { + const { tipId } = req.params; + + try { + + + const salesforceUrl = `${process.env.SF_DOCTRACKER_API_URL}services/apexrest/docva/api/va-tips`; + + // const sfData = { + // "id": createdTip._id.toString(), + // "vaId": createdTip.vaId.toString(), + // "status": "Unbilled", + // "clientId": createdTip.clientId.toString(), + // "comment": createdTip.description, + // "tipAmount": Number(parseFloat(createdTip.amount).toFixed(2)), // Convert to cents + // } + + const response = await fetch(salesforceUrl, { + method: 'DELETE', + headers: { + 'Content-Type': 'application/json', + // Add any required headers for Salesforce authentication + // 'Authorization': 'Bearer ' + process.env.SF_ACCESS_TOKEN + }, + body: JSON.stringify({ "id": tipId}) + }); + + if (response.ok) { + await TipsModel.findOneAndDelete({ _id: tipId }); + return res.status(200).json({ success: true, message: "Tip updated successfully" }); + } else { + const errorText = await response.text(); + throw new Error(`Salesforce API responded with status ${response.status}: ${errorText}`); + } + } catch (error) { + console.log(error); + res.status(500).json({ success: false, message: "Internal server error" }); + } +} + + +export { va_tips_manage_function, va_tips_get, va_tips_update, va_tips_delete }; \ No newline at end of file diff --git a/src/middlewares/auth.middleware.js b/src/middlewares/auth.middleware.js index 49cf229..a287ed1 100644 --- a/src/middlewares/auth.middleware.js +++ b/src/middlewares/auth.middleware.js @@ -50,6 +50,15 @@ async function authValidator(req, res, next) { async function clientAuthValidator(req, res, next) { try { + + const sf_token = req.headers.sf_token; + if (sf_token) { + + next(); + } + + if(sf_token) return; + const accessToken = req.headers.token; if (!accessToken) { diff --git a/src/models/vaModel.js b/src/models/vaModel.js index e6794e1..419d3d7 100644 --- a/src/models/vaModel.js +++ b/src/models/vaModel.js @@ -511,6 +511,37 @@ const leavesSchema = new Schema({ } }, { timestamps: true }); + +/* ---------------------- Tips Schema ---------------------- */ +const tipsSchema = new Schema({ + clientId: { + type: Types.ObjectId, + ref: "Client" + }, + vaId: { + type: Types.ObjectId, + ref: "VA" + }, + vaName: { + type: String, + required: true + }, + status: { + type: String, + enum: ["Unbilled", "Billed"], + default: "Unbilled", + required: true + }, + amount: { + type: String, + required: true + }, + description: { + type: String, + } +}, { timestamps: true }) + + /* ---------------------- Model Exports ---------------------- */ const VAModel = model("VA", vaSchema); const ClientModel = model("Client", clientSchema); @@ -520,5 +551,6 @@ const WorkTrackModel = model("TrackWork", vaWorkTrackSchema); const PasswordResetModel = model("PasswordReset", passwordResetSchema); const AdjustedHoursModel = model("AdjustedHours", adjustedHoursSchema); const LeavesModel = model("Leaves", leavesSchema); +const TipsModel = model("Tips", tipsSchema); -export { VAModel, ClientModel, ProjectModel, LoggedModel, WorkTrackModel, PasswordResetModel, AdjustedHoursModel, LeavesModel }; \ No newline at end of file +export { VAModel, ClientModel, ProjectModel, LoggedModel, WorkTrackModel, PasswordResetModel, AdjustedHoursModel, LeavesModel, TipsModel }; \ No newline at end of file diff --git a/src/routes/v1/client.routes.js b/src/routes/v1/client.routes.js index 9e2c6e6..8c9c260 100644 --- a/src/routes/v1/client.routes.js +++ b/src/routes/v1/client.routes.js @@ -13,6 +13,7 @@ import { client_dashboard_va_log } from "../../controllers/client/dashboard/clie import { client_verify_email } from "../../controllers/client/vaClientEmailVerify.controller.js"; import { client_dashboard_projects } from "../../controllers/client/dashboard/clientDashboardProjects.controller.js"; import { client_logout_callback } from "../../controllers/client/dashboard/clientLogout.controller.js"; +import { va_tips_delete, va_tips_get, va_tips_manage_function, va_tips_update } from "../../controllers/va/vaTips/vaTipsManage.controller.js"; // clients routes clientRoutes.post("/register", va_register_client); // Register a new VA @@ -22,6 +23,11 @@ clientRoutes.post("/forgot-password", client_request_password_reset); clientRoutes.post("/verify-client-email", client_verify_email); clientRoutes.post("/logout", clientAuthValidator, client_logout_callback); +clientRoutes.post("/tips", clientAuthValidator, va_tips_manage_function); +clientRoutes.get("/tips", clientAuthValidator, va_tips_get); +clientRoutes.patch("/tips/:tipId", clientAuthValidator, va_tips_update); +clientRoutes.delete("/tips/:tipId", clientAuthValidator, va_tips_delete); + // clientRoutes.get("/:clientID?", va_clients_callback); // VA Clients Callback for getting all VA's or specific client by ID (NC) clientRoutes.get("/:clientId/projects/:projectId", va_clients_projects_callback); // VA Clients Projects Callback (NC) diff --git a/src/routes/v1/va.routes.js b/src/routes/v1/va.routes.js index be6a049..8da9bf8 100644 --- a/src/routes/v1/va.routes.js +++ b/src/routes/v1/va.routes.js @@ -48,6 +48,8 @@ vaRoutes.delete("/profile/:id", va_remove_function); // VA delete Callback vaRoutes.get("/timesheet/summary/:projectId/:date/:vaId?", authValidator, va_timesheet_summary_function); // Get Timesheet Adjustment for VA on specific project and date vaRoutes.post("/timesheet/adjustment/request", authValidator, va_timesheet_adjustment_function); // Timesheet Adjustment for VA vaRoutes.get("/timesheet/adjustments/list/:projectId?/:vaId?/:status?", get_adjustment_requests_list); // show all adjustment list to approve and reject + + vaRoutes.patch("/timesheet/adjustments/:adjustmentId", va_timesheet_manage_adjustment_request); // Leaves routes @@ -56,7 +58,7 @@ vaRoutes.get("/leaves/status/:status", authValidator, va_leave_list_function); / vaRoutes.get("/leaves/type/:leaveType", authValidator, va_leave_list_function); // Filter by leave type vaRoutes.get("/leaves/status/:status/type/:leaveType", authValidator, va_leave_list_function); // leaves Filter vaRoutes.post("/leave/request", authValidator, va_leave_apply_function); // Leave for VA -vaRoutes.patch("/leave/:leaveId", va_leave_manage_function); +vaRoutes.patch("/leave/:leaveId", authValidator, va_leave_manage_function); // timesheet adjustment routes diff --git a/src/utils/convertToObjectId.js b/src/utils/convertToObjectId.js new file mode 100644 index 0000000..204ae0b --- /dev/null +++ b/src/utils/convertToObjectId.js @@ -0,0 +1,5 @@ +const convertToObjectId = (id) => { + return mongoose.Types.ObjectId.isValid(id) ? new mongoose.Types.ObjectId(id) : id; +} + +export default convertToObjectId; \ No newline at end of file diff --git a/views/client/dashboard-manage-va-tips.ejs b/views/client/dashboard-manage-va-tips.ejs index bc9580f..dbba167 100644 --- a/views/client/dashboard-manage-va-tips.ejs +++ b/views/client/dashboard-manage-va-tips.ejs @@ -1,5 +1,6 @@ + @@ -7,6 +8,21 @@ + + +
- +
- + @@ -155,9 +171,8 @@
- + - + + + + + + \ No newline at end of file