From 9c736dea02774eade165671fa1cd86731464644c Mon Sep 17 00:00:00 2001 From: Aakash Date: Mon, 24 Nov 2025 19:49:28 +0530 Subject: [PATCH 1/7] 24-11-2025 --- views/client/dashboard-dashboard.ejs | 2 +- views/client/dashboard-virtual-assistants.ejs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/views/client/dashboard-dashboard.ejs b/views/client/dashboard-dashboard.ejs index 0e55e21..10eb82e 100644 --- a/views/client/dashboard-dashboard.ejs +++ b/views/client/dashboard-dashboard.ejs @@ -242,7 +242,7 @@ // Format time function function formatTime(seconds) { - if (!seconds) return 'Not working'; + if (!seconds) return 'No Record Found'; const hours = Math.floor(seconds / 3600); const minutes = Math.floor((seconds % 3600) / 60); diff --git a/views/client/dashboard-virtual-assistants.ejs b/views/client/dashboard-virtual-assistants.ejs index 972052a..773cbbf 100644 --- a/views/client/dashboard-virtual-assistants.ejs +++ b/views/client/dashboard-virtual-assistants.ejs @@ -242,7 +242,7 @@ // Format time function function formatTime(seconds) { - if (!seconds) return 'Not working'; + if (!seconds) return 'No Record Found'; const hours = Math.floor(seconds / 3600); const minutes = Math.floor((seconds % 3600) / 60); From 39010ee61c48d5b09638069141723eb0ca2ce2ef Mon Sep 17 00:00:00 2001 From: Aakash Date: Tue, 25 Nov 2025 19:39:48 +0530 Subject: [PATCH 2/7] Added delete VA and project route, fixed tracker api issues --- public/js/va-client-register.js | 12 +- .../project/deleteProjectSf.controller.js | 97 ++++++++++++++ .../va/vaLog/vaLogTrackWork.controller.js | 3 +- .../va/vaProfile/vaDelete.controller.js | 92 ++++++++++++++ .../va/vaProjects/vaProjectLog.controller.js | 120 +++++++++++++++--- src/models/vaModel.js | 3 + src/routes/v1/index.js | 2 + src/routes/v1/va.routes.js | 2 + 8 files changed, 306 insertions(+), 25 deletions(-) create mode 100644 src/controllers/project/deleteProjectSf.controller.js create mode 100644 src/controllers/va/vaProfile/vaDelete.controller.js diff --git a/public/js/va-client-register.js b/public/js/va-client-register.js index a9d6788..7c330b2 100644 --- a/public/js/va-client-register.js +++ b/public/js/va-client-register.js @@ -693,12 +693,12 @@ async function submitForm() { let cardNumber = document.getElementById('card-number'); let expiryDate = document.getElementById('expiry-date'); let securityCode = document.getElementById('security-code'); - let cardZip = document.getElementById('card-zip').value; - let bankName = document.getElementById('bank-name').value; - let accountNumber = document.getElementById('account-number').value; - let routingNumber = document.getElementById('routing-number').value; - let accountType = document.getElementById('account-type').value; - let accountHolderName = document.getElementById('account-holder-name').value; + let cardZip = document.getElementById('card-zip'); + let bankName = document.getElementById('bank-name'); + let accountNumber = document.getElementById('account-number'); + let routingNumber = document.getElementById('routing-number'); + let accountType = document.getElementById('account-type'); + let accountHolderName = document.getElementById('account-holder-name'); if(cardNumber.value) { formData.userDetails.cardNumber = cardNumber.value; diff --git a/src/controllers/project/deleteProjectSf.controller.js b/src/controllers/project/deleteProjectSf.controller.js new file mode 100644 index 0000000..c3c6e39 --- /dev/null +++ b/src/controllers/project/deleteProjectSf.controller.js @@ -0,0 +1,97 @@ +import { ProjectModel, VAModel, ClientModel } from "../../models/vaModel.js"; +import { Types } from "mongoose"; + +/** + * Delete project and all related data (without transaction) + */ +async function delete_project_sf(req, res) { + try { + const { projectId } = req.params; + + // Validate projectId + if (!projectId) { + return res.status(400).json({ + success: false, + message: "Project ID is required" + }); + } + + let projectObjectId; + try { + projectObjectId = new Types.ObjectId(projectId); + } catch (error) { + return res.status(400).json({ + success: false, + message: "Invalid Project ID format" + }); + } + + // Check if project exists + const existingProject = await ProjectModel.findById(projectObjectId); + if (!existingProject) { + return res.status(404).json({ + success: false, + message: "Project not found" + }); + } + + const clientId = existingProject.clientId; + const vaAssigned = existingProject.vaAssigned || []; + const projectName = existingProject.name; + + console.log(`Deleting project: ${projectName} (${projectId})`); + + const deletionSummary = {}; + + // 1. Remove project from Client's projectIds array + if (clientId) { + await ClientModel.findByIdAndUpdate( + clientId, + { $pull: { projectIds: projectObjectId } } + ); + deletionSummary.clientUpdated = true; + console.log(`Removed project from client`); + } + + // 2. Remove project from all assigned VAs' projectIds arrays + if (vaAssigned.length > 0) { + const vaResult = await VAModel.updateMany( + { _id: { $in: vaAssigned } }, + { $pull: { projectIds: projectObjectId } } + ); + deletionSummary.vasUpdated = vaResult.modifiedCount; + console.log(`Removed project from ${deletionSummary.vasUpdated} VAs`); + } else { + deletionSummary.vasUpdated = 0; + } + + // 6. Finally delete the project + await ProjectModel.findByIdAndDelete(projectObjectId); + + console.log(`✅ Successfully deleted project: ${projectName} (${projectId})`); + + res.status(200).json({ + success: true, + message: 'Project and all related data deleted successfully', + data: { + projectId: projectId, + projectName: projectName, + clientId: clientId, + deletedAt: new Date().toISOString() + } + }); + + } catch (error) { + console.error("delete_project_sf error:", error); + + return res.status(500).json({ + success: false, + message: "Failed to delete project", + error: process.env.NODE_ENV === 'production' + ? 'Internal server error' + : error.message + }); + } +} + +export { delete_project_sf }; \ No newline at end of file diff --git a/src/controllers/va/vaLog/vaLogTrackWork.controller.js b/src/controllers/va/vaLog/vaLogTrackWork.controller.js index ddf5d22..8333dfd 100644 --- a/src/controllers/va/vaLog/vaLogTrackWork.controller.js +++ b/src/controllers/va/vaLog/vaLogTrackWork.controller.js @@ -147,7 +147,7 @@ async function va_log_track_work_callback(req, res) { const vaId = vaUser._id; const payload = req.body || {}; - const { projectId, logId, title, appName } = payload; + const { projectId, logId, title, appName, url = null } = payload; const project = await ProjectModel.findById(projectId); if (!project) return res.status(400).json({ success: false, message: "Project not found" }); @@ -190,6 +190,7 @@ async function va_log_track_work_callback(req, res) { logId, title, appName, + url, from: Date.now(), }); diff --git a/src/controllers/va/vaProfile/vaDelete.controller.js b/src/controllers/va/vaProfile/vaDelete.controller.js new file mode 100644 index 0000000..8b71c12 --- /dev/null +++ b/src/controllers/va/vaProfile/vaDelete.controller.js @@ -0,0 +1,92 @@ +import { VAModel, ClientModel, ProjectModel, LoggedModel, WorkTrackModel } from "../../../models/vaModel.js"; + +// Delete VA and all related data +async function va_remove_function(req, res) { + try { + const vaId = req.params.id; + + // Validate VA ID + if (!vaId) { + return res.status(400).json({ + success: false, + message: "VA ID is required" + }); + } + + // Find the VA to get clientIds and projectIds before deletion + const vaUser = await VAModel.findById(vaId); + if (!vaUser) { + return res.status(404).json({ + success: false, + message: "VA not found" + }); + } + + const clientIds = vaUser.clientIds || []; + const projectIds = vaUser.projectIds || []; + const vaEmail = vaUser.userDetails?.email; + + console.log(`Deleting VA ${vaId} - Affected: ${clientIds.length} clients, ${projectIds.length} projects`); + + // 1. Remove VA from all Clients + let clientsUpdated = 0; + if (clientIds.length > 0) { + const clientResult = await ClientModel.updateMany( + { _id: { $in: clientIds } }, + { $pull: { vaIds: vaId } } + ); + clientsUpdated = clientResult.modifiedCount; + console.log(`Removed VA from ${clientsUpdated} clients`); + } + + // 2. Remove VA from all Projects + let projectsUpdated = 0; + if (projectIds.length > 0) { + const projectResult = await ProjectModel.updateMany( + { _id: { $in: projectIds } }, + { $pull: { vaAssigned: vaId } } + ); + projectsUpdated = projectResult.modifiedCount; + console.log(`Removed VA from ${projectsUpdated} projects`); + } + + // 3. Delete all WorkTrack records for this VA + const workTrackResult = await WorkTrackModel.deleteMany({ vaId: vaId }); + console.log(`Deleted ${workTrackResult.deletedCount} work track records`); + + // 4. Delete all Logged records for this VA + const loggedResult = await LoggedModel.deleteMany({ vaId: vaId }); + console.log(`Deleted ${loggedResult.deletedCount} log records`); + + // 5. Finally delete the VA + await VAModel.findByIdAndDelete(vaId); + + console.log(`Successfully deleted VA: ${vaId} and all related data`); + + res.status(200).json({ + success: true, + message: 'Virtual Assistant and all related data deleted successfully', + data: { + vaId: vaId, + vaName: `${vaUser.userDetails?.firstName || ''} ${vaUser.userDetails?.lastName || ''}`.trim() || 'Unknown VA', + vaEmail: vaEmail, + clientsUpdated: clientsUpdated, + projectsUpdated: projectsUpdated, + workTracksDeleted: workTrackResult.deletedCount, + logsDeleted: loggedResult.deletedCount, + deletedAt: new Date().toISOString() + } + }); + + } catch (error) { + console.error('Error deleting VA:', error); + + res.status(500).json({ + success: false, + message: 'Failed to delete Virtual Assistant', + error: 'Internal server error' + }); + } +} + +export { va_remove_function }; \ No newline at end of file diff --git a/src/controllers/va/vaProjects/vaProjectLog.controller.js b/src/controllers/va/vaProjects/vaProjectLog.controller.js index 1f62636..07ca354 100644 --- a/src/controllers/va/vaProjects/vaProjectLog.controller.js +++ b/src/controllers/va/vaProjects/vaProjectLog.controller.js @@ -1,12 +1,12 @@ import { LoggedModel, WorkTrackModel, ProjectModel } from "../../../models/vaModel.js"; -// Helper function to get start and end of day +// Helper function to get start and end of day in UTC function getDayRange(date = new Date()) { const start = new Date(date); - start.setHours(0, 0, 0, 0); + start.setUTCHours(0, 0, 0, 0); const end = new Date(date); - end.setHours(23, 59, 59, 999); + end.setUTCHours(23, 59, 59, 999); return { start, end }; } @@ -25,10 +25,53 @@ function formatHoursToReadable(hours) { } } -// function to close open work tracks and create logout -async function closePreviousSession(vaId, lastLog) { +// Helper function to parse ISO time string +function parseISOTimeString(timeString) { + if (!timeString) return new Date(); + try { - const currentTime = new Date(); + // Parse ISO string directly + const parsedDate = new Date(timeString); + + // Check if the date is valid + if (isNaN(parsedDate.getTime())) { + throw new Error('Invalid date'); + } + parsedDate.setMilliseconds(0); + + console.log("pasdes date" + parsedDate) + return parsedDate; + } catch (error) { + console.error('Error parsing ISO time string:', error); + return new Date(); + } +} + +function setMillisecondsToZero(date) { + const newDate = new Date(date); + newDate.setMilliseconds(0); + return newDate; +} + +// Helper function to check if time difference is less than threshold +function isTimeWithinThreshold(receivedTime, currentTime, thresholdSeconds = 10) { + const diffMs = Math.abs(currentTime - receivedTime); + const diffSeconds = diffMs / 1000; + console.log(`Time comparison - Received: ${receivedTime.toISOString()}, Current: ${currentTime.toISOString()}, Diff: ${diffSeconds}s`); + return diffSeconds <= thresholdSeconds; +} + +// Helper function to get current UTC time +function getCurrentUTCTime() { + const now = new Date(); + now.setMilliseconds(0); + return now; +} + +// function to close open work tracks and create logout +async function closePreviousSession(vaId, lastLog, logTime = new Date()) { + try { + const currentTime = logTime; // Find all open work tracks for this VA and project (where to is not set or totalTime is 0) const openWorkTracks = await WorkTrackModel.find({ @@ -120,18 +163,44 @@ async function closePreviousSession(vaId, lastLog) { async function va_project_log_callback(req, res) { try { const payload = req.body || {}; - const { vaId, vaEmail, projectId, type } = payload; + const { vaId, vaEmail, projectId, type, time: receivedTimeString } = payload; + console.log("Received payload:", payload); - if (!vaId, !vaEmail, !projectId, !type) return res.status(400).send("Missing Props"); + if (!vaId || !vaEmail || !projectId || !type) { + return res.status(400).send("Missing required properties"); + } + + // Determine the log time based on received time (both in UTC) + const currentTime = getCurrentUTCTime(); // Current UTC time + let logTime = currentTime; + + if (receivedTimeString) { + // Parse the received ISO time string + const receivedTime = parseISOTimeString(receivedTimeString); + + // Check if the difference is less than 10 seconds + if (isTimeWithinThreshold(receivedTime, currentTime, 10)) { + logTime = receivedTime; + console.log(`✅ Using received ISO time: ${receivedTimeString}, difference is within 10 seconds`); + console.log(`✅ Parsed as: ${receivedTime.toISOString()}`); + } else { + logTime = currentTime; + console.log(`⚠️ Using current UTC time, received time difference exceeds 10 seconds.`); + console.log(`Received: ${receivedTimeString} (parsed: ${receivedTime.toISOString()})`); + console.log(`Current: ${currentTime.toISOString()}`); + } + } else { + console.log(`ℹ️ No time received, using current UTC time: ${currentTime.toISOString()}`); + } const project = await ProjectModel.findById(projectId); if (!project) { return res.status(400).json({ success: false, message: "Project not found" }); } - // For LOGIN requests, check daily hours + // For LOGIN requests, check daily hours (using UTC day range) if (type === "LOGIN") { - const { start, end } = getDayRange(); + const { start, end } = getDayRange(); // UTC day range // Get today's total hours const todayWorkEntries = await WorkTrackModel.find({ vaId, @@ -163,7 +232,7 @@ async function va_project_log_callback(req, res) { // check last workModel login time and make a logout request then create login // console.log(`User already logged in. Closing previous session...`); - const closeResult = await closePreviousSession(vaId, lastLog); + const closeResult = await closePreviousSession(vaId, lastLog, logTime); // console.log(`Previous session closed: ${closeResult.closedWorkTracks} work tracks, logout log: ${closeResult.logoutLogId}`); @@ -172,7 +241,8 @@ async function va_project_log_callback(req, res) { loggedType: "LOGIN", vaId, vaEmail, - projectId + projectId, + time: logTime }); return res.status(201).send({ @@ -182,13 +252,21 @@ async function va_project_log_callback(req, res) { previousSession: { closedWorkTracks: closeResult.closedWorkTracks, logoutLogId: closeResult.logoutLogId - } + }, + timeUsed: receivedTimeString ? 'received' : 'current', + actualTime: createdLog.time }); } } - const createdLog = await LoggedModel.create({ loggedType: type, vaId, vaEmail, projectId }) -// console.log(createdLog) + const createdLog = await LoggedModel.create({ + loggedType: type, + vaId, + vaEmail, + projectId, + time: logTime + }); + try { const salesforceUrl = `${process.env.SF_DOCTRACKER_API_URL}services/apexrest/docva/api/va-project-log`; const sfData = { @@ -218,7 +296,14 @@ async function va_project_log_callback(req, res) { const result = await response.json(); // console.log("✅ Successfully synced VA data to Salesforce:", result); - res.status(201).json({ success: true, message: `${type} Successfully`, logId: createdLog._id, SyncSalesforce: result }); + res.status(201).json({ + success: true, + message: `${type} Successfully`, + logId: createdLog._id, + SyncSalesforce: result, + timeUsed: receivedTimeString ? 'received' : 'current', + actualTime: createdLog.time + }); } catch (error) { console.error("Failed to sync with Salesforce:", error.message); console.log(error) @@ -229,9 +314,8 @@ async function va_project_log_callback(req, res) { throw salesforceError; } - // return res.status(201).send({ success: true, message: `${type} Successfully`, logId: createdLog._id }); - } catch (error) { + console.error("Error in va_project_log_callback:", error); return res.status(500).send("Internal Server Error"); } } diff --git a/src/models/vaModel.js b/src/models/vaModel.js index b33241f..b16190f 100644 --- a/src/models/vaModel.js +++ b/src/models/vaModel.js @@ -320,6 +320,9 @@ const vaWorkTrackSchema = new Schema({ appName: { type: String, }, + url: { + type: String, + }, from: { type: Date, default: Date.now, diff --git a/src/routes/v1/index.js b/src/routes/v1/index.js index a426f60..4cd658a 100644 --- a/src/routes/v1/index.js +++ b/src/routes/v1/index.js @@ -12,6 +12,7 @@ import { process_reset } from "../../controllers/passwordReset.controllers.js"; import { project_add_sf } from "../../controllers/project/projectAddSf.controller.js" import { update_project_sf } from "../../controllers/project/projectUpdateSf.controller.js" import { authValidator } from "../../middlewares/auth.middleware.js"; +import { delete_project_sf } from "../../controllers/project/deleteProjectSf.controller.js" const v1Routes = express.Router() @@ -29,6 +30,7 @@ v1Routes.use("/client", clientRoutes); v1Routes.post("/project", authValidator, project_add_sf); v1Routes.patch("/project/:projectId", authValidator, update_project_sf); +v1Routes.delete("/project/:projectId", authValidator, delete_project_sf); v1Routes.post("/password/reset", process_reset); diff --git a/src/routes/v1/va.routes.js b/src/routes/v1/va.routes.js index 087e684..fa8549d 100644 --- a/src/routes/v1/va.routes.js +++ b/src/routes/v1/va.routes.js @@ -26,6 +26,7 @@ import { va_verify_email } from "../../controllers/va/vaAuth/vaVerifyEmail.contr import { va_leave_apply_function } from "../../controllers/va/vaLeaves/vaLeavesApply.controller.js"; import { va_leave_list_function } from "../../controllers/va/vaLeaves/vaLeaveList.controller.js" import { va_leave_manage_function } from "../../controllers/va/vaLeaves/vaLeaveManage.controller.js"; +import { va_remove_function } from "../../controllers/va/vaProfile/vaDelete.controller.js"; vaRoutes.get("/", va_callback); // VA Callback for getting active VA @@ -42,6 +43,7 @@ vaRoutes.patch("/profile", authValidator, va_profile_update_callback); // Update vaRoutes.get("/:userName", va_username_callback); // VA Username Callback +vaRoutes.delete("/:id", va_remove_function); // VA Username Callback // timesheet adjustment routes vaRoutes.get("/timesheet/summary/:projectId/:date", authValidator, va_timesheet_summary_function); // Get Timesheet Adjustment for VA on specific project and date From 16a62670e7f916d2521626ba0196eae2d71fe033 Mon Sep 17 00:00:00 2001 From: Aakash Date: Wed, 26 Nov 2025 19:38:08 +0530 Subject: [PATCH 3/7] Added delete route form project, va, and client - Updated leaves and time adjustment api --- public/js/va-register.js | 369 +++++++++++++----- src/controllers/client/vaClient.controller.js | 107 ++++- .../project/projectAddSf.controller.js | 32 +- .../va/vaLeaves/vaLeavesApply.controller.js | 195 +++++++-- .../vaProfile/vaProfileUpdate.controller.js | 118 ++++-- .../va/vaProfile/vaRegister.controller.js | 1 + .../vaProjects/vaProjectReport.controller.js | 41 +- .../vaTimesheetAdjustment.controller.js | 131 ++++++- ...vaTimesheetAdjustmentSummary.controller.js | 47 ++- src/middlewares/auth.middleware.js | 2 + src/models/vaModel.js | 19 + src/repository/vaClient.repository.js | 25 +- src/routes/v1/client.routes.js | 4 +- src/routes/v1/va.routes.js | 4 +- src/services/vaClient.service.js | 138 ++++++- views/va_register.ejs | 30 +- 16 files changed, 1002 insertions(+), 261 deletions(-) diff --git a/public/js/va-register.js b/public/js/va-register.js index fea6594..ef2e9ac 100644 --- a/public/js/va-register.js +++ b/public/js/va-register.js @@ -9,65 +9,113 @@ // Countries and states data const countries = [ - { code: 'US', name: 'United States' }, - { code: 'CA', name: 'Canada' }, - { code: 'GB', name: 'United Kingdom' }, - { code: 'AU', name: 'Australia' }, - { code: 'IN', name: 'India' }, - { code: 'DE', name: 'Germany' }, - { code: 'FR', name: 'France' }, - { code: 'BR', name: 'Brazil' }, - { code: 'JP', name: 'Japan' }, - { code: 'CN', name: 'China' } + { code: 'US', name: 'United States' }, + { code: 'CA', name: 'Canada' }, + { code: 'GB', name: 'United Kingdom' }, + { code: 'AU', name: 'Australia' }, + { code: 'IN', name: 'India' }, + { code: 'DE', name: 'Germany' }, + { code: 'FR', name: 'France' }, + { code: 'BR', name: 'Brazil' }, + { code: 'JP', name: 'Japan' }, + { code: 'CN', name: 'China' }, + { code: 'MX', name: 'Mexico' }, + { code: 'ES', name: 'Spain' }, + { code: 'IT', name: 'Italy' }, + { code: 'NL', name: 'Netherlands' }, + { code: 'SE', name: 'Sweden' }, + { code: 'NO', name: 'Norway' }, + { code: 'DK', name: 'Denmark' }, + { code: 'FI', name: 'Finland' }, + { code: 'BE', name: 'Belgium' }, + { code: 'CH', name: 'Switzerland' }, + { code: 'AT', name: 'Austria' }, + { code: 'PT', name: 'Portugal' }, + { code: 'IE', name: 'Ireland' }, + { code: 'PL', name: 'Poland' }, + { code: 'CZ', name: 'Czech Republic' }, + { code: 'HU', name: 'Hungary' }, + { code: 'RO', name: 'Romania' }, + { code: 'GR', name: 'Greece' }, + { code: 'TR', name: 'Turkey' }, + { code: 'SA', name: 'Saudi Arabia' }, + { code: 'AE', name: 'United Arab Emirates' }, + { code: 'ZA', name: 'South Africa' }, + { code: 'NG', name: 'Nigeria' }, + { code: 'EG', name: 'Egypt' }, + { code: 'KE', name: 'Kenya' }, + { code: 'AR', name: 'Argentina' }, + { code: 'CO', name: 'Colombia' }, + { code: 'PE', name: 'Peru' }, + { code: 'CL', name: 'Chile' }, + { code: 'VE', name: 'Venezuela' }, + { code: 'KR', name: 'South Korea' }, + { code: 'SG', name: 'Singapore' }, + { code: 'MY', name: 'Malaysia' }, + { code: 'TH', name: 'Thailand' }, + { code: 'ID', name: 'Indonesia' }, + { code: 'PH', name: 'Philippines' }, + { code: 'VN', name: 'Vietnam' }, + { code: 'PK', name: 'Pakistan' }, + { code: 'BD', name: 'Bangladesh' }, + { code: 'LK', name: 'Sri Lanka' }, + { code: 'NP', name: 'Nepal' }, + { code: 'RU', name: 'Russia' }, + { code: 'UA', name: 'Ukraine' }, + { code: 'IL', name: 'Israel' }, + { code: 'NZ', name: 'New Zealand' }, + { code: 'HK', name: 'Hong Kong' }, + { code: 'TW', name: 'Taiwan' } ]; - const states = { - 'US': [ - { code: 'CA', name: 'California' }, { code: 'TX', name: 'Texas' }, { code: 'FL', name: 'Florida' }, - { code: 'NY', name: 'New York' }, { code: 'IL', name: 'Illinois' }, { code: 'PA', name: 'Pennsylvania' }, - { code: 'OH', name: 'Ohio' }, { code: 'GA', name: 'Georgia' }, { code: 'NC', name: 'North Carolina' }, - { code: 'MI', name: 'Michigan' } - ], - 'CA': [ - { code: 'ON', name: 'Ontario' }, { code: 'QC', name: 'Quebec' }, { code: 'BC', name: 'British Columbia' }, - { code: 'AB', name: 'Alberta' }, { code: 'MB', name: 'Manitoba' }, { code: 'SK', name: 'Saskatchewan' } - ], - 'GB': [ - { code: 'ENG', name: 'England' }, { code: 'SCT', name: 'Scotland' }, { code: 'WLS', name: 'Wales' }, - { code: 'NIR', name: 'Northern Ireland' } - ], - 'AU': [ - { code: 'NSW', name: 'New South Wales' }, { code: 'VIC', name: 'Victoria' }, { code: 'QLD', name: 'Queensland' }, - { code: 'WA', name: 'Western Australia' }, { code: 'SA', name: 'South Australia' }, { code: 'TAS', name: 'Tasmania' } - ], - 'IN': [ - { code: 'MH', name: 'Maharashtra' }, { code: 'TN', name: 'Tamil Nadu' }, { code: 'UP', name: 'Uttar Pradesh' }, - { code: 'KA', name: 'Karnataka' }, { code: 'GJ', name: 'Gujarat' }, { code: 'RJ', name: 'Rajasthan' } - ], - 'DE': [ - { code: 'BW', name: 'Baden-Württemberg' }, { code: 'BY', name: 'Bavaria' }, { code: 'BE', name: 'Berlin' }, - { code: 'HB', name: 'Bremen' }, { code: 'HH', name: 'Hamburg' }, { code: 'HE', name: 'Hesse' } - ], - 'FR': [ - { code: 'ARA', name: 'Auvergne-Rhône-Alpes' }, { code: 'BFC', name: 'Bourgogne-Franche-Comté' }, - { code: 'BRE', name: 'Brittany' }, { code: 'CVL', name: 'Centre-Val de Loire' }, - { code: 'COR', name: 'Corsica' }, { code: 'GES', name: 'Grand Est' } - ], - 'BR': [ - { code: 'SP', name: 'São Paulo' }, { code: 'RJ', name: 'Rio de Janeiro' }, { code: 'MG', name: 'Minas Gerais' }, - { code: 'BA', name: 'Bahia' }, { code: 'RS', name: 'Rio Grande do Sul' }, { code: 'PR', name: 'Paraná' } - ], - 'JP': [ - { code: 'TK', name: 'Tokyo' }, { code: 'OS', name: 'Osaka' }, { code: 'KY', name: 'Kyoto' }, - { code: 'HK', name: 'Hokkaido' }, { code: 'OK', name: 'Okinawa' }, { code: 'KN', name: 'Kanagawa' } - ], - 'CN': [ - { code: 'BJ', name: 'Beijing' }, { code: 'SH', name: 'Shanghai' }, { code: 'TJ', name: 'Tianjin' }, - { code: 'CQ', name: 'Chongqing' }, { code: 'GD', name: 'Guangdong' }, { code: 'JS', name: 'Jiangsu' } - ] - }; + // const states = { + // 'US': [ + // { code: 'CA', name: 'California' }, { code: 'TX', name: 'Texas' }, { code: 'FL', name: 'Florida' }, + // { code: 'NY', name: 'New York' }, { code: 'IL', name: 'Illinois' }, { code: 'PA', name: 'Pennsylvania' }, + // { code: 'OH', name: 'Ohio' }, { code: 'GA', name: 'Georgia' }, { code: 'NC', name: 'North Carolina' }, + // { code: 'MI', name: 'Michigan' } + // ], + // 'CA': [ + // { code: 'ON', name: 'Ontario' }, { code: 'QC', name: 'Quebec' }, { code: 'BC', name: 'British Columbia' }, + // { code: 'AB', name: 'Alberta' }, { code: 'MB', name: 'Manitoba' }, { code: 'SK', name: 'Saskatchewan' } + // ], + // 'GB': [ + // { code: 'ENG', name: 'England' }, { code: 'SCT', name: 'Scotland' }, { code: 'WLS', name: 'Wales' }, + // { code: 'NIR', name: 'Northern Ireland' } + // ], + // 'AU': [ + // { code: 'NSW', name: 'New South Wales' }, { code: 'VIC', name: 'Victoria' }, { code: 'QLD', name: 'Queensland' }, + // { code: 'WA', name: 'Western Australia' }, { code: 'SA', name: 'South Australia' }, { code: 'TAS', name: 'Tasmania' } + // ], + // 'IN': [ + // { code: 'MH', name: 'Maharashtra' }, { code: 'TN', name: 'Tamil Nadu' }, { code: 'UP', name: 'Uttar Pradesh' }, + // { code: 'KA', name: 'Karnataka' }, { code: 'GJ', name: 'Gujarat' }, { code: 'RJ', name: 'Rajasthan' } + // ], + // 'DE': [ + // { code: 'BW', name: 'Baden-Württemberg' }, { code: 'BY', name: 'Bavaria' }, { code: 'BE', name: 'Berlin' }, + // { code: 'HB', name: 'Bremen' }, { code: 'HH', name: 'Hamburg' }, { code: 'HE', name: 'Hesse' } + // ], + // 'FR': [ + // { code: 'ARA', name: 'Auvergne-Rhône-Alpes' }, { code: 'BFC', name: 'Bourgogne-Franche-Comté' }, + // { code: 'BRE', name: 'Brittany' }, { code: 'CVL', name: 'Centre-Val de Loire' }, + // { code: 'COR', name: 'Corsica' }, { code: 'GES', name: 'Grand Est' } + // ], + // 'BR': [ + // { code: 'SP', name: 'São Paulo' }, { code: 'RJ', name: 'Rio de Janeiro' }, { code: 'MG', name: 'Minas Gerais' }, + // { code: 'BA', name: 'Bahia' }, { code: 'RS', name: 'Rio Grande do Sul' }, { code: 'PR', name: 'Paraná' } + // ], + // 'JP': [ + // { code: 'TK', name: 'Tokyo' }, { code: 'OS', name: 'Osaka' }, { code: 'KY', name: 'Kyoto' }, + // { code: 'HK', name: 'Hokkaido' }, { code: 'OK', name: 'Okinawa' }, { code: 'KN', name: 'Kanagawa' } + // ], + // 'CN': [ + // { code: 'BJ', name: 'Beijing' }, { code: 'SH', name: 'Shanghai' }, { code: 'TJ', name: 'Tianjin' }, + // { code: 'CQ', name: 'Chongqing' }, { code: 'GD', name: 'Guangdong' }, { code: 'JS', name: 'Jiangsu' } + // ] + // }; // Initialize the form + document.addEventListener('DOMContentLoaded', function() { // Populate country dropdown const countrySelect = document.getElementById('country'); @@ -80,32 +128,34 @@ countrySelect.appendChild(option); }); + autoDetectCountry(); + // Add event listener for country change - countrySelect.addEventListener('change', function() { - const countryCode = this.value; - const stateSelect = document.getElementById('state'); + // countrySelect.addEventListener('change', function() { + // const countryCode = this.value; + // const stateSelect = document.getElementById('state'); - // Show loading - document.getElementById('state-loading').style.display = 'block'; + // // Show loading + // document.getElementById('state-loading').style.display = 'block'; - // Clear current states - stateSelect.innerHTML = ''; + // // Clear current states + // stateSelect.innerHTML = ''; - // Simulate API delay - setTimeout(() => { - if (countryCode && states[countryCode]) { - states[countryCode].forEach(state => { - const option = document.createElement('option'); - option.value = state.code; - option.textContent = state.name; - stateSelect.appendChild(option); - }); - } + // // Simulate API delay + // setTimeout(() => { + // if (countryCode && states[countryCode]) { + // states[countryCode].forEach(state => { + // const option = document.createElement('option'); + // option.value = state.code; + // option.textContent = state.name; + // stateSelect.appendChild(option); + // }); + // } - // Hide loading - document.getElementById('state-loading').style.display = 'none'; - }, 500); - }); + // // Hide loading + // document.getElementById('state-loading').style.display = 'none'; + // }, 500); + // }); // Add input event listeners to handle label animation @@ -209,6 +259,102 @@ }); }); + async function autoDetectCountry() { + const countrySelect = document.getElementById('country'); + // const loadingElement = document.getElementById('country-loading'); + + try { + // loadingElement.style.display = 'block'; + + // Using ipapi.co API (free tier available) + const response = await fetch('https://ipapi.co/json/'); + + if (!response.ok) { + throw new Error('Geolocation service unavailable'); + } + + const data = await response.json(); + + if (data && data.country_code) { + const detectedCountryCode = data.country_code; + const detectedCountryName = data.country_name; + + // Find if the detected country is in our list + const countryExists = countries.find(country => + country.code === detectedCountryCode + ); + + if (countryExists) { + // Set the country in dropdown + countrySelect.value = detectedCountryCode; + + // Trigger the change event to update UI + countrySelect.dispatchEvent(new Event('change')); + + // Show success message + console.log(`Auto-detected country: ${detectedCountryName}`); + } else { + console.log('Detected country not in supported list'); + } + } + } catch (error) { + console.log('Could not auto-detect country:', error.message); + // Fallback: Use browser's geolocation API as backup + fallbackGeolocation(); + } finally { + // loadingElement.style.display = 'none'; + } + } + + // Fallback geolocation using browser's navigator.geolocation + function fallbackGeolocation() { + if (!navigator.geolocation) { + console.log('Geolocation is not supported by this browser'); + return; + } + + navigator.geolocation.getCurrentPosition( + async function(position) { + try { + const { latitude, longitude } = position.coords; + + // Use OpenStreetMap Nominatim API for reverse geocoding + const response = await fetch( + `https://nominatim.openstreetmap.org/reverse?format=json&lat=${latitude}&lon=${longitude}` + ); + + if (response.ok) { + const data = await response.json(); + if (data && data.address && data.address.country_code) { + const countryCode = data.address.country_code.toUpperCase(); + const countrySelect = document.getElementById('country'); + + const countryExists = countries.find(country => + country.code === countryCode + ); + + if (countryExists) { + countrySelect.value = countryCode; + countrySelect.dispatchEvent(new Event('change')); + console.log(`Fallback geolocation detected: ${data.address.country}`); + } + } + } + } catch (error) { + console.log('Fallback geolocation failed:', error); + } + }, + function(error) { + console.log('Browser geolocation error:', error.message); + }, + { + enableHighAccuracy: false, + timeout: 10000, + maximumAge: 600000 // 10 minutes + } + ); + } + // Function to navigate to any step (for tab navigation) function goToStep(step) { // Only allow navigation to completed steps or the next step @@ -469,20 +615,29 @@ showError('Please select a payment method', null, step); isValid = false; } else { - const paymentMethod = selectedPayment.getAttribute('onclick').match(/'([^']+)'/)[1]; - if (paymentMethod === 'payoneer') { - const payoneerAccount = document.getElementById('payoneerAccount').value; - if (!payoneerAccount) { - showError('Please enter your Payoneer account number', 'payoneerAccount', step); - isValid = false; - } - } else if (paymentMethod === 'veem') { - const veemAccount = document.getElementById('veemAccount').value; - if (!veemAccount) { - showError('Please enter your Veem account number', 'veemAccount', step); - isValid = false; - } - } + const paymentMethod = selectedPayment.getAttribute('onclick').match(/'([^']+)'/)[1]; + if (paymentMethod === 'payoneer') { + // const payoneerAccount = document.getElementById('payoneerAccount').value; + // if (!payoneerAccount) { + // showError('Please enter your Payoneer account number', 'payoneerAccount', step); + // isValid = false; + // } + } else if (paymentMethod === 'veem') { + const veemAccount = document.getElementById('veemAccount').value; + if (!veemAccount) { + showError('Please enter your Veem account number', 'veemAccount', step); + isValid = false; + } + } else if (paymentMethod === 'wise') { + const wiseEmail = document.getElementById('wiseEmail').value; + if (!wiseEmail) { + showError('Please enter your Wise email address', 'wiseEmail', step); + isValid = false; + } else if (!validateEmail(wiseEmail)) { + showError('Please enter a valid email address for Wise', 'wiseEmail', step); + isValid = false; + } + } } } @@ -547,12 +702,15 @@ const paymentMethod = selectedPayment.getAttribute('onclick').match(/'([^']+)'/)[1]; formData.userDetails.paymentMethod = paymentMethod; - if (document.getElementById('payoneerAccount').value) { - formData.userDetails.payoneerAccount = document.getElementById('payoneerAccount').value; - } + // if (document.getElementById('payoneerAccount').value) { + // formData.userDetails.payoneerAccount = document.getElementById('payoneerAccount').value; + // } if (document.getElementById('veemAccount').value) { formData.userDetails.veemAccount = document.getElementById('veemAccount').value; } + if (paymentMethod === 'wise') { + formData.userDetails.wiseEmail = document.getElementById('wiseEmail').value; + } } break; } @@ -575,12 +733,35 @@ // Show appropriate payment details if (method === 'payoneer') { - document.getElementById('payoneer-details').style.display = 'block'; + // document.getElementById('payoneer-details').style.display = 'block'; } else if (method === 'veem') { document.getElementById('veem-details').style.display = 'block'; + } else if (method === 'wise') { + document.getElementById('wise-details').style.display = 'block'; } } + function validateWiseEmail(input) { + const wiseError = document.getElementById('wise-email-error'); + const value = input.value.trim(); + + // Remove any validation styling first + input.style.border = ''; + wiseError.style.display = 'none'; + + if (value.length > 0) { + // Check if it's a valid email + if (!validateEmail(value)) { + wiseError.textContent = 'Please enter a valid email address for Wise'; + wiseError.style.display = 'block'; + input.style.border = '1px solid #ef4444'; + return false; + } + } + + return true; + } + // Function to generate a strong password function generatePassword() { const length = 12; diff --git a/src/controllers/client/vaClient.controller.js b/src/controllers/client/vaClient.controller.js index 700b238..8f23662 100644 --- a/src/controllers/client/vaClient.controller.js +++ b/src/controllers/client/vaClient.controller.js @@ -76,7 +76,112 @@ async function get_client_by_email(req, res) { } } +async function delete_client(req, res) { + logger.info("ClientController - Delete Client Hit ..."); + + try { + const { clientId } = req.params; + + if (!clientId) { + return res.status(400).json({ + success: false, + message: "Client ID is required" + }); + } + + const deletedClient = await vaClientService.deleteClient(clientId); + + if (!deletedClient) { + return res.status(404).json({ + success: false, + message: "Client not found" + }); + } + + logger.info(`ClientController - Client deleted successfully: ${clientId}`); + return res.status(200).json({ + success: true, + message: "Client deleted successfully", + data: { + clientId: clientId, + clientName: deletedClient.userDetails?.name || 'Unknown Client', + deletedAt: new Date().toISOString() + } + }); + } catch (error) { + logger.error(`ClientController - Delete error: ${error.message}`); + + if (error.message === "Client not found") { + return res.status(404).json({ + success: false, + message: error.message + }); + } + + return res.status(500).json({ + success: false, + message: "Internal Server Error" + }); + } +} + +async function update_client(req, res) { + logger.info("ClientController - Update Client Hit ..."); + + try { + const { clientId } = req.params; + const payload = req.body || {}; + const { userDetails } = payload; + + if (!clientId) { + return res.status(400).json({ + success: false, + message: "Client ID is required" + }); + } + + if (!userDetails) { + return res.status(400).json({ + success: false, + message: "Missing userDetails in request body" + }); + } + + const updatedClient = await vaClientService.updateClient(clientId, payload); + + logger.info(`ClientController - Client updated successfully: ${clientId}`); + return res.status(200).json({ + success: true, + message: "Client updated successfully", + data: updatedClient + }); + } catch (error) { + logger.error(`ClientController - Update error: ${error.message}`); + + if (error.message === "Client not found") { + return res.status(404).json({ + success: false, + message: error.message + }); + } + + if (error.message.includes("Email already exists")) { + return res.status(409).json({ + success: false, + message: error.message + }); + } + + return res.status(500).json({ + success: false, + message: "Internal Server Error" + }); + } +} + export { va_register_client, - get_client_by_email + get_client_by_email, + delete_client, + update_client }; \ No newline at end of file diff --git a/src/controllers/project/projectAddSf.controller.js b/src/controllers/project/projectAddSf.controller.js index cc08b15..cabe634 100644 --- a/src/controllers/project/projectAddSf.controller.js +++ b/src/controllers/project/projectAddSf.controller.js @@ -81,36 +81,8 @@ async function project_add_sf(req, res) { console.warn('Project created but relationship update failed:', relationshipUpdate.message); } - // Send data to Salesforce API after successful registration - try { - const salesforceUrl = `${process.env.SF_DOCTRACKER_API_URL}services/apexrest/docva/api/`; - - const response = await fetch(salesforceUrl, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(created) - }); - - if (!response.ok) { - const errorText = await response.text(); - throw new Error(`Salesforce API responded with status ${response.status}: ${errorText}`); - } - - const result = await response.json(); - console.log("✅ Successfully synced VA data to Salesforce:", result); - - res.status(201).json({ success: true, data: created, SyncSalesforce: result }); - // res.status(201).json({ success: true, data: created, relationships: relationshipUpdate }); - } catch (error) { - console.error("❌ Failed to sync with Salesforce:", error.message); - - // Create a custom error to distinguish Salesforce errors - const salesforceError = new Error(`Salesforce sync failed: ${error.message}`); - salesforceError.isSalesforceError = true; - throw salesforceError; - } + res.status(201).json({ success: true, data: created, relationships: relationshipUpdate }); + } catch (error) { console.error("project_add_sf error:", error); return res.status(500).json({ success: false, message: "Internal Server Error" }); diff --git a/src/controllers/va/vaLeaves/vaLeavesApply.controller.js b/src/controllers/va/vaLeaves/vaLeavesApply.controller.js index b7e3fde..5076441 100644 --- a/src/controllers/va/vaLeaves/vaLeavesApply.controller.js +++ b/src/controllers/va/vaLeaves/vaLeavesApply.controller.js @@ -131,6 +131,7 @@ async function va_leave_apply_function(req, res) { }); } + const processedComment = processHTMLComment(comment); // Create leave request const leaveRequest = new LeavesModel({ vaId, @@ -138,62 +139,62 @@ async function va_leave_apply_function(req, res) { dateTo: endDate, days: totalDays.toString(), leaveType, - comment, + comment: processedComment.processedHTML, + attachments: processedComment.extractedImages, status: "PENDING" }); await leaveRequest.save(); try { - const salesforceUrl = `${process.env.SF_DOCTRACKER_API_URL}services/apexrest/docva/api/timesheet-adjustment`; - const sfData = { - "id": adjustmentRequest._id, - "va": { - "id": vaId - }, - "project": { - "id": projectId - }, - "date": adjustmentDate.toISOString().split('T')[0], - "loginTime": loginDateTime.toISOString(), - "logoutTime": logoutDateTime.toISOString(), - "comment": comment, - "status": "PENDING", - "rejectionReason": "", - "adjustedHours": formatTime(adjustedSeconds) - } + // const salesforceUrl = `${process.env.SF_DOCTRACKER_API_URL}services/apexrest/docva/api/timesheet-adjustment`; + // const sfData = { + // "id": leaveRequest._id, + // "va": { + // "id": vaId + // }, + // "dateFrom": startDate.toISOString().split('T')[0], + // "dateTo": endDate.toISOString().split('T')[0], + // "totalDays": totalDays, + // "leaveType": leaveType, + // "comment": processedComment.plainText, // Send plain text to Salesforce + // "attachments": processedComment.extractedImages, // Send extracted images info + // "status": "PENDING", + // "rejectionReason": "" + // } - 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({"data": sfData}) - }); + // 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({"data": sfData}) + // }); - if (!response.ok) { - const errorText = await response.text(); - throw new Error(`Salesforce API responded with status ${response.status}: ${errorText}`); - } + // if (!response.ok) { + // const errorText = await response.text(); + // throw new Error(`Salesforce API responded with status ${response.status}: ${errorText}`); + // } - const result = await response.json(); - // console.log("✅ Successfully synced VA data to Salesforce:", result); + // const result = await response.json(); + // // console.log("✅ Successfully synced VA data to Salesforce:", result); return res.status(201).json({ success: true, - message: "Adjustment request submitted successfully", + message: "Leave request submitted successfully", data: { - adjustmentId: adjustmentRequest._id, - date: adjustmentDate.toISOString().split('T')[0], - loginTime: loginDateTime.toISOString(), - logoutTime: logoutDateTime.toISOString(), - adjustedHours: formatTime(adjustedSeconds), - comment: comment, + leaveId: leaveRequest._id, + dateFrom: startDate.toISOString().split('T')[0], + dateTo: endDate.toISOString().split('T')[0], + totalDays: totalDays, + leaveType, + comment: processedComment.processedHTML, + attachments: processedComment.extractedImages, status: "PENDING" }, - SyncSalesforce: result + // SyncSalesforce: result }); } catch (error) { console.error("Failed to sync with Salesforce:", error.message); @@ -238,4 +239,116 @@ async function va_leave_apply_function(req, res) { } } +function processHTMLComment(htmlComment) { + try { + let processedHTML = htmlComment; + const extractedImages = []; + let plainText = ''; + + // Extract plain text from HTML for Salesforce + plainText = extractPlainTextFromHTML(htmlComment); + + // Process base64 images in HTML + const imageProcessingResult = extractAndProcessBase64Images(htmlComment); + processedHTML = imageProcessingResult.processedHTML; + extractedImages.push(...imageProcessingResult.extractedImages); + + return { + processedHTML, + plainText, + extractedImages + }; + } catch (error) { + console.error("Error processing HTML comment:", error); + // Return original content if processing fails + return { + processedHTML: htmlComment, + plainText: extractPlainTextFromHTML(htmlComment), + extractedImages: [] + }; + } +} + +// Helper function to extract plain text from HTML +function extractPlainTextFromHTML(html) { + try { + // Remove HTML tags and decode HTML entities + let plainText = html + .replace(/<[^>]*>/g, ' ') // Replace tags with spaces + .replace(/\s+/g, ' ') // Collapse multiple spaces + .trim(); + + // Decode common HTML entities + plainText = plainText + .replace(/ /g, ' ') + .replace(/&/g, '&') + .replace(/</g, '<') + .replace(/>/g, '>') + .replace(/"/g, '"') + .replace(/'/g, "'") + .replace(/'/g, "'") + .replace(///g, '/'); + + return plainText; + } catch (error) { + console.error("Error extracting plain text from HTML:", error); + return html.replace(/<[^>]*>/g, ' ').trim(); + } +} + +// Helper function to extract and process base64 images from HTML +function extractAndProcessBase64Images(html) { + const extractedImages = []; + let processedHTML = html; + + try { + // Regex to find base64 images in HTML (various formats) + const base64Regex = /]+src="data:image\/([a-zA-Z0-9+]+);base64,([^"]+)"[^>]*>/gi; + + let match; + let imageIndex = 0; + + while ((match = base64Regex.exec(html)) !== null) { + const fullMatch = match[0]; + const imageType = match[1]; // png, jpeg, gif, etc. + const base64Data = match[2]; + + // Create a unique filename + const timestamp = Date.now(); + const filename = `leave_image_${timestamp}_${imageIndex}.${imageType}`; + + // Calculate approximate file size + const size = Math.floor((base64Data.length * 3) / 4); // Base64 size calculation + + // Store image information + extractedImages.push({ + filename: filename, + mimeType: `image/${imageType}`, + base64Data: base64Data, + size: size, + originalHTML: fullMatch, // Store original img tag for reference + index: imageIndex + }); + + // Replace base64 with a reference in HTML + const replacementHTML = `Leave evidence ${imageIndex + 1}`; + + processedHTML = processedHTML.replace(fullMatch, replacementHTML); + + imageIndex++; + } + + return { + processedHTML, + extractedImages + }; + } catch (error) { + console.error("Error processing base64 images:", error); + return { + processedHTML: html, + extractedImages: [] + }; + } +} + export { va_leave_apply_function }; \ No newline at end of file diff --git a/src/controllers/va/vaProfile/vaProfileUpdate.controller.js b/src/controllers/va/vaProfile/vaProfileUpdate.controller.js index 66a78b3..0e8082f 100644 --- a/src/controllers/va/vaProfile/vaProfileUpdate.controller.js +++ b/src/controllers/va/vaProfile/vaProfileUpdate.controller.js @@ -5,28 +5,61 @@ import { VAModel } from "../../../models/vaModel.js"; // Register a new VA async function va_profile_update_callback(req, res) { try { + const { sf_token } = req.headers.sf_token; const accessToken = req.headers.token; - if (!accessToken) { - return res.status(401).json({ success: false, message: "Missing access token" }); - } - - let decoded; - try { - decoded = jwt.verify(accessToken, process.env.JWT_SECRET_ACCESS_TOKEN); - } catch (err) { - return res.status(401).json({ success: false, message: "Invalid or expired token" }); - } - - if (!decoded?.email) { - return res.status(400).json({ success: false, message: "Invalid token payload" }); - } - - const vaUser = await VAModel.findOne({ "userDetails.email": decoded.email }); - if (!vaUser) { - return res.status(404).json({ success: false, message: "VA not found" }); + let vaUser; + let authSource = 'regular'; + + if(sf_token) { + try { + const sfDecoded = jwt.verify(sf_token, process.env.JWT_SECRET_SF_TOKEN); + + if (!sfDecoded?.email && !sfDecoded?.vaId) { + return res.status(400).json({ success: false, message: "Invalid Salesforce token payload" }); + } + + // Find user by email or VA ID from Salesforce token + if (sfDecoded.email) { + vaUser = await VAModel.findOne({ "userDetails.email": sfDecoded.email }); + } else if (sfDecoded.vaId) { + vaUser = await VAModel.findById(sfDecoded.vaId); + } + + if (!vaUser) { + return res.status(404).json({ success: false, message: "VA not found with Salesforce token" }); + } + + authSource = 'salesforce'; + + } catch (sfError) { + return res.status(401).json({ success: false, message: "Invalid or expired Salesforce token" }); + } + } else { + try { + if (!accessToken) { + return res.status(401).json({ success: false, message: "Missing access token" }); + } + + const decoded = jwt.verify(accessToken, process.env.JWT_SECRET_ACCESS_TOKEN); + + if (!decoded?.email) { + return res.status(400).json({ success: false, message: "Invalid token payload" }); + } + + vaUser = await VAModel.findOne({ "userDetails.email": decoded.email }); + if (!vaUser) { + return res.status(404).json({ success: false, message: "VA not found" }); + } + + authSource = 'regular'; + + } catch (tokenError) { + return res.status(401).json({ success: false, message: "Invalid or expired access token" }); + } } + const payload = req.body || {}; const { userDetails } = payload; @@ -34,23 +67,36 @@ async function va_profile_update_callback(req, res) { return res.status(400).json({ success: false, message: "Missing userDetails" }); } - // Allowed fields to update - // const allowedFields = [ - // "firstName", "lastName", "dob", "age", "address", - // "bankName", "accountHolderName", "accountNumber", "routingNumber" - // ]; + const allowedFields = [ + "firstName", "lastName", "dob", "age", "address", + "city", "state", "country", "phone", "skype", + "paymentMethod", "payoneerAccount", "veemAccount" + ]; - // // Update allowed fields only - // allowedFields.forEach(field => { - // if (userDetails[field] !== undefined) { - // vaUser.userDetails[field] = userDetails[field]; - // } - // }); + let hasUpdates = false; + const updatedFields = []; + + // Update allowed fields if provided + allowedFields.forEach(field => { + if (userDetails[field] !== undefined && userDetails[field] !== null && userDetails[field] !== '') { + vaUser.userDetails[field] = userDetails[field]; + hasUpdates = true; + updatedFields.push(field); + } + }); /* ---------------------- PASSWORD UPDATE ---------------------- */ + let passwordUpdated = false; if (userDetails.password || userDetails.cPassword || userDetails.oldPassword) { const { oldPassword, password, cPassword } = userDetails; + if (authSource === 'salesforce') { + return res.status(403).json({ + success: false, + message: "Password changes require regular authentication", + }); + } + if (!oldPassword || !password || !cPassword) { return res.status(400).json({ success: false, @@ -78,11 +124,16 @@ async function va_profile_update_callback(req, res) { const saltRounds = 10; const hashedPassword = await bcrypt.hash(password, saltRounds); vaUser.userDetails.password = hashedPassword; - } else { + hasUpdates = true; + passwordUpdated = true; + updatedFields.push('password'); + } + + if (!hasUpdates) { return res.status(400).json({ - success: false, - message: "Data not found", - }); + success: false, + message: "No valid fields to update", + }); } await vaUser.save(); @@ -95,6 +146,7 @@ async function va_profile_update_callback(req, res) { success: true, message: "Profile updated successfully", data: updatedUser, + updatedFields: updatedFields, }); } catch (error) { return res.status(500).json({ success: false, message: "Internal Server Error" }); diff --git a/src/controllers/va/vaProfile/vaRegister.controller.js b/src/controllers/va/vaProfile/vaRegister.controller.js index e88cecf..5746119 100644 --- a/src/controllers/va/vaProfile/vaRegister.controller.js +++ b/src/controllers/va/vaProfile/vaRegister.controller.js @@ -36,6 +36,7 @@ async function va_register(req, res) { paymentMethod: userDetails.paymentMethod || null, payoneerAccount: userDetails.payoneerAccount || null, veemAccount: userDetails.veemAccount || null, + wiseEmail: userDetails.wiseEmail || null, phone: userDetails.phone || null, skype: userDetails.skype || null, }, diff --git a/src/controllers/va/vaProjects/vaProjectReport.controller.js b/src/controllers/va/vaProjects/vaProjectReport.controller.js index 2b2bbc6..13c7a34 100644 --- a/src/controllers/va/vaProjects/vaProjectReport.controller.js +++ b/src/controllers/va/vaProjects/vaProjectReport.controller.js @@ -11,23 +11,36 @@ function formatHMS(ms) { async function va_projects_report_function(req, res) { try { - const accessToken = req.headers.token; - if (!accessToken) { - return res.status(401).json({ success: false, message: "Missing token" }); - } + const sf_token = req.headers.sf_token; + let vaId; + let vaUser; + let vaEmail; + if(sf_token) { + vaId = req.body.vaId; + vaUser = await VAModel.findById(vaId); + if (!vaUser) { + return res.status(404).json({ success: false, message: "VA not found" }); + } + vaEmail = vaUser.userDetails.email + } else { + const accessToken = req.headers.token; + if (!accessToken) { + return res.status(401).json({ success: false, message: "Missing token" }); + } - const decoded = jwt.verify(accessToken, process.env.JWT_SECRET_ACCESS_TOKEN); - if (!decoded || !decoded.email) { - return res.status(401).json({ success: false, message: "Invalid token" }); - } + const decoded = jwt.verify(accessToken, process.env.JWT_SECRET_ACCESS_TOKEN); + if (!decoded || !decoded.email) { + return res.status(401).json({ success: false, message: "Invalid token" }); + } - const vaEmail = decoded.email; - const vaUser = await VAModel.findOne({ "userDetails.email": vaEmail }); - if (!vaUser) { - return res.status(404).json({ success: false, message: "VA not found" }); - } + vaEmail = decoded.email; + vaUser = await VAModel.findOne({ "userDetails.email": vaEmail }); + if (!vaUser) { + return res.status(404).json({ success: false, message: "VA not found" }); + } - const vaId = vaUser._id; + vaId = vaUser._id; + } const { projectId, from, to } = req.params; if (!projectId) { diff --git a/src/controllers/va/vaTimesheetAdjustment/vaTimesheetAdjustment.controller.js b/src/controllers/va/vaTimesheetAdjustment/vaTimesheetAdjustment.controller.js index 6931442..a252386 100644 --- a/src/controllers/va/vaTimesheetAdjustment/vaTimesheetAdjustment.controller.js +++ b/src/controllers/va/vaTimesheetAdjustment/vaTimesheetAdjustment.controller.js @@ -59,14 +59,6 @@ async function va_timesheet_adjustment_function(req, res) { const combineDateTime = (dateObj, timeStr) => { const [hours, minutes, seconds] = timeStr.split(':').map(Number); - - // // Extract date components from the Date object - // const year = dateObj.getFullYear(); - // const month = dateObj.getMonth(); - // const day = dateObj.getDate(); - - // // Create UTC date directly - // return new Date(year, month, day, hours, minutes, seconds); const combined = new Date(dateObj); combined.setHours(hours, minutes, seconds, 0); @@ -120,13 +112,16 @@ async function va_timesheet_adjustment_function(req, res) { }); } + const processedComment = processHTMLComment(comment); + const adjustmentRequest = new AdjustedHoursModel({ vaId, projectId, date: startOfDay, loginTime: loginDateTime, logoutTime: logoutDateTime, - comment, + comment: processedComment.processedHTML, + attachments: processedComment.extractedImages, status: "PENDING" }); @@ -153,7 +148,8 @@ async function va_timesheet_adjustment_function(req, res) { "date": adjustmentDate.toISOString().split('T')[0], "loginTime": loginDateTime.toISOString(), "logoutTime": logoutDateTime.toISOString(), - "comment": comment, + "comment": processedComment.plainText, // Send plain text to Salesforce + "attachments": processedComment.extractedImages, // Send extracted images info "status": "PENDING", "rejectionReason": "", "adjustedHours": formatTime(adjustedSeconds) @@ -186,7 +182,8 @@ async function va_timesheet_adjustment_function(req, res) { loginTime: loginDateTime.toISOString(), logoutTime: logoutDateTime.toISOString(), adjustedHours: formatTime(adjustedSeconds), - comment: comment, + comment: processedComment.processedHTML, + attachments: processedComment.extractedImages, status: "PENDING" }, SyncSalesforce: result @@ -221,4 +218,116 @@ async function va_timesheet_adjustment_function(req, res) { } } +function processHTMLComment(htmlComment) { + try { + let processedHTML = htmlComment; + const extractedImages = []; + let plainText = ''; + + // Extract plain text from HTML for Salesforce + plainText = extractPlainTextFromHTML(htmlComment); + + // Process base64 images in HTML + const imageProcessingResult = extractAndProcessBase64Images(htmlComment); + processedHTML = imageProcessingResult.processedHTML; + extractedImages.push(...imageProcessingResult.extractedImages); + + return { + processedHTML, + plainText, + extractedImages + }; + } catch (error) { + console.error("Error processing HTML comment:", error); + // Return original content if processing fails + return { + processedHTML: htmlComment, + plainText: extractPlainTextFromHTML(htmlComment), + extractedImages: [] + }; + } +} + +// Helper function to extract plain text from HTML +function extractPlainTextFromHTML(html) { + try { + // Remove HTML tags and decode HTML entities + let plainText = html + .replace(/<[^>]*>/g, ' ') // Replace tags with spaces + .replace(/\s+/g, ' ') // Collapse multiple spaces + .trim(); + + // Decode common HTML entities + plainText = plainText + .replace(/ /g, ' ') + .replace(/&/g, '&') + .replace(/</g, '<') + .replace(/>/g, '>') + .replace(/"/g, '"') + .replace(/'/g, "'") + .replace(/'/g, "'") + .replace(///g, '/'); + + return plainText; + } catch (error) { + console.error("Error extracting plain text from HTML:", error); + return html.replace(/<[^>]*>/g, ' ').trim(); + } +} + +// Helper function to extract and process base64 images from HTML +function extractAndProcessBase64Images(html) { + const extractedImages = []; + let processedHTML = html; + + try { + // Regex to find base64 images in HTML (various formats) + const base64Regex = /]+src="data:image\/([a-zA-Z0-9+]+);base64,([^"]+)"[^>]*>/gi; + + let match; + let imageIndex = 0; + + while ((match = base64Regex.exec(html)) !== null) { + const fullMatch = match[0]; + const imageType = match[1]; // png, jpeg, gif, etc. + const base64Data = match[2]; + + // Create a unique filename + const timestamp = Date.now(); + const filename = `adjustment_image_${timestamp}_${imageIndex}.${imageType}`; + + // Calculate approximate file size + const size = Math.floor((base64Data.length * 3) / 4); // Base64 size calculation + + // Store image information + extractedImages.push({ + filename: filename, + mimeType: `image/${imageType}`, + base64Data: base64Data, + size: size, + originalHTML: fullMatch, // Store original img tag for reference + index: imageIndex + }); + + // Replace base64 with a reference in HTML + const replacementHTML = `Adjustment evidence ${imageIndex + 1}`; + + processedHTML = processedHTML.replace(fullMatch, replacementHTML); + + imageIndex++; + } + + return { + processedHTML, + extractedImages + }; + } catch (error) { + console.error("Error processing base64 images:", error); + return { + processedHTML: html, + extractedImages: [] + }; + } +} + export { va_timesheet_adjustment_function }; \ No newline at end of file diff --git a/src/controllers/va/vaTimesheetAdjustment/vaTimesheetAdjustmentSummary.controller.js b/src/controllers/va/vaTimesheetAdjustment/vaTimesheetAdjustmentSummary.controller.js index e0d2096..94020f2 100644 --- a/src/controllers/va/vaTimesheetAdjustment/vaTimesheetAdjustmentSummary.controller.js +++ b/src/controllers/va/vaTimesheetAdjustment/vaTimesheetAdjustmentSummary.controller.js @@ -3,24 +3,37 @@ import { VAModel, ProjectModel, LoggedModel } from "../../../models/vaModel.js"; async function va_timesheet_summary_function(req, res) { try { - const accessToken = req.headers.token; - - if (!accessToken) { - return res.status(401).json({ success: false, message: "Missing token" }); + const sf_token = req.headers.sf_token; + let vaId; + let vaUser; + let vaEmail; + if(sf_token) { + vaId = req.body.vaId; + vaUser = await VAModel.findById(vaId); + if (!vaUser) { + return res.status(404).json({ success: false, message: "VA not found" }); + } + vaEmail = vaUser.userDetails.email + } else { + const accessToken = req.headers.token; + + if (!accessToken) { + return res.status(401).json({ success: false, message: "Missing token" }); + } + + const decoded = jwt.verify(accessToken, process.env.JWT_SECRET_ACCESS_TOKEN); + if (!decoded || !decoded.email) { + return res.status(401).json({ success: false, message: "Invalid token" }); + } + + vaEmail = decoded.email; + vaUser = await VAModel.findOne({ "userDetails.email": vaEmail }); + if (!vaUser) { + return res.status(404).json({ success: false, message: "VA not found" }); + } + + vaId = vaUser._id; } - - const decoded = jwt.verify(accessToken, process.env.JWT_SECRET_ACCESS_TOKEN); - if (!decoded || !decoded.email) { - return res.status(401).json({ success: false, message: "Invalid token" }); - } - - const vaEmail = decoded.email; - const vaUser = await VAModel.findOne({ "userDetails.email": vaEmail }); - if (!vaUser) { - return res.status(404).json({ success: false, message: "VA not found" }); - } - - const vaId = vaUser._id; const { projectId, date } = req.params; if (!projectId || !date) { diff --git a/src/middlewares/auth.middleware.js b/src/middlewares/auth.middleware.js index ee688f6..49cf229 100644 --- a/src/middlewares/auth.middleware.js +++ b/src/middlewares/auth.middleware.js @@ -6,6 +6,8 @@ async function authValidator(req, res, next) { try { const sf_token = req.headers.sf_token; if (sf_token) { + const { vaId } = req.params; + if(vaId) req.body.vaId = vaId; next(); } else { const accessToken = req.headers.token; diff --git a/src/models/vaModel.js b/src/models/vaModel.js index b16190f..ee1ef66 100644 --- a/src/models/vaModel.js +++ b/src/models/vaModel.js @@ -46,6 +46,9 @@ const userDetailsSchema = new Schema({ veemAccount: { type: String, }, + wiseEmail: { + type: String, + }, phone: { type: String, }, @@ -404,6 +407,14 @@ const adjustedHoursSchema = new Schema({ type: String, required: true }, + attachments: [{ + filename: String, + mimeType: String, + base64Data: String, + size: Number, + originalHTML: String, + index: Number + }], status: { type: String, enum: ["PENDING", "APPROVED", "REJECTED"], @@ -453,6 +464,14 @@ const leavesSchema = new Schema({ type: String, required: true }, + attachments: [{ + filename: String, + mimeType: String, + base64Data: String, + size: Number, + originalHTML: String, + index: Number + }], leaveType: { type: String, enum: ["SICK", "PTO"], diff --git a/src/repository/vaClient.repository.js b/src/repository/vaClient.repository.js index d087d4c..15aea74 100644 --- a/src/repository/vaClient.repository.js +++ b/src/repository/vaClient.repository.js @@ -31,10 +31,27 @@ class VaClientRepository { async update(id, data) { try { + // Convert nested userDetails object to dot notation + const updateObj = {}; + + // Handle userDetails fields with dot notation + if (data.userDetails) { + Object.keys(data.userDetails).forEach(key => { + updateObj[`userDetails.${key}`] = data.userDetails[key]; + }); + } + + // Handle other top-level fields if any + Object.keys(data).forEach(key => { + if (key !== 'userDetails') { + updateObj[key] = data[key]; + } + }); + const response = await ClientModel.findByIdAndUpdate( id, - { $set: { ...data } }, - { new: true } + { $set: updateObj }, + { new: true, runValidators: true } ); return response; } catch (error) { @@ -42,9 +59,9 @@ class VaClientRepository { } } - async delete(id) { + async delete(id, options = {}) { try { - const response = await ClientModel.findByIdAndDelete(id); + const response = await ClientModel.findByIdAndDelete(id, options); return response; } catch (error) { throw error; diff --git a/src/routes/v1/client.routes.js b/src/routes/v1/client.routes.js index 066ccc3..cad13c1 100644 --- a/src/routes/v1/client.routes.js +++ b/src/routes/v1/client.routes.js @@ -1,7 +1,7 @@ import express from "express"; const clientRoutes = express.Router(); -import { va_register_client } from "../../controllers/client/vaClient.controller.js"; +import { va_register_client, delete_client, update_client } from "../../controllers/client/vaClient.controller.js"; // import { va_clients_callback } from "../../controllers/va/vaClients/vaClients.controller.js"; import { va_clients_projects_callback } from "../../controllers/va/vaClients/vaClientProject.controller.js"; import { va_login_client } from "../../controllers/client/vaClientLogin.controller.js"; @@ -21,6 +21,8 @@ clientRoutes.get("/refreshToken", client_refresh_token_callback); // Client Refr clientRoutes.post("/forgot-password", client_request_password_reset); clientRoutes.post("/verify-client-email", client_verify_email); clientRoutes.post("/logout", clientAuthValidator, client_logout_callback); +clientRoutes.patch("/:clientId", update_client); +clientRoutes.delete("/:clientId", delete_client); // 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 fa8549d..98cc2b9 100644 --- a/src/routes/v1/va.routes.js +++ b/src/routes/v1/va.routes.js @@ -46,7 +46,7 @@ vaRoutes.get("/:userName", va_username_callback); // VA Username Callback vaRoutes.delete("/:id", va_remove_function); // VA Username Callback // timesheet adjustment routes -vaRoutes.get("/timesheet/summary/:projectId/:date", authValidator, va_timesheet_summary_function); // Get Timesheet Adjustment for VA on specific project and date +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/:status?/:projectId?/:vaId?", get_adjustment_requests_list); // show all adjustment list to approve and reject vaRoutes.patch("/timesheet/adjustments/:adjustmentId", va_timesheet_manage_adjustment_request); @@ -67,7 +67,7 @@ vaRoutes.get("/search", va_filter_search); // GET Windows Tab Logged Data for VA IN Between Login and LOGOUT -vaRoutes.get("/report/project/:projectId/:from?/:to?", authValidator, va_projects_report_function); +vaRoutes.get("/report/project/:projectId/:from?/:to?/:vaId?", authValidator, va_projects_report_function); // Getting Project List assigned to VA should be ACTIVE and add total logged hours added by VA vaRoutes.get("/list/projects/:projectId?/:from?/:to?", authValidator, va_projects_log_list_function); diff --git a/src/services/vaClient.service.js b/src/services/vaClient.service.js index b6c971b..c2a518e 100644 --- a/src/services/vaClient.service.js +++ b/src/services/vaClient.service.js @@ -1,5 +1,6 @@ import logger from "../config/logger.js"; import SalesforceService from "./salesforce.service.js"; +import { VAModel, ProjectModel, LoggedModel, WorkTrackModel } from "../models/vaModel.js"; class VaClientService { constructor(vaClientRepository) { @@ -53,20 +54,143 @@ class VaClientService { } async updateClient(id, data) { - try { - const response = await this.clientRepository.update(id, data); - if (!response) throw new Error("Client not found"); - return response; + try { + // Check if client exists + const existingClient = await this.clientRepository.findById(id); + if (!existingClient) { + throw new Error("Client not found"); + } + + const { userDetails } = data; + + if (!userDetails) { + throw new Error("No user details provided for update"); + } + + // Prepare update data + const updateData = { + userDetails: {} + }; + + // Define allowed fields for update (excluding password) + const allowedFields = [ + "name", "address", "city", "state", "zip", + "billingName", "billingEmail", "billingAddress", "billingCity", + "billingState", "billingZip", "position", "signatoryName", + "signatoryEmail", "paymentMethod", "bankName", "accountNumber", + "routingNumber", "accountType", "accountHolderName", "cardNumber", + "expiryDate", "securityCode", "cardZip" + ]; + + let hasUpdates = false; + const updatedFields = []; + + // Process allowed fields + allowedFields.forEach(field => { + if (userDetails[field] !== undefined && userDetails[field] !== null && userDetails[field] !== '') { + updateData.userDetails[field] = userDetails[field]; + hasUpdates = true; + updatedFields.push(field); + } + }); + + // Handle email update separately (needs uniqueness check) + if (userDetails.email && userDetails.email !== existingClient.userDetails.email) { + // Check if new email already exists + const emailExists = await this.clientRepository.findByEmail(userDetails.email); + if (emailExists && emailExists._id.toString() !== id) { + throw new Error("Email already exists"); + } + updateData.userDetails.email = userDetails.email.toLowerCase(); + hasUpdates = true; + updatedFields.push('email'); + } + + // Check if any fields were actually updated + if (!hasUpdates) { + throw new Error("No valid fields to update"); + } + + // Update the client + const updatedClient = await this.clientRepository.update(id, updateData); + + // Notify Salesforce about the update + try { + await this.salesforceService.updateClientInSalesforce(id, data); + logger.info(`Salesforce notified about client update: ${id}`); + } catch (sfError) { + logger.warn(`Salesforce notification failed for client update: ${sfError.message}`); + // Don't throw error - client update should proceed even if Salesforce fails + } + + logger.info(`Client updated successfully: ${id}, Fields: ${updatedFields.join(', ')}`); + + return { + ...updatedClient.toObject(), + updateSummary: { + updatedFields + } + }; + } catch (error) { logger.error("Client Service - updateClient error: " + error.message); throw error; } } - async deleteClient(id) { + async deleteClient(clientId) { try { - const response = await this.clientRepository.delete(id); - return response; + // Check if client exists + const existingClient = await this.clientRepository.findById(clientId); + if (!existingClient) { + throw new Error("Client not found"); + } + + const clientName = existingClient.userDetails?.name; + const vaIds = existingClient.vaIds || []; + const projectIds = existingClient.projectIds || []; + + logger.info(`Deleting client: ${clientName} (${clientId})`); + + const deletionSummary = {}; + + // 1. Remove client from all VAs + if (vaIds.length > 0) { + const vaResult = await VAModel.updateMany( + { _id: { $in: vaIds } }, + { $pull: { clientIds: clientId } } + ); + deletionSummary.vasUpdated = vaResult.modifiedCount; + } + + // 2. Delete all projects and associated data + if (projectIds.length > 0) { + // Remove project references from VAs + await VAModel.updateMany( + { projectIds: { $in: projectIds } }, + { $pull: { projectIds: { $in: projectIds } } } + ); + + // Delete work tracks and logs + await WorkTrackModel.deleteMany({ projectId: { $in: projectIds } }); + await LoggedModel.deleteMany({ projectId: { $in: projectIds } }); + + // Delete projects + const projectResult = await ProjectModel.deleteMany({ _id: { $in: projectIds } }); + deletionSummary.projectsDeleted = projectResult.deletedCount; + } + + // 3. Delete the client + const deletedClient = await this.clientRepository.delete(clientId); + + + logger.info(`✅ Successfully deleted client: ${clientName} (${clientId})`); + + return { + ...deletedClient.toObject(), + deletionSummary + }; + } catch (error) { logger.error("Client Service - deleteClient error: " + error.message); throw error; diff --git a/views/va_register.ejs b/views/va_register.ejs index 3bf2223..34841f9 100644 --- a/views/va_register.ejs +++ b/views/va_register.ejs @@ -266,17 +266,19 @@ +
- + + -
Loading states...
+
@@ -327,7 +329,7 @@
-
+

Veem

@@ -358,6 +360,22 @@
+ +
+

Wise

+

Receive payments via Wise

+
+
+
+
+ + +
+ +
+
From 4d757daf97a1f2988d7747442d041fed6d8fb6d4 Mon Sep 17 00:00:00 2001 From: Aakash Date: Thu, 27 Nov 2025 19:58:33 +0530 Subject: [PATCH 4/7] Updated client dashboard --- public/css/dashboard.css | 10 + public/js/va-register.js | 5 +- .../dashboard/clientDashboard.controller.js | 243 +++---- src/controllers/client/vaClient.controller.js | 130 +++- .../project/projectAddSf.controller.js | 7 +- .../project/projectUpdateSf.controller.js | 28 +- .../va/vaLeaves/vaLeavesApply.controller.js | 2 +- .../vaProfile/vaProfileUpdate.controller.js | 7 +- .../va/vaProjects/vaProjectLog.controller.js | 179 ++++- src/models/vaModel.js | 22 +- src/repository/vaClient.repository.js | 46 +- src/routes/v1/client.routes.js | 8 +- src/routes/v1/va.routes.js | 2 +- src/server.js | 12 +- src/services/vaClient.service.js | 65 ++ views/client/dashboard-dashboard.ejs | 212 ++++-- views/client/dashboard-profile.ejs | 685 ++++++++++++++++++ views/client/dashboard-projects.ejs | 34 +- views/client/dashboard-virtual-assistants.ejs | 203 +++++- views/va_client_register.ejs | 8 +- views/va_register.ejs | 15 +- 21 files changed, 1633 insertions(+), 290 deletions(-) create mode 100644 views/client/dashboard-profile.ejs diff --git a/public/css/dashboard.css b/public/css/dashboard.css index d73ce47..c20f34e 100644 --- a/public/css/dashboard.css +++ b/public/css/dashboard.css @@ -826,6 +826,7 @@ tr[data-vaid]:hover { cursor: pointer; z-index: 10; position: relative; + text-decoration: none; } /* Project Details Styles in Popup */ @@ -1299,4 +1300,13 @@ tr[data-vaid]:hover { [data-theme="dark"] .status-badge.inactive { background: #4b5563; color: #d1d5db; +} + +.modal-actions { + text-align: center; + margin-top: 20px; +} + +.modal-actions a { + text-decoration: none; } \ No newline at end of file diff --git a/public/js/va-register.js b/public/js/va-register.js index ef2e9ac..7ce3f38 100644 --- a/public/js/va-register.js +++ b/public/js/va-register.js @@ -691,10 +691,11 @@ formData.userDetails.city = document.getElementById('city').value; formData.userDetails.state = document.getElementById('state').value; formData.userDetails.country = document.getElementById('country').value; + formData.status = document.getElementById('status').value; break; case 3: formData.userDetails.phone = document.getElementById('phone').value; - formData.userDetails.skype = document.getElementById('skype').value; + // formData.userDetails.skype = document.getElementById('skype').value; break; case 4: const selectedPayment = document.querySelector('.payment-option.selected'); @@ -1150,7 +1151,7 @@ function validateSkypeId(input) { if (value && !/^[a-zA-Z][a-zA-Z0-9\.,\-_]+$/.test(value)) { input.style.border = '1px solid #ef4444'; - showError('Skype ID must start with a letter and can only contain letters, numbers, dots, hyphens, and underscores', 'skype', 3); + showError('Invalid Skype ID', 'skype', 3); return false; } else { input.style.border = ''; diff --git a/src/controllers/client/dashboard/clientDashboard.controller.js b/src/controllers/client/dashboard/clientDashboard.controller.js index cfd80bf..b13f92d 100644 --- a/src/controllers/client/dashboard/clientDashboard.controller.js +++ b/src/controllers/client/dashboard/clientDashboard.controller.js @@ -68,12 +68,7 @@ async function client_dashboard(req, res) { $cond: [{ $eq: ["$loggedType", "LOGIN"] }, 1, 0] } }, - lastActivity: { $first: "$time" }, - projectsWorked: { - $addToSet: "$projectId" - }, - // Get all login logs for this VA - allLogs: { $push: "$$ROOT" } + lastActivity: { $first: "$time" } } }, { @@ -85,22 +80,12 @@ async function client_dashboard(req, res) { const activeVaIds = agg.map((a) => a._id).filter(Boolean); - // Get active VA details - const activeVAs = await VAModel.find({ - _id: { $in: activeVaIds }, - clientIds: clientID - }) - .select('userDetails.firstName userDetails.lastName userDetails.email userDetails.phone status createdAt projectIds') - .lean() - .exec(); - // Create mapping for last log and activity data const activityByVa = agg.reduce((acc, a) => { acc[String(a._id)] = { lastLog: a.lastLog, totalLogins: a.totalLogins, - lastActivity: a.lastActivity, - projectsWorked: a.projectsWorked || [] + lastActivity: a.lastActivity }; return acc; }, {}); @@ -126,14 +111,14 @@ async function client_dashboard(req, res) { logsByVa[vaId].push(log); }); - // Calculate total worked time for each VA using the same logic as va_log_work_report_callback + // Calculate total worked time for each VA for (const [vaId, logs] of Object.entries(logsByVa)) { let totalWorkSeconds = 0; // Group logs by date (YYYY-MM-DD format) const logsByDate = {}; logs.forEach(log => { - const date = new Date(log.time).toISOString().split('T')[0]; // YYYY-MM-DD + const date = new Date(log.time).toISOString().split('T')[0]; if (!logsByDate[date]) { logsByDate[date] = []; } @@ -147,7 +132,6 @@ async function client_dashboard(req, res) { let dayWorkSeconds = 0; let lastLogin = null; - let lastProjectId = null; // If day has adjustment logs, only process adjustment logs // If no adjustment logs, process all logs for that day @@ -164,14 +148,10 @@ async function client_dashboard(req, res) { if (log.loggedType === "LOGIN") { lastLogin = time; - lastProjectId = log.projectId?.toString() || null; } else if (log.loggedType === "LOGOUT" && lastLogin) { const diffSec = (time - lastLogin) / 1000; dayWorkSeconds += diffSec; - - // reset after each pair lastLogin = null; - lastProjectId = null; } } @@ -184,25 +164,93 @@ async function client_dashboard(req, res) { }; } + // NEW: Calculate project-wise work time for each VA + const projectWorkTimeByVa = {}; + + for (const [vaId, logs] of Object.entries(logsByVa)) { + const projectWorkTime = {}; + + // Group logs by project + const logsByProject = {}; + logs.forEach(log => { + const projectId = log.projectId?.toString(); + if (projectId) { + if (!logsByProject[projectId]) { + logsByProject[projectId] = []; + } + logsByProject[projectId].push(log); + } + }); + + // Calculate work time per project + for (const [projectId, projectLogs] of Object.entries(logsByProject)) { + let projectWorkSeconds = 0; + + // Group logs by date + const logsByDate = {}; + projectLogs.forEach(log => { + const date = new Date(log.time).toISOString().split('T')[0]; + if (!logsByDate[date]) { + logsByDate[date] = []; + } + logsByDate[date].push(log); + }); + + // Process each day separately + for (const [date, dayLogs] of Object.entries(logsByDate)) { + const hasAdjustmentLogs = dayLogs.some(log => log.isAdjustment === true); + let dayWorkSeconds = 0; + let lastLogin = null; + + const logsToProcess = hasAdjustmentLogs + ? dayLogs.filter(log => log.isAdjustment === true) + : dayLogs; + + logsToProcess.sort((a, b) => new Date(a.time) - new Date(b.time)); + + for (const log of logsToProcess) { + const time = new Date(log.time); + + if (log.loggedType === "LOGIN") { + lastLogin = time; + } else if (log.loggedType === "LOGOUT" && lastLogin) { + const diffSec = (time - lastLogin) / 1000; + dayWorkSeconds += diffSec; + lastLogin = null; + } + } + + projectWorkSeconds += dayWorkSeconds; + } + + projectWorkTime[projectId] = { + totalSeconds: projectWorkSeconds, + formatted: formatTime(projectWorkSeconds), + projectName: projectMap[projectId]?.name || 'Unknown Project' + }; + } + + projectWorkTimeByVa[vaId] = projectWorkTime; + } + // Calculate session duration for currently active VAs const currentTime = new Date(); - // Format all assigned VAs + // Format all assigned VAs - Only include data needed for frontend const allVAsFormatted = allAssignedVAs.map((v) => { const activity = activityByVa[String(v._id)] || {}; const workedTime = totalWorkedTimeByVa[String(v._id)] || { totalSeconds: 0, formatted: '00:00:00' }; const lastLog = activity.lastLog; + const projectWorkTime = projectWorkTimeByVa[String(v._id)] || {}; // Calculate current session duration if still logged in let currentSessionDuration = null; let isCurrentlyActive = false; - let currentSessionFormatted = null; let currentProjectDetails = null; if (lastLog && lastLog.loggedType === "LOGIN") { const loginTime = new Date(lastLog.time); currentSessionDuration = Math.floor((currentTime - loginTime) / 1000); - currentSessionFormatted = formatTime(currentSessionDuration); isCurrentlyActive = true; // Consider VA inactive if last login was more than 24 hours ago @@ -223,6 +271,29 @@ async function client_dashboard(req, res) { // Combine first and last name const fullName = `${v.userDetails?.firstName || ''} ${v.userDetails?.lastName || ''}`.trim() || 'Unknown'; + // Filter: Only include projects that belong to this client + const clientProjectsAssigned = (v.projectIds || []).filter(projectId => + clientProjectIds.some(clientProjectId => + clientProjectId.toString() === projectId.toString() + ) + ); + + // Format project work time for popup + const projectsWithWorkTime = clientProjectsAssigned.map(projectId => { + const projectIdStr = projectId.toString(); + const workData = projectWorkTime[projectIdStr] || { totalSeconds: 0, formatted: '00:00:00' }; + const project = projectMap[projectIdStr]; + + return { + id: projectId, + name: project?.name || 'Unknown Project', + totalWorkedTime: workData.formatted, + totalWorkedSeconds: workData.totalSeconds, + isCurrentlyWorking: currentProjectDetails?.id?.toString() === projectIdStr, + hourlyRate: project?.hourlyRate || 0 + }; + }); + return { id: v._id, name: fullName, @@ -231,99 +302,18 @@ async function client_dashboard(req, res) { status: v.status || 'active', isCurrentlyActive: isCurrentlyActive, lastLoginTime: lastLog?.time || null, - lastActivity: activity.lastActivity, currentSessionDuration: currentSessionDuration, - currentSessionFormatted: currentSessionFormatted, totalWorkedTime: workedTime.formatted, totalWorkedSeconds: workedTime.totalSeconds, - totalLogins: activity.totalLogins || 0, lastProject: currentProjectDetails, lastProjectId: lastLog?.projectId || null, - projectsWorked: activity.projectsWorked || [], - projectsAssigned: v.projectIds?.length || 0, + projectsAssigned: clientProjectsAssigned.length, + // Detailed project information for popup + projects: projectsWithWorkTime, vaSince: v.createdAt }; }); - // Format active VAs (currently working) - const activeVAsFormatted = activeVAs.map((v) => { - const activity = activityByVa[String(v._id)] || {}; - const workedTime = totalWorkedTimeByVa[String(v._id)] || { totalSeconds: 0, formatted: '00:00:00' }; - const lastLog = activity.lastLog; - - let currentSessionDuration = null; - let currentSessionFormatted = null; - let currentProjectDetails = null; - - if (lastLog && lastLog.loggedType === "LOGIN") { - const loginTime = new Date(lastLog.time); - currentSessionDuration = Math.floor((currentTime - loginTime) / 1000); - currentSessionFormatted = formatTime(currentSessionDuration); - - // Get current project details - if (lastLog.projectId && projectMap[lastLog.projectId.toString()]) { - currentProjectDetails = { - id: lastLog.projectId, - name: projectMap[lastLog.projectId.toString()].name - }; - } - } - - // Combine first and last name - const fullName = `${v.userDetails?.firstName || ''} ${v.userDetails?.lastName || ''}`.trim() || 'Unknown'; - - return { - id: v._id, - name: fullName, - email: v.userDetails?.email, - phone: v.userDetails?.phone, - status: v.status || 'active', - lastLoginTime: lastLog?.time || null, - currentSessionDuration: currentSessionDuration, - currentSessionFormatted: currentSessionFormatted, - totalWorkedTime: workedTime.formatted, - totalWorkedSeconds: workedTime.totalSeconds, - totalLogins: activity.totalLogins || 0, - currentProject: currentProjectDetails, - currentProjectId: lastLog?.projectId || null, - projectsAssigned: v.projectIds?.length || 0, - vaSince: v.createdAt - }; - }); - - // Format projects with additional data - const projectsFormatted = clientProjects.map((p) => { - // Find VAs currently working on this project - const activeVAsOnProject = allVAsFormatted.filter(va => - va.isCurrentlyActive && va.lastProjectId && - va.lastProjectId.toString() === p._id.toString() - ); - - // Find all VAs assigned to this project - const assignedVAs = allAssignedVAs.filter(va => - va.projectIds && va.projectIds.some(projectId => - projectId.toString() === p._id.toString() - ) - ); - - return { - id: p._id, - name: p.name, - hourlyRate: p.hourlyRate, - totalLoggedHours: p.totalLoggedHours || 0, - dedicatedHours: p.dedicatedHours || 0, - vaAssigned: assignedVAs.length, - vaCurrentlyWorking: activeVAsOnProject.length, - activeVAs: activeVAsOnProject.map(va => ({ - id: va.id, - name: va.name, - currentSession: va.currentSessionFormatted, - lastLoginTime: va.lastLoginTime - })), - createdAt: p.createdAt - }; - }); - // Calculate dashboard statistics const totalHoursLogged = clientProjects.reduce((sum, p) => sum + (p.totalLoggedHours || 0), 0); const totalDedicatedHours = clientProjects.reduce((sum, p) => sum + (p.dedicatedHours || 0), 0); @@ -334,45 +324,22 @@ async function client_dashboard(req, res) { // Calculate total worked time across all VAs const totalWorkedTimeAllVAs = allVAsFormatted.reduce((total, va) => total + (va.totalWorkedSeconds || 0), 0); - // Calculate projects with active workers - const projectsWithActiveWorkers = projectsFormatted.filter(p => p.vaCurrentlyWorking > 0).length; - return res.status(200).json({ success: true, data: { - // All assigned VAs with their status - allVAs: { - active: currentlyActiveVAs, - inactive: inactiveVAs, - total: allVAsFormatted - }, - // Currently working VAs (active session) - activeVAs: activeVAsFormatted, - // All client projects - projects: projectsFormatted + // Only send data that frontend actually uses + allVAs: allVAsFormatted }, summary: { totalVAs: allAssignedVAs.length, activeVAs: currentlyActiveVAs.length, inactiveVAs: inactiveVAs.length, totalProjects: clientProjects.length, - projectsWithActiveWorkers: projectsWithActiveWorkers, totalHoursLogged: totalHoursLogged, totalDedicatedHours: totalDedicatedHours, totalWorkedTime: formatTime(totalWorkedTimeAllVAs), utilizationRate: totalDedicatedHours > 0 ? - ((totalHoursLogged / totalDedicatedHours) * 100).toFixed(2) + '%' : '0%', - lastUpdated: currentTime.toISOString() - }, - counts: { - vas: { - total: allAssignedVAs.length, - active: currentlyActiveVAs.length, - inactive: inactiveVAs.length - }, - projects: clientProjects.length, - activeSessions: activeVAsFormatted.length, - projectsWithActivity: projectsWithActiveWorkers + ((totalHoursLogged / totalDedicatedHours) * 100).toFixed(2) + '%' : '0%' } }); @@ -386,7 +353,7 @@ async function client_dashboard(req, res) { } } -// Helper function to format time (same as in va_log_work_report_callback) +// Helper function to format time function formatTime(seconds) { const h = Math.floor(seconds / 3600); const m = Math.floor((seconds % 3600) / 60); diff --git a/src/controllers/client/vaClient.controller.js b/src/controllers/client/vaClient.controller.js index 8f23662..1547e84 100644 --- a/src/controllers/client/vaClient.controller.js +++ b/src/controllers/client/vaClient.controller.js @@ -1,6 +1,7 @@ import VaClientRepository from "../../repository/vaClient.repository.js"; import VaClientService from "../../services/vaClient.service.js"; import logger from "../../config/logger.js"; +import jwt from "jsonwebtoken"; const vaClientService = new VaClientService(new VaClientRepository()); @@ -51,9 +52,9 @@ async function va_register_client(req, res) { } // Additional controller methods can be added here -async function get_client_by_email(req, res) { +async function get_client_profile(req, res) { try { - const { email } = req.params; + const email = req.body.clientEmail; const client = await vaClientService.getClientByEmail(email); if (!client) { @@ -179,9 +180,130 @@ async function update_client(req, res) { } } +async function get_client_dashboard_profile(req, res) { + try { + const accessToken = req.headers.token; + + if (!accessToken) { + return res.status(401).json({ success: false, message: "Missing token" }); + } + + const decoded = jwt.verify(accessToken, process.env.JWT_SECRET_ACCESS_TOKEN); + + if (!decoded || !decoded.email) { + return res.status(401).json({ success: false, message: "Invalid token" }); + } + + const clientEmail = decoded.email; + + // Use the new method that returns only necessary fields + const client = await vaClientService.getClientProfileByEmail(clientEmail); + + if (!client) { + return res.status(404).json({ + success: false, + message: "Client not found" + }); + } + + // Format the response to only include userDetails + const profileData = { + name: client.userDetails?.name || '', + email: client.userDetails?.email || '', + address: client.userDetails?.address || '', + city: client.userDetails?.city || '', + state: client.userDetails?.state || '', + zip: client.userDetails?.zip || '', + position: client.userDetails?.position || '' + }; + + return res.status(200).json({ + success: true, + data: profileData + }); + } catch (error) { + logger.error(`ClientController - Get dashboard profile error: ${error.message}`); + return res.status(500).json({ + success: false, + message: "Internal Server Error" + }); + } +} + +async function update_client_password(req, res) { + logger.info("ClientController - Update Client Password Hit ..."); + console.log("Update Client Password - Request Body:", req.body); + try { + const accessToken = req.headers.token; + + if (!accessToken) { + return res.status(401).json({ success: false, message: "Missing token" }); + } + + const decoded = jwt.verify(accessToken, process.env.JWT_SECRET_ACCESS_TOKEN); + + if (!decoded || !decoded.email) { + return res.status(401).json({ success: false, message: "Invalid token" }); + } + + const clientEmail = decoded.email; + const payload = req.body || {}; + const { userDetails } = payload; + + // Get client by email to get the ID + const client = await vaClientService.getClientByEmail(clientEmail); + + if (!client) { + return res.status(404).json({ + success: false, + message: "Client not found" + }); + } + + const clientId = client._id; + + // Call service to update password + const result = await vaClientService.updateClientPassword(clientId, userDetails); + + logger.info(`ClientController - Client password updated successfully: ${clientEmail}`); + + return res.status(200).json({ + success: true, + message: "Password updated successfully", + data: result + }); + + } catch (error) { + logger.error(`ClientController - Update password error: ${error.message}`); + + if (error.message === "Client not found") { + return res.status(404).json({ + success: false, + message: error.message + }); + } + + if (error.message.includes("Current password is incorrect") || + error.message.includes("New passwords do not match") || + error.message.includes("All password fields are required")) { + return res.status(400).json({ + success: false, + message: error.message + }); + } + + return res.status(500).json({ + success: false, + message: "Internal Server Error" + }); + } +} + export { va_register_client, - get_client_by_email, + get_client_profile, delete_client, - update_client + update_client, + get_client_dashboard_profile, + update_client_password }; \ No newline at end of file diff --git a/src/controllers/project/projectAddSf.controller.js b/src/controllers/project/projectAddSf.controller.js index cabe634..ccad5d0 100644 --- a/src/controllers/project/projectAddSf.controller.js +++ b/src/controllers/project/projectAddSf.controller.js @@ -5,7 +5,7 @@ import { updateEntityRelationships } from "../../helper/updateEntityRelationship async function project_add_sf(req, res) { try { const payload = req.body|| {}; - const { name, hourlyRate, clientId, vaAssigned, dedicatedHours } = payload; + const { name, hourlyRate, clientId, vaAssigned, dedicatedHours, vaWorkingScheduleStart, vaWorkingScheduleEnd, isFlexibleAllowedPt, vaWorkingDelayAllowedStart, vaWorkingDelayAllowedEnd } = payload; // Validate required fields if (!name) { @@ -65,6 +65,11 @@ async function project_add_sf(req, res) { clientId: clientObjectId, vaAssigned: vaAssignedArray, dedicatedHours: dedicatedHours ? parseFloat(dedicatedHours) : 0, + vaWorkingScheduleStart: vaWorkingScheduleStart || null, + vaWorkingScheduleEnd: vaWorkingScheduleEnd || null, + isFlexibleAllowedPt: isFlexibleAllowedPt || true, + vaWorkingDelayAllowedStart: vaWorkingDelayAllowedStart || null, + vaWorkingDelayAllowedEnd: vaWorkingDelayAllowedEnd || null }; const created = await ProjectModel.create(toCreate); diff --git a/src/controllers/project/projectUpdateSf.controller.js b/src/controllers/project/projectUpdateSf.controller.js index 5035f29..62e3047 100644 --- a/src/controllers/project/projectUpdateSf.controller.js +++ b/src/controllers/project/projectUpdateSf.controller.js @@ -16,7 +16,12 @@ async function update_project_sf(req, res) { vaAssigned, dedicatedHours, status, - totalLoggedHours + totalLoggedHours , + vaWorkingScheduleStart, + vaWorkingScheduleEnd, + isFlexibleAllowedPt, + vaWorkingDelayAllowedStart, + vaWorkingDelayAllowedEnd } = payload; // Validate projectId @@ -88,6 +93,27 @@ async function update_project_sf(req, res) { updateData.status = status; } + // Update VA working schedule if provided + if (vaWorkingScheduleStart !== undefined) { + updateData.vaWorkingScheduleStart = vaWorkingScheduleStart || null; + } + + if (vaWorkingScheduleEnd !== undefined) { + updateData.vaWorkingScheduleEnd = vaWorkingScheduleEnd || null; + } + + if (isFlexibleAllowedPt !== undefined) { + updateData.isFlexibleAllowedPt = isFlexibleAllowedPt; + } + + if (vaWorkingDelayAllowedStart !== undefined) { + updateData.vaWorkingDelayAllowedStart = vaWorkingDelayAllowedStart || null; + } + + if (vaWorkingDelayAllowedEnd !== undefined) { + updateData.vaWorkingDelayAllowedEnd = vaWorkingDelayAllowedEnd || null; + } + // Handle VA assignments - this requires relationship updates if (vaAssigned !== undefined) { let newVaAssignedArray = []; diff --git a/src/controllers/va/vaLeaves/vaLeavesApply.controller.js b/src/controllers/va/vaLeaves/vaLeavesApply.controller.js index 5076441..35ba56d 100644 --- a/src/controllers/va/vaLeaves/vaLeavesApply.controller.js +++ b/src/controllers/va/vaLeaves/vaLeavesApply.controller.js @@ -191,7 +191,7 @@ async function va_leave_apply_function(req, res) { totalDays: totalDays, leaveType, comment: processedComment.processedHTML, - attachments: processedComment.extractedImages, + // attachments: processedComment.extractedImages, status: "PENDING" }, // SyncSalesforce: result diff --git a/src/controllers/va/vaProfile/vaProfileUpdate.controller.js b/src/controllers/va/vaProfile/vaProfileUpdate.controller.js index 0e8082f..780a7d8 100644 --- a/src/controllers/va/vaProfile/vaProfileUpdate.controller.js +++ b/src/controllers/va/vaProfile/vaProfileUpdate.controller.js @@ -61,7 +61,7 @@ async function va_profile_update_callback(req, res) { const payload = req.body || {}; - const { userDetails } = payload; + const { userDetails, status } = payload; if (!userDetails) { return res.status(400).json({ success: false, message: "Missing userDetails" }); @@ -136,6 +136,11 @@ async function va_profile_update_callback(req, res) { }); } + if (status) { + vaUser.status = status; + updatedFields.push('status'); + } + await vaUser.save(); const updatedUser = await VAModel.findById(vaUser._id) diff --git a/src/controllers/va/vaProjects/vaProjectLog.controller.js b/src/controllers/va/vaProjects/vaProjectLog.controller.js index 07ca354..a4ab951 100644 --- a/src/controllers/va/vaProjects/vaProjectLog.controller.js +++ b/src/controllers/va/vaProjects/vaProjectLog.controller.js @@ -1,4 +1,4 @@ -import { LoggedModel, WorkTrackModel, ProjectModel } from "../../../models/vaModel.js"; +import { LoggedModel, WorkTrackModel, ProjectModel, VAModel } from "../../../models/vaModel.js"; // Helper function to get start and end of day in UTC function getDayRange(date = new Date()) { @@ -47,12 +47,6 @@ function parseISOTimeString(timeString) { } } -function setMillisecondsToZero(date) { - const newDate = new Date(date); - newDate.setMilliseconds(0); - return newDate; -} - // Helper function to check if time difference is less than threshold function isTimeWithinThreshold(receivedTime, currentTime, thresholdSeconds = 10) { const diffMs = Math.abs(currentTime - receivedTime); @@ -68,6 +62,89 @@ function getCurrentUTCTime() { return now; } +// function parseTimeToMinutes(timeString) { +// if (!timeString) return null; + +// const [hours, minutes] = timeString.split(':').map(Number); +// return hours * 60 + (minutes || 0); +// } + +// function parseDelayToMinutes(delayString) { +// if (!delayString) return 0; + +// if (delayString.includes('h')) { +// const hours = parseInt(delayString.replace('h', '').trim()); +// return hours * 60; +// } else if (delayString.includes('m')) { +// return parseInt(delayString.replace('m', '').trim()); +// } + +// return parseInt(delayString) || 0; // Default to minutes if no unit specified +// } + +// function checkScheduleCompliance(currentTime, project, vaStatus, type) { +// const warnings = []; + +// // If project doesn't have schedule or VA is PARTTIME with flexible allowed, no schedule restrictions +// if (!project.vaWorkingScheduleStart || !project.vaWorkingScheduleEnd) { +// return { isCompliant: true, warnings }; +// } + +// const currentDate = new Date(currentTime); +// const currentMinutes = currentDate.getUTCHours() * 60 + currentDate.getUTCMinutes(); + +// const scheduleStartMinutes = parseTimeToMinutes(project.vaWorkingScheduleStart); +// const scheduleEndMinutes = parseTimeToMinutes(project.vaWorkingScheduleEnd); +// const allowedStartDelay = parseDelayToMinutes(project.vaWorkingDelayAllowedStart); +// const allowedEndDelay = parseDelayToMinutes(project.vaWorkingDelayAllowedEnd); + +// // Calculate allowed time ranges +// const allowedStartMinutes = scheduleStartMinutes - allowedStartDelay; +// const allowedEndMinutes = scheduleEndMinutes + allowedEndDelay; + +// console.log(`Schedule Check - Current: ${currentMinutes}, Start: ${scheduleStartMinutes}, End: ${scheduleEndMinutes}, Allowed Start: ${allowedStartMinutes}, Allowed End: ${allowedEndMinutes}`); + +// // Check for PARTTIME with flexible schedule +// if (vaStatus === "PARTTIME" && project.isFlexibleAllowedPt) { +// return { isCompliant: true, warnings }; +// } + +// // For all other cases, check schedule compliance +// if (type === "LOGIN") { +// // Check if login is too early +// if (currentMinutes < allowedStartMinutes) { +// warnings.push("early_login"); +// } + +// // Check if login is after shift end +// if (currentMinutes > scheduleEndMinutes) { +// warnings.push("working_after_shift_end"); +// } + +// // Check if login is within allowed range +// // const isWithinAllowedRange = currentMinutes >= allowedStartMinutes && currentMinutes <= allowedEndMinutes; + +// return { +// isCompliant: true, // Allow login regardless of time +// warnings +// }; + +// } else if (type === "LOGOUT") { +// // Check if logout is too late +// if (currentMinutes > allowedEndMinutes) { +// warnings.push("late_logout"); +// } + +// // For logout, we're generally more permissive - allow logout even if late +// return { +// isCompliant: true, // Allow logout regardless of time +// warnings +// }; +// } + +// return { isCompliant: true, warnings }; +// } + // function to close open work tracks and create logout async function closePreviousSession(vaId, lastLog, logTime = new Date()) { try { @@ -164,7 +241,7 @@ async function va_project_log_callback(req, res) { try { const payload = req.body || {}; const { vaId, vaEmail, projectId, type, time: receivedTimeString } = payload; - console.log("Received payload:", payload); + // console.log("Received payload:", payload); if (!vaId || !vaEmail || !projectId || !type) { return res.status(400).send("Missing required properties"); @@ -189,8 +266,6 @@ async function va_project_log_callback(req, res) { console.log(`Received: ${receivedTimeString} (parsed: ${receivedTime.toISOString()})`); console.log(`Current: ${currentTime.toISOString()}`); } - } else { - console.log(`ℹ️ No time received, using current UTC time: ${currentTime.toISOString()}`); } const project = await ProjectModel.findById(projectId); @@ -198,6 +273,22 @@ async function va_project_log_callback(req, res) { return res.status(400).json({ success: false, message: "Project not found" }); } + const vaUser = await VAModel.findById(vaId); + if (!vaUser) { + return res.status(400).json({ success: false, message: "VA not found" }); + } + + // Check schedule compliance + // const scheduleCheck = checkScheduleCompliance(logTime, project, vaUser.status, type); + // console.log(`Schedule compliance check:`, scheduleCheck); + // if (!scheduleCheck.isCompliant) { + // return res.status(400).json({ + // success: false, + // message: `Cannot ${type.toLowerCase()} - outside of allowed schedule time`, + // warnings: scheduleCheck.warnings + // }); + // } + // For LOGIN requests, check daily hours (using UTC day range) if (type === "LOGIN") { const { start, end } = getDayRange(); // UTC day range @@ -218,7 +309,8 @@ async function va_project_log_callback(req, res) { if (project.dedicatedHours > 0 && totalTodayHours >= project.dedicatedHours) { return res.status(400).json({ success: false, - message: `Cannot login - daily hours already completed (${formatHoursToReadable(totalTodayHours)} / ${formatHoursToReadable(project.dedicatedHours)})` + message: `Cannot login - daily hours already completed (${formatHoursToReadable(totalTodayHours)} / ${formatHoursToReadable(project.dedicatedHours)})`, + // warnings: scheduleCheck.warnings }); } @@ -245,17 +337,57 @@ async function va_project_log_callback(req, res) { time: logTime }); - return res.status(201).send({ - success: true, - message: `Login Successful (previous session closed)`, - logId: createdLog._id, - previousSession: { - closedWorkTracks: closeResult.closedWorkTracks, - logoutLogId: closeResult.logoutLogId - }, - timeUsed: receivedTimeString ? 'received' : 'current', - actualTime: createdLog.time - }); + try { + const salesforceUrl = `${process.env.SF_DOCTRACKER_API_URL}services/apexrest/docva/api/va-project-log`; + const sfData = { + "_id": createdLog._id, + "loggedType": "LOGIN", + "vaId": vaId, + "vaEmail": vaEmail, + "projectId": projectId, + "time": createdLog.time, + } + + 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) { + const errorText = await response.text(); + throw new Error(`Salesforce API responded with status ${response.status}: ${errorText}`); + } + + const result = await response.json(); + // console.log("✅ Successfully synced VA data to Salesforce:", result); + + res.status(201).json({ + success: true, + message: `${type} Successfully`, + logId: createdLog._id, + SyncSalesforce: result, + previousSession: { + closedWorkTracks: closeResult.closedWorkTracks, + logoutLogId: closeResult.logoutLogId + }, + timeUsed: receivedTimeString ? 'received' : 'current', + actualTime: createdLog.time, + // warnings: scheduleCheck.warnings + }); + } catch (error) { + console.error("Failed to sync with Salesforce:", error.message); + console.log(error) + + // Create a custom error to distinguish Salesforce errors + const salesforceError = new Error(`Salesforce sync failed: ${error.message}`); + salesforceError.isSalesforceError = true; + throw salesforceError; + } } } @@ -302,7 +434,8 @@ async function va_project_log_callback(req, res) { logId: createdLog._id, SyncSalesforce: result, timeUsed: receivedTimeString ? 'received' : 'current', - actualTime: createdLog.time + actualTime: createdLog.time, + // warnings: scheduleCheck.warnings }); } catch (error) { console.error("Failed to sync with Salesforce:", error.message); diff --git a/src/models/vaModel.js b/src/models/vaModel.js index ee1ef66..e6794e1 100644 --- a/src/models/vaModel.js +++ b/src/models/vaModel.js @@ -101,7 +101,27 @@ const projectSchema = new Schema({ type: String, enum: ["ACTIVE", "INACTIVE"], default: "ACTIVE" - } + }, + vaWorkingScheduleStart: { + type: String, + default: null + }, + vaWorkingScheduleEnd: { + type: String, + default: null + }, + isFlexibleAllowedPt: { + type: Boolean, + default: true + }, + vaWorkingDelayAllowedStart: { + type: String, + default: '5m' + }, + vaWorkingDelayAllowedEnd: { + type: String, + default: '5m' + }, }); /* ---------------------- Client Details Schema ---------------------- */ diff --git a/src/repository/vaClient.repository.js b/src/repository/vaClient.repository.js index 15aea74..15a170f 100644 --- a/src/repository/vaClient.repository.js +++ b/src/repository/vaClient.repository.js @@ -13,7 +13,7 @@ class VaClientRepository { async findByEmail(email) { try { - const response = await ClientModel.findOne({ "userDetails.email": email.toLowerCase() }); + const response = await ClientModel.findOne({ "userDetails.email": email }); return response; } catch (error) { throw error; @@ -59,6 +59,50 @@ class VaClientRepository { } } + async findProfileByEmail(email) { + try { + const response = await ClientModel.findOne( + { "userDetails.email": email }, + { + // Only specify the fields you want to include + "userDetails.name": 1, + "userDetails.email": 1, + "userDetails.address": 1, + "userDetails.city": 1, + "userDetails.state": 1, + "userDetails.zip": 1, + "userDetails.position": 1 + // Don't include any exclusion fields (0) when using inclusion (1) + } + ).lean(); // Use lean() for better performance since we don't need mongoose document + + return response; + } catch (error) { + throw error; + } + } + + // Alternative: If you need to be more explicit about excluding sensitive fields + async findProfileByEmailExplicit(email) { + try { + const response = await ClientModel.findOne( + { "userDetails.email": email } + ).select({ + 'userDetails.name': 1, + 'userDetails.email': 1, + 'userDetails.address': 1, + 'userDetails.city': 1, + 'userDetails.state': 1, + 'userDetails.zip': 1, + 'userDetails.position': 1 + }).lean(); + + return response; + } catch (error) { + throw error; + } + } + async delete(id, options = {}) { try { const response = await ClientModel.findByIdAndDelete(id, options); diff --git a/src/routes/v1/client.routes.js b/src/routes/v1/client.routes.js index cad13c1..9e2c6e6 100644 --- a/src/routes/v1/client.routes.js +++ b/src/routes/v1/client.routes.js @@ -1,7 +1,7 @@ import express from "express"; const clientRoutes = express.Router(); -import { va_register_client, delete_client, update_client } from "../../controllers/client/vaClient.controller.js"; +import { va_register_client, delete_client, update_client, get_client_dashboard_profile, update_client_password } from "../../controllers/client/vaClient.controller.js"; // import { va_clients_callback } from "../../controllers/va/vaClients/vaClients.controller.js"; import { va_clients_projects_callback } from "../../controllers/va/vaClients/vaClientProject.controller.js"; import { va_login_client } from "../../controllers/client/vaClientLogin.controller.js"; @@ -21,8 +21,6 @@ clientRoutes.get("/refreshToken", client_refresh_token_callback); // Client Refr clientRoutes.post("/forgot-password", client_request_password_reset); clientRoutes.post("/verify-client-email", client_verify_email); clientRoutes.post("/logout", clientAuthValidator, client_logout_callback); -clientRoutes.patch("/:clientId", update_client); -clientRoutes.delete("/:clientId", delete_client); // 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) @@ -33,6 +31,10 @@ clientRoutes.get("/:clientId/projects/:projectId", va_clients_projects_callback) clientRoutes.get("/dashboard", clientAuthValidator, client_dashboard); // VA Clients Projects Callback clientRoutes.post("/dashboard/va-report", clientAuthValidator, client_dashboard_va_log); // VA details clientRoutes.get("/dashboard-projects", clientAuthValidator, client_dashboard_projects); // VA Clients Projects Callback +clientRoutes.get("/profile", clientAuthValidator, get_client_dashboard_profile); +clientRoutes.patch("/profile", clientAuthValidator, update_client_password); +clientRoutes.patch("/:clientId", update_client); +clientRoutes.delete("/:clientId", delete_client); diff --git a/src/routes/v1/va.routes.js b/src/routes/v1/va.routes.js index 98cc2b9..c80794d 100644 --- a/src/routes/v1/va.routes.js +++ b/src/routes/v1/va.routes.js @@ -56,7 +56,7 @@ vaRoutes.get("/leaves/all", authValidator, va_leave_list_function); // Get all l vaRoutes.get("/leaves/status/:status", authValidator, va_leave_list_function); // Leaves Filter by status 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); // Timesheet Adjustment for VA +vaRoutes.post("/leave/request", authValidator, va_leave_apply_function); // Leave for VA vaRoutes.patch("/leave/:leaveId", va_leave_manage_function); // timesheet adjustment routes diff --git a/src/server.js b/src/server.js index 5e99b37..0a295d7 100644 --- a/src/server.js +++ b/src/server.js @@ -154,7 +154,17 @@ app.get('/client/projects', async (req, res) => { res.render('client/dashboard-projects'); } catch (error) { console.error('Projects error:', error); - res.status(500).render('client/dashboard-projects', { error: 'Failed to load virtual assistants' }); + res.status(500).render('client/dashboard-projects', { error: 'Failed to load projects' }); + } +}); + +app.get('/client/profile', async (req, res) => { + try { + // const vaData = await virtualAssistantService.getClientVAs(req.user.id); + res.render('client/dashboard-profile'); + } catch (error) { + console.error('Projects error:', error); + res.status(500).render('client/dashboard-profile', { error: 'Failed to load profile' }); } }); diff --git a/src/services/vaClient.service.js b/src/services/vaClient.service.js index c2a518e..ea5e297 100644 --- a/src/services/vaClient.service.js +++ b/src/services/vaClient.service.js @@ -1,6 +1,7 @@ import logger from "../config/logger.js"; import SalesforceService from "./salesforce.service.js"; import { VAModel, ProjectModel, LoggedModel, WorkTrackModel } from "../models/vaModel.js"; +import bcrypt from "bcrypt"; class VaClientService { constructor(vaClientRepository) { @@ -197,6 +198,70 @@ class VaClientService { } } + async getClientProfileByEmail(email) { + try { + const response = await this.clientRepository.findProfileByEmail(email); + return response; + } catch (error) { + logger.error("Client Service - getClientProfileByEmail error: " + error.message); + throw error; + } + } + + async updateClientPassword(id, passwordData) { + try { + // Check if client exists + const existingClient = await this.clientRepository.findById(id); + if (!existingClient) { + throw new Error("Client not found"); + } + + const { currentPassword, newPassword, confirmPassword } = passwordData; + + // Validate required fields + if (!currentPassword || !newPassword || !confirmPassword) { + throw new Error("All password fields are required"); + } + + // Validate password match + if (newPassword !== confirmPassword) { + throw new Error("New passwords do not match"); + } + + // Verify current password + const isMatch = await bcrypt.compare(currentPassword, existingClient.userDetails.password); + if (!isMatch) { + throw new Error("Current password is incorrect"); + } + + // Hash new password + const saltRounds = 10; + const hashedPassword = await bcrypt.hash(newPassword, saltRounds); + + // Prepare update data + const updateData = { + userDetails: { + password: hashedPassword + } + }; + + // Update the client password + const updatedClient = await this.clientRepository.update(id, updateData); + + logger.info(`Client password updated successfully: ${id}`); + + return { + success: true, + message: "Password updated successfully", + clientId: id + }; + + } catch (error) { + logger.error("Client Service - updateClientPassword error: " + error.message); + throw error; + } + } + // Helper method to sanitize data sanitizeClientData(payload) { const { userDetails } = payload; diff --git a/views/client/dashboard-dashboard.ejs b/views/client/dashboard-dashboard.ejs index 10eb82e..a845ea3 100644 --- a/views/client/dashboard-dashboard.ejs +++ b/views/client/dashboard-dashboard.ejs @@ -34,16 +34,6 @@ Projects - +
+ + + diff --git a/views/client/dashboard-profile.ejs b/views/client/dashboard-profile.ejs new file mode 100644 index 0000000..cc2ea96 --- /dev/null +++ b/views/client/dashboard-profile.ejs @@ -0,0 +1,685 @@ + + + + + + DocVA Client | Profile + + + + + +
+ +
+ +
+
+
+
+

Client Profile

+
+ +
+ +
+
+ +

Personal Information

+
+
+
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+
+ +

Address Information

+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+
+ +
+
+
+
+
+
+
+ + + +
+ + + + \ No newline at end of file diff --git a/views/client/dashboard-projects.ejs b/views/client/dashboard-projects.ejs index 4a58a1e..1bbf461 100644 --- a/views/client/dashboard-projects.ejs +++ b/views/client/dashboard-projects.ejs @@ -78,18 +78,12 @@ Projects - - + + + + diff --git a/views/va_client_register.ejs b/views/va_client_register.ejs index 2cf6120..e9ad8c6 100644 --- a/views/va_client_register.ejs +++ b/views/va_client_register.ejs @@ -144,7 +144,7 @@
@@ -157,7 +157,7 @@
@@ -203,7 +203,7 @@
@@ -353,7 +353,7 @@
diff --git a/views/va_register.ejs b/views/va_register.ejs index 34841f9..7964692 100644 --- a/views/va_register.ejs +++ b/views/va_register.ejs @@ -281,6 +281,15 @@
+
+
+ + +
+
@@ -300,19 +309,19 @@
- +
-
+
From 870d81e2ca0df7007e9132c9a155758708238ea2 Mon Sep 17 00:00:00 2001 From: Aakash Date: Fri, 28 Nov 2025 12:51:08 +0530 Subject: [PATCH 5/7] Updated dashboard, Added payroll API --- public/css/dashboard.css | 333 ++++++++++++++++++ .../va/vaPayroll/vaPaySlip.controller.js | 12 + .../vaPayroll/vaPayrollSchedule.controller.js | 37 ++ src/routes/v1/va.routes.js | 13 +- views/client/dashboard-dashboard.ejs | 68 +++- views/client/dashboard-profile.ejs | 330 +++-------------- views/client/dashboard-projects.ejs | 67 +++- views/client/dashboard-virtual-assistants.ejs | 68 +++- 8 files changed, 617 insertions(+), 311 deletions(-) create mode 100644 src/controllers/va/vaPayroll/vaPaySlip.controller.js create mode 100644 src/controllers/va/vaPayroll/vaPayrollSchedule.controller.js diff --git a/public/css/dashboard.css b/public/css/dashboard.css index c20f34e..15b6b02 100644 --- a/public/css/dashboard.css +++ b/public/css/dashboard.css @@ -1309,4 +1309,337 @@ tr[data-vaid]:hover { .modal-actions a { text-decoration: none; +} + +.profile-container { + max-width: 1200px; + margin: 0 auto; + padding: 20px; +} + +.profile-header { + margin-bottom: 30px; + padding-bottom: 20px; + border-bottom: 1px solid #e1e5e9; +} + +.profile-title { + font-size: 2rem; + font-weight: 600; + color: #2d3748; + margin: 0; +} + +.profile-sections { + display: grid; + gap: 30px; +} + +.profile-section { + background: white; + border-radius: 12px; + padding: 30px; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); + border: 1px solid #e1e5e9; +} + +.section-header { + display: flex; + align-items: center; + gap: 12px; + margin-bottom: 25px; + padding-bottom: 15px; + border-bottom: 1px solid #e1e5e9; +} + +.section-icon { + font-size: 1.5rem; + color: #4299e1; +} + +.section-title { + font-size: 1.5rem; + font-weight: 600; + color: #2d3748; + margin: 0; +} + +.form-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); + gap: 20px; +} + +.form-group { + display: flex; + flex-direction: column; + gap: 8px; +} + +.form-label { + font-weight: 500; + color: #4a5568; + font-size: 0.875rem; +} + +.form-input { + padding: 12px; + border: 1px solid #e1e5e9; + border-radius: 8px; + font-size: 1rem; + background: #f7fafc; + color: #4a5568; +} + +.empty-field { + color: #a0aec0; + font-style: italic; +} + +.password-section { + text-align: center; + padding: 40px 20px; +} + +.change-password-btn { + background: #4299e1; + color: white; + border: none; + padding: 12px 24px; + border-radius: 8px; + cursor: pointer; + font-weight: 500; + font-size: 1rem; + transition: background-color 0.2s; +} + +.change-password-btn:hover { + background: #3182ce; +} + +/* Modal Styles */ +.modal-overlay { + display: none; + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.5); + z-index: 1000; + align-items: center; + justify-content: center; + padding: 20px; +} + +.modal-content { + background: white; + border-radius: 12px; + width: 100%; + max-width: 500px; + max-height: 90vh; + overflow-y: auto; + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3); +} + +.modal-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 24px; + border-bottom: 1px solid #e1e5e9; +} + +.modal-title { + font-size: 1.5rem; + font-weight: 600; + color: #2d3748; + margin: 0; +} + +.modal-close { + background: none; + border: none; + font-size: 1.25rem; + cursor: pointer; + color: #718096; + padding: 5px; + border-radius: 4px; +} + +.modal-close:hover { + background: #f7fafc; + color: #2d3748; +} + +.modal-body { + padding: 24px; +} + +.modal-form { + display: flex; + flex-direction: column; + gap: 20px; +} + +.modal-form .form-group { + gap: 8px; +} + +.modal-form .form-label { + font-weight: 500; + color: #4a5568; +} + +.modal-form .form-input { + padding: 12px; + border: 1px solid #cbd5e0; + border-radius: 8px; + font-size: 1rem; + background: white; + transition: border-color 0.2s; +} + +.modal-form .form-input:focus { + outline: none; + border-color: #4299e1; + box-shadow: 0 0 0 3px rgba(66, 153, 225, 0.1); +} + +.modal-form .form-input.error { + border-color: #e53e3e; +} + +.error-message { + color: #e53e3e; + font-size: 0.875rem; + margin-top: 4px; +} + +.success-message { + color: #38a169; + font-size: 0.875rem; + margin-top: 4px; +} + +.modal-actions { + display: flex; + gap: 15px; + justify-content: flex-end; + margin-top: 30px; + padding-top: 20px; + border-top: 1px solid #e1e5e9; +} + +.save-btn { + background: #48bb78; + color: white; + border: none; + padding: 12px 24px; + border-radius: 8px; + cursor: pointer; + font-weight: 500; + transition: background-color 0.2s; +} + +.save-btn:hover { + background: #38a169; +} + +.save-btn:disabled { + background: #a0aec0; + cursor: not-allowed; +} + +.cancel-btn { + background: #e53e3e; + color: white; + border: none; + padding: 12px 24px; + border-radius: 8px; + cursor: pointer; + font-weight: 500; + transition: background-color 0.2s; +} + +.cancel-btn:hover { + background: #c53030; +} + +/* Logout Confirmation Modal Styles */ +.logout-modal-body { + text-align: center; + padding: 30px 20px; +} + +.logout-icon { + font-size: 3rem; + color: #e53e3e; + margin-bottom: 20px; +} + +.logout-title { + font-size: 1.5rem; + font-weight: 600; + color: #2d3748; + margin-bottom: 10px; +} + +.logout-message { + color: #718096; + font-size: 1rem; + line-height: 1.5; + margin-bottom: 30px; +} + +.logout-actions { + display: flex; + gap: 15px; + justify-content: center; +} + +.logout-confirm-btn { + background: #e53e3e; + color: white; + border: none; + padding: 12px 24px; + border-radius: 8px; + cursor: pointer; + font-weight: 500; + transition: background-color 0.2s; + min-width: 120px; +} + +.logout-confirm-btn:hover { + background: #c53030; +} + +.logout-cancel-btn { + background: #718096; + color: white; + border: none; + padding: 12px 24px; + border-radius: 8px; + cursor: pointer; + font-weight: 500; + transition: background-color 0.2s; + min-width: 120px; +} + +.logout-cancel-btn:hover { + background: #4a5568; +} + +@media (max-width: 768px) { + .form-grid { + grid-template-columns: 1fr; + } + + .modal-actions { + flex-direction: column; + } + + .logout-actions { + flex-direction: column; + } } \ No newline at end of file diff --git a/src/controllers/va/vaPayroll/vaPaySlip.controller.js b/src/controllers/va/vaPayroll/vaPaySlip.controller.js new file mode 100644 index 0000000..d81bfa8 --- /dev/null +++ b/src/controllers/va/vaPayroll/vaPaySlip.controller.js @@ -0,0 +1,12 @@ +async function va_payslip_data_function(req, res) { + try { + // Assuming we have a service function to get payroll schedule + const payrollSchedule = await getPayrollScheduleForVA(req.user.id); + return res.status(200).json({ success: true, data: payrollSchedule }); + } catch (error) { + console.error("Error fetching payroll schedule:", error); + return res.status(500).json({ success: false, message: "Internal Server Error" }); + } +} + +export { va_payslip_data_function }; \ No newline at end of file diff --git a/src/controllers/va/vaPayroll/vaPayrollSchedule.controller.js b/src/controllers/va/vaPayroll/vaPayrollSchedule.controller.js new file mode 100644 index 0000000..543abc5 --- /dev/null +++ b/src/controllers/va/vaPayroll/vaPayrollSchedule.controller.js @@ -0,0 +1,37 @@ +async function va_payroll_schedule_function(req, res) { + try { + const salesforceUrl = `${process.env.SF_DOCTRACKER_API_URL}services/apexrest/docva/api/payroll-schedule`; + + const response = await fetch(salesforceUrl, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + // Add any required headers for Salesforce authentication + // 'Authorization': 'Bearer ' + process.env.SF_ACCESS_TOKEN + }, + }); + + if (!response.ok) { + const errorText = await response.text(); + throw new Error(`Salesforce API responded with status ${response.status}: ${errorText}`); + } + + const result = await response.json(); + // console.log("✅ Successfully synced VA data to Salesforce:", result); + + res.status(201).json({ + success: true, + result + }); + } catch (error) { + console.error("Failed to sync with Salesforce:", error.message); + console.log(error) + + // Create a custom error to distinguish Salesforce errors + const salesforceError = new Error(`Salesforce sync failed: ${error.message}`); + salesforceError.isSalesforceError = true; + throw salesforceError; + } +} + +export { va_payroll_schedule_function }; \ No newline at end of file diff --git a/src/routes/v1/va.routes.js b/src/routes/v1/va.routes.js index c80794d..e50f1f0 100644 --- a/src/routes/v1/va.routes.js +++ b/src/routes/v1/va.routes.js @@ -27,6 +27,8 @@ import { va_leave_apply_function } from "../../controllers/va/vaLeaves/vaLeavesA import { va_leave_list_function } from "../../controllers/va/vaLeaves/vaLeaveList.controller.js" import { va_leave_manage_function } from "../../controllers/va/vaLeaves/vaLeaveManage.controller.js"; import { va_remove_function } from "../../controllers/va/vaProfile/vaDelete.controller.js"; +import { va_payroll_schedule_function } from "../../controllers/va/vaPayroll/vaPayrollSchedule.controller.js"; +// import { va_payslip_data_function } from "../../controllers/va/vaPayroll/vaPaySlip.controller.js"; vaRoutes.get("/", va_callback); // VA Callback for getting active VA @@ -40,9 +42,6 @@ vaRoutes.post("/verify-va-email", va_verify_email); vaRoutes.get("/profile", authValidator, va_profile_callback); // Get VA Profile vaRoutes.patch("/profile", authValidator, va_profile_update_callback); // Update VA Profile - - -vaRoutes.get("/:userName", va_username_callback); // VA Username Callback vaRoutes.delete("/:id", va_remove_function); // VA Username Callback // timesheet adjustment routes @@ -81,6 +80,14 @@ vaRoutes.get("/log/project/:projectId/:from?/:to?", authValidator, va_log_callba // VA filter search: supports query params `name`, `email`, `from`, `to` +// Payroll routes +vaRoutes.get("/payrollSchedule", authValidator, va_payroll_schedule_function); // VA Payroll Schedule +// vaRoutes.get("/paySlip/:vaId", authValidator, va_payslip_data_function); // VA Payslip + + + + +vaRoutes.get("/:userName", va_username_callback); // VA Username Callback export default vaRoutes; \ No newline at end of file diff --git a/views/client/dashboard-dashboard.ejs b/views/client/dashboard-dashboard.ejs index a845ea3..e317683 100644 --- a/views/client/dashboard-dashboard.ejs +++ b/views/client/dashboard-dashboard.ejs @@ -46,6 +46,12 @@
+ +
diff --git a/views/client/dashboard-profile.ejs b/views/client/dashboard-profile.ejs index cc2ea96..002091e 100644 --- a/views/client/dashboard-profile.ejs +++ b/views/client/dashboard-profile.ejs @@ -6,272 +6,7 @@ DocVA Client | Profile - + + + diff --git a/views/client/dashboard-projects.ejs b/views/client/dashboard-projects.ejs index 1bbf461..3561bc7 100644 --- a/views/client/dashboard-projects.ejs +++ b/views/client/dashboard-projects.ejs @@ -60,6 +60,12 @@ -->