From 4d757daf97a1f2988d7747442d041fed6d8fb6d4 Mon Sep 17 00:00:00 2001 From: Aakash Date: Thu, 27 Nov 2025 19:58:33 +0530 Subject: [PATCH] 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 @@
- +
-
+