diff --git a/public/css/dashboard.css b/public/css/dashboard.css index a816238..4bfd24f 100644 --- a/public/css/dashboard.css +++ b/public/css/dashboard.css @@ -643,4 +643,306 @@ body { .data-table td { padding: 0.5rem 0.75rem; } +} + +/* Popup Styles */ +.popup-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.5); + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; + backdrop-filter: blur(5px); +} + +.popup-content { + background: white; + border-radius: 12px; + box-shadow: 0 20px 60px rgba(0, 0, 0, 0.2); + width: 90%; + max-width: 500px; + max-height: 90vh; + overflow-y: auto; + animation: popupFadeIn 0.3s ease-out; +} + +@keyframes popupFadeIn { + from { + opacity: 0; + transform: scale(0.9) translateY(-20px); + } + to { + opacity: 1; + transform: scale(1) translateY(0); + } +} + +.popup-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 1.5rem; + border-bottom: 1px solid #e5e7eb; +} + +.popup-header h3 { + margin: 0; + color: #1f2937; + font-size: 1.25rem; + font-weight: 600; +} + +.popup-close { + background: none; + border: none; + font-size: 1.25rem; + color: #6b7280; + cursor: pointer; + padding: 0.25rem; + border-radius: 4px; + transition: all 0.2s; +} + +.popup-close:hover { + background: #f3f4f6; + color: #374151; +} + +.popup-body { + padding: 1.5rem; +} + +.va-details-grid { + display: grid; + gap: 1rem; +} + +.detail-item { + display: flex; + justify-content: space-between; + align-items: center; + padding: 0.75rem 0; + border-bottom: 1px solid #f3f4f6; +} + +.detail-item:last-child { + border-bottom: none; +} + +.detail-item label { + font-weight: 600; + color: #374151; + font-size: 0.875rem; +} + +.detail-item span { + color: #6b7280; + text-align: right; +} + +.status-active { + color: #059669; + font-weight: 600; +} + +.status-inactive { + color: #dc2626; + font-weight: 600; +} + +.popup-footer { + display: flex; + gap: 1rem; + padding: 1.5rem; + border-top: 1px solid #e5e7eb; + justify-content: flex-end; +} + +.btn { + padding: 0.75rem 1.5rem; + border: none; + border-radius: 8px; + font-weight: 500; + cursor: pointer; + text-decoration: none; + display: inline-flex; + align-items: center; + gap: 0.5rem; + transition: all 0.2s; + font-size: 0.875rem; +} + +.btn-primary { + background: #3b82f6; + color: white; +} + +.btn-primary:hover { + background: #2563eb; +} + +.btn-secondary { + background: #6b7280; + color: white; +} + +.btn-secondary:hover { + background: #4b5563; +} + +/* Make table rows clickable */ +tr[data-vaid] { + cursor: pointer; + transition: background-color 0.2s; +} + +tr[data-vaid]:hover { + background-color: #f8fafc !important; +} + +/* Ensure view link doesn't trigger row click */ +.view-link { + cursor: pointer; + z-index: 10; + position: relative; +} + +/* Project Details Styles in Popup */ +.projects-section { + margin-top: 1.5rem; + padding-top: 1.5rem; + border-top: 1px solid #e5e7eb; +} + +.projects-title { + font-size: 1rem; + font-weight: 600; + color: #1f2937; + margin: 0 0 1rem 0; +} + +.projects-list { + display: flex; + flex-direction: column; + gap: 0.75rem; +} + +.project-item { + background: #f8fafc; + border: 1px solid #e2e8f0; + border-radius: 8px; + padding: 1rem; + transition: all 0.2s; + margin-top: 12px; +} + +.project-item:hover { + background: #f1f5f9; + border-color: #cbd5e1; +} + +.project-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 0.5rem; +} + +.project-name { + font-weight: 600; + color: #1f2937; + font-size: 0.9rem; +} + +.project-time { + background: #3b82f6; + color: white; + padding: 0.25rem 0.5rem; + border-radius: 4px; + font-size: 0.8rem; + font-weight: 600; +} + +.project-id { + font-size: 0.75rem; + color: #6b7280; + font-family: monospace; +} + +.no-projects { + text-align: center; + color: #6b7280; + font-style: italic; + padding: 1rem; + background: #f8fafc; + border-radius: 6px; + border: 1px dashed #d1d5db; +} + +/* Adjust basic info layout */ +.va-basic-info { + display: grid; + gap: 1rem; + margin-bottom: 1rem; +} + +.detail-item { + display: flex; + justify-content: space-between; + align-items: center; + padding: 0.5rem 0; + border-bottom: 1px solid #f3f4f6; +} + +.detail-item:last-child { + border-bottom: none; +} + +.detail-item label { + font-weight: 600; + color: #374151; + font-size: 0.875rem; +} + +.detail-item span { + color: #6b7280; + text-align: right; +} + +.status-active { + color: #059669; + font-weight: 600; +} + +.status-inactive { + color: #dc2626; + font-weight: 600; +} + +/* Popup responsive adjustments */ +@media (max-width: 480px) { + .project-header { + flex-direction: column; + align-items: flex-start; + gap: 0.5rem; + } + + .project-time { + align-self: flex-start; + } + + .detail-item { + flex-direction: column; + align-items: flex-start; + gap: 0.25rem; + } + + .detail-item span { + text-align: left; + } } \ No newline at end of file diff --git a/src/controllers/client/dashboard/clientDashboard.controller.js b/src/controllers/client/dashboard/clientDashboard.controller.js index 5dcd6f9..cfd80bf 100644 --- a/src/controllers/client/dashboard/clientDashboard.controller.js +++ b/src/controllers/client/dashboard/clientDashboard.controller.js @@ -105,12 +105,92 @@ async function client_dashboard(req, res) { return acc; }, {}); + // Calculate total worked time for each VA + const totalWorkedTimeByVa = {}; + + // Get all logs for client's VAs to calculate total worked time + const allLogs = await LoggedModel.find({ + vaId: { $in: clientVaIds }, + projectId: { $in: clientProjectIds } + }) + .sort({ time: 1 }) + .lean(); + + // Group logs by VA + const logsByVa = {}; + allLogs.forEach(log => { + const vaId = log.vaId.toString(); + if (!logsByVa[vaId]) { + logsByVa[vaId] = []; + } + logsByVa[vaId].push(log); + }); + + // Calculate total worked time for each VA using the same logic as va_log_work_report_callback + 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 + if (!logsByDate[date]) { + logsByDate[date] = []; + } + logsByDate[date].push(log); + }); + + // Process each day separately + for (const [date, dayLogs] of Object.entries(logsByDate)) { + // Check if this day has any adjustment logs + const hasAdjustmentLogs = dayLogs.some(log => log.isAdjustment === true); + + 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 + const logsToProcess = hasAdjustmentLogs + ? dayLogs.filter(log => log.isAdjustment === true) + : dayLogs; + + // Sort logs by time for proper pairing + logsToProcess.sort((a, b) => new Date(a.time) - new Date(b.time)); + + // Calculate work duration for this day + for (const log of logsToProcess) { + const time = new Date(log.time); + + 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; + } + } + + totalWorkSeconds += dayWorkSeconds; + } + + totalWorkedTimeByVa[vaId] = { + totalSeconds: totalWorkSeconds, + formatted: formatTime(totalWorkSeconds) + }; + } + // Calculate session duration for currently active VAs const currentTime = new Date(); // Format all assigned VAs 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; // Calculate current session duration if still logged in @@ -140,10 +220,12 @@ async function client_dashboard(req, res) { } } + // Combine first and last name + const fullName = `${v.userDetails?.firstName || ''} ${v.userDetails?.lastName || ''}`.trim() || 'Unknown'; + return { id: v._id, - firstName: v.userDetails?.firstName || '', - lastName: v.userDetails?.lastName || '', + name: fullName, email: v.userDetails?.email, phone: v.userDetails?.phone, status: v.status || 'active', @@ -152,8 +234,10 @@ async function client_dashboard(req, res) { lastActivity: activity.lastActivity, currentSessionDuration: currentSessionDuration, currentSessionFormatted: currentSessionFormatted, + totalWorkedTime: workedTime.formatted, + totalWorkedSeconds: workedTime.totalSeconds, totalLogins: activity.totalLogins || 0, - lastProject: currentProjectDetails, // Now with project details + lastProject: currentProjectDetails, lastProjectId: lastLog?.projectId || null, projectsWorked: activity.projectsWorked || [], projectsAssigned: v.projectIds?.length || 0, @@ -164,6 +248,7 @@ async function client_dashboard(req, res) { // 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; @@ -184,16 +269,20 @@ async function client_dashboard(req, res) { } } + // Combine first and last name + const fullName = `${v.userDetails?.firstName || ''} ${v.userDetails?.lastName || ''}`.trim() || 'Unknown'; + return { id: v._id, - firstName: v.userDetails?.firstName || '', - lastName: v.userDetails?.lastName || '', + 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, @@ -242,6 +331,9 @@ async function client_dashboard(req, res) { const currentlyActiveVAs = allVAsFormatted.filter(va => va.isCurrentlyActive); const inactiveVAs = allVAsFormatted.filter(va => !va.isCurrentlyActive); + // 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; @@ -267,6 +359,7 @@ async function client_dashboard(req, res) { projectsWithActiveWorkers: projectsWithActiveWorkers, totalHoursLogged: totalHoursLogged, totalDedicatedHours: totalDedicatedHours, + totalWorkedTime: formatTime(totalWorkedTimeAllVAs), utilizationRate: totalDedicatedHours > 0 ? ((totalHoursLogged / totalDedicatedHours) * 100).toFixed(2) + '%' : '0%', lastUpdated: currentTime.toISOString() @@ -293,19 +386,12 @@ async function client_dashboard(req, res) { } } -// Helper function to format time +// Helper function to format time (same as in va_log_work_report_callback) function formatTime(seconds) { - const hours = Math.floor(seconds / 3600); - const minutes = Math.floor((seconds % 3600) / 60); - const remainingSeconds = seconds % 60; - - if (hours > 0) { - return `${hours}h ${minutes}m ${remainingSeconds}s`; - } else if (minutes > 0) { - return `${minutes}m ${remainingSeconds}s`; - } else { - return `${remainingSeconds}s`; - } + const h = Math.floor(seconds / 3600); + const m = Math.floor((seconds % 3600) / 60); + const s = Math.floor(seconds % 60); + return `${h.toString().padStart(2, "0")}:${m.toString().padStart(2, "0")}:${s.toString().padStart(2, "0")}`; } export { client_dashboard }; \ No newline at end of file diff --git a/src/controllers/client/dashboard/clientDashboardVaLog.controller.js b/src/controllers/client/dashboard/clientDashboardVaLog.controller.js new file mode 100644 index 0000000..4e1c5c7 --- /dev/null +++ b/src/controllers/client/dashboard/clientDashboardVaLog.controller.js @@ -0,0 +1,166 @@ +import jwt from "jsonwebtoken"; +import { VAModel, ProjectModel, LoggedModel, ClientModel } from "../../../models/vaModel.js"; + +// Helper function to parse date string in DD-MM-YYYY format +function parseDate(dateString) { + if (!dateString) return null; + + const parts = dateString.split('-'); + if (parts.length !== 3) return null; + + const day = parseInt(parts[0], 10); + const month = parseInt(parts[1], 10) - 1; // Months are 0-indexed in JavaScript + const year = parseInt(parts[2], 10); + + // Validate date components + if (isNaN(day) || isNaN(month) || isNaN(year)) return null; + if (day < 1 || day > 31 || month < 0 || month > 11 || year < 1900) return null; + + return new Date(year, month, day); +} + +// Getting logged hours in a specific project of VA +async function client_dashboard_va_log(req, res) { + console.log(req) + 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 clientUser = await ClientModel.findOne({ "userDetails.email": clientEmail }); + if (!clientUser) { + return res.status(404).json({ success: false, message: "Client not found" }); + } + + const clientID = clientUser._id; + const clientProjectIds = clientUser.projectIds || []; + + const payload = req.body || {}; + const { vaId } = payload; + + // const va = await VAModel.findOne({ + // _id: vaId, + // clientIds: clientID + // }); + + // if (!va) { + // return res.status(403).json({ + // success: false, + // message: "VA not found or not assigned to this client" + // }); + // } + + let logs = await LoggedModel.find({ + vaId, + projectId: { $in: clientProjectIds }, + }) + .sort({ time: 1 }) + .populate("projectId", "name") // attach project name + .lean(); + + if (!logs.length) { + return res.status(200).json({ + success: true, + message: "No logs found in given range", + data: [], + totalWorkTime: "00:00:00", + }); + } + + let totalWorkSeconds = 0; + const projectWork = {}; // key: projectId -> seconds + + // 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 + if (!logsByDate[date]) { + logsByDate[date] = []; + } + logsByDate[date].push(log); + }); + + // Process each day separately + for (const [date, dayLogs] of Object.entries(logsByDate)) { + // Check if this day has any adjustment logs + const hasAdjustmentLogs = dayLogs.some(log => log.isAdjustment === true); + + 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 + const logsToProcess = hasAdjustmentLogs + ? dayLogs.filter(log => log.isAdjustment === true) + : dayLogs; + + // Sort logs by time for proper pairing (already sorted by query, but ensure) + logsToProcess.sort((a, b) => new Date(a.time) - new Date(b.time)); + + // Calculate work duration for this day + for (const log of logsToProcess) { + const time = new Date(log.time); + + if (log.loggedType === "LOGIN") { + lastLogin = time; + lastProjectId = log.projectId?._id?.toString() || null; + } else if (log.loggedType === "LOGOUT" && lastLogin) { + const diffSec = (time - lastLogin) / 1000; + dayWorkSeconds += diffSec; + + if (lastProjectId) { + projectWork[lastProjectId] = (projectWork[lastProjectId] || 0) + diffSec; + } + + // reset after each pair + lastLogin = null; + lastProjectId = null; + } + } + + totalWorkSeconds += dayWorkSeconds; + } + + // 🕓 Convert total seconds to hh:mm:ss + const formatTime = (seconds) => { + const h = Math.floor(seconds / 3600); + const m = Math.floor((seconds % 3600) / 60); + const s = Math.floor(seconds % 60); + return `${h.toString().padStart(2, "0")}:${m.toString().padStart(2, "0")}:${s.toString().padStart(2, "0")}`; + }; + + // 🧠 Build project-wise array + const projectDetails = await Promise.all( + Object.entries(projectWork).map(async ([projectId, sec]) => { + const project = await ProjectModel.findById(projectId).lean(); + return { + projectId, + projectName: project?.name || "Unknown Project", + totalTime: formatTime(sec), + }; + }) + ); + + return res.status(200).json({ + success: true, + totalWorkTime: formatTime(totalWorkSeconds), + projects: projectDetails, + }); + + } catch (error) { + console.log(error) + return res.status(500).send("Internal Server Error"); + } +} + +export { client_dashboard_va_log }; \ No newline at end of file diff --git a/src/controllers/project/projectAddSf.controller.js b/src/controllers/project/projectAddSf.controller.js new file mode 100644 index 0000000..39bb9b5 --- /dev/null +++ b/src/controllers/project/projectAddSf.controller.js @@ -0,0 +1,120 @@ +import { ProjectModel, VAModel, ClientModel } from "../../models/vaModel.js"; +import { Types } from "mongoose"; +import { updateEntityRelationships } from "../../helper/updateEntityRelationships.js"; +// Register a new VA +async function project_add_sf(req, res) { + try { + const payload = req.body|| {}; + const { name, hourlyRate, clientId, vaAssigned, dedicatedHours } = payload; + + // Validate required fields + if (!name) { + return res.status(400).json({ + success: false, + message: "Project name is required" + }); + } + + if (!clientId) { + return res.status(400).json({ + success: false, + message: "Client ID is required" + }); + } + + // Validate and convert clientId to ObjectId + let clientObjectId; + try { + clientObjectId = new Types.ObjectId(clientId); + } catch (error) { + return res.status(400).json({ + success: false, + message: "Invalid Client ID format" + }); + } + + // Validate and convert vaAssigned to array of ObjectIds + let vaAssignedArray = []; + if (vaAssigned) { + if (Array.isArray(vaAssigned)) { + try { + vaAssignedArray = vaAssigned.map(vaId => new Types.ObjectId(vaId)); + } catch (error) { + return res.status(400).json({ + success: false, + message: "Invalid VA ID format in vaAssigned array" + }); + } + } else if (typeof vaAssigned === 'string') { + // Handle single VA ID as string + try { + vaAssignedArray = [new Types.ObjectId(vaAssigned)]; + } catch (error) { + return res.status(400).json({ + success: false, + message: "Invalid VA ID format" + }); + } + } + } + + // sanitize/construct allowed fields + const toCreate = { + name: name.trim(), + hourlyRate: hourlyRate || '0', + clientId: clientObjectId, + vaAssigned: vaAssignedArray, + dedicatedHours: dedicatedHours ? parseFloat(dedicatedHours) : 0, + }; + + const created = await ProjectModel.create(toCreate); + + const relationshipUpdate = await updateEntityRelationships({ + project: { + projectId: created._id, + clientId: clientObjectId, + vaId: vaAssignedArray + } + }); + + if (!relationshipUpdate.success) { + 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; + } + } catch (error) { + console.error("project_add_sf error:", error); + return res.status(500).json({ success: false, message: "Internal Server Error" }); + } +} + +export { project_add_sf }; \ No newline at end of file diff --git a/src/controllers/project/projectUpdateSf.controller.js b/src/controllers/project/projectUpdateSf.controller.js new file mode 100644 index 0000000..5035f29 --- /dev/null +++ b/src/controllers/project/projectUpdateSf.controller.js @@ -0,0 +1,217 @@ +import { ProjectModel } from "../../models/vaModel.js"; +import { Types } from "mongoose"; +import { updateEntityRelationships, removeEntityRelationships } from "../../helper/updateEntityRelationships.js"; + +/** + * Update project details - PATCH endpoint + * Supports updating: status, VA assignments, hourly rate, dedicated hours, etc. + */ +async function update_project_sf(req, res) { + try { + const { projectId } = req.params; + const payload = req.body || {}; + const { + name, + hourlyRate, + vaAssigned, + dedicatedHours, + status, + totalLoggedHours + } = payload; + + // 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" + }); + } + + // Prepare update data + const updateData = {}; + let relationshipUpdate = null; + let vaRelationshipUpdate = null; + + // Update name if provided + if (name !== undefined) { + if (!name.trim()) { + return res.status(400).json({ + success: false, + message: "Project name cannot be empty" + }); + } + updateData.name = name.trim(); + } + + // Update hourly rate if provided + if (hourlyRate !== undefined) { + updateData.hourlyRate = hourlyRate; + } + + // Update dedicated hours if provided + if (dedicatedHours !== undefined) { + updateData.dedicatedHours = parseFloat(dedicatedHours) || 0; + } + + // Update total logged hours if provided + if (totalLoggedHours !== undefined) { + updateData.totalLoggedHours = parseFloat(totalLoggedHours) || 0; + } + + // Update status if provided + if (status !== undefined) { + if (!["ACTIVE", "INACTIVE"].includes(status)) { + return res.status(400).json({ + success: false, + message: "Status must be either ACTIVE or INACTIVE" + }); + } + updateData.status = status; + } + + // Handle VA assignments - this requires relationship updates + if (vaAssigned !== undefined) { + let newVaAssignedArray = []; + + // Parse and validate VA IDs + if (vaAssigned && vaAssigned.length > 0) { + if (Array.isArray(vaAssigned)) { + try { + newVaAssignedArray = vaAssigned.map(vaId => new Types.ObjectId(vaId)); + } catch (error) { + return res.status(400).json({ + success: false, + message: "Invalid VA ID format in vaAssigned array" + }); + } + } else if (typeof vaAssigned === 'string') { + try { + newVaAssignedArray = [new Types.ObjectId(vaAssigned)]; + } catch (error) { + return res.status(400).json({ + success: false, + message: "Invalid VA ID format" + }); + } + } + } + + updateData.vaAssigned = newVaAssignedArray; + + // Calculate VAs to add and remove for relationship updates + const currentVaIds = existingProject.vaAssigned.map(va => va.toString()); + const newVaIds = newVaAssignedArray.map(va => va.toString()); + + const vaToAdd = newVaAssignedArray.filter(va => !currentVaIds.includes(va.toString())); + const vaToRemove = existingProject.vaAssigned.filter(va => !newVaIds.includes(va.toString())); + + // Update relationships for added VAs + if (vaToAdd.length > 0) { + relationshipUpdate = await updateEntityRelationships({ + project: { + projectId: projectObjectId, + clientId: existingProject.clientId, + vaId: vaToAdd + } + }); + } + + // Remove relationships for removed VAs + if (vaToRemove.length > 0) { + vaRelationshipUpdate = await removeEntityRelationships({ + project: { + projectId: projectObjectId, + clientId: existingProject.clientId, + vaId: vaToRemove + } + }); + } + } + + // If no fields to update + if (Object.keys(updateData).length === 0) { + return res.status(400).json({ + success: false, + message: "No valid fields to update" + }); + } + + // Update the project + const updatedProject = await ProjectModel.findByIdAndUpdate( + projectObjectId, + updateData, + { new: true, runValidators: true } + ).populate('clientId', 'userDetails.name userDetails.email') + .populate('vaAssigned', 'userDetails.firstName userDetails.lastName userDetails.email'); + + // If VA assignments were changed but relationship update failed, log warning + if (vaAssigned !== undefined) { + if (relationshipUpdate && !relationshipUpdate.success) { + console.warn('Project updated but VA relationship addition failed:', relationshipUpdate.message); + } + if (vaRelationshipUpdate && !vaRelationshipUpdate.success) { + console.warn('Project updated but VA relationship removal failed:', vaRelationshipUpdate.message); + } + } + + return res.status(200).json({ + success: true, + message: "Project updated successfully", + data: updatedProject, + relationships: { + added: relationshipUpdate, + removed: vaRelationshipUpdate + } + }); + + } catch (error) { + console.error("update_project_sf error:", error); + + // MongoDB duplicate key error + if (error.code === 11000) { + return res.status(400).json({ + success: false, + message: "Project with this name already exists" + }); + } + + // MongoDB validation error + if (error.name === 'ValidationError') { + const errors = Object.values(error.errors).map(err => err.message); + return res.status(400).json({ + success: false, + message: "Validation error", + errors: errors + }); + } + + return res.status(500).json({ + success: false, + message: "Internal Server Error", + error: error.message + }); + } +} + +export { + update_project_sf +}; \ No newline at end of file diff --git a/src/controllers/va/vaLog/vaLogWorkReport.js b/src/controllers/va/vaLog/vaLogWorkReport.js index 8064a39..91adb44 100644 --- a/src/controllers/va/vaLog/vaLogWorkReport.js +++ b/src/controllers/va/vaLog/vaLogWorkReport.js @@ -85,35 +85,56 @@ async function va_log_work_report_callback(req, res) { let totalWorkSeconds = 0; const projectWork = {}; // key: projectId -> seconds - let lastLogin = null; - let lastProjectId = null; - - const hasAdjustmentLogs = logs.some(log => log.isAdjustment === true); - - if(hasAdjustmentLogs) { - logs = logs.filter(log => log.isAdjustment === true); - } - - - // ⏱️ Calculate work duration - for (const log of logs) { - const time = new Date(log.time); - - if (log.loggedType === "LOGIN") { - lastLogin = time; - lastProjectId = log.projectId?._id?.toString() || null; - } else if (log.loggedType === "LOGOUT" && lastLogin) { - const diffSec = (time - lastLogin) / 1000; - totalWorkSeconds += diffSec; - - if (lastProjectId) { - projectWork[lastProjectId] = (projectWork[lastProjectId] || 0) + diffSec; - } - - // reset after each pair - lastLogin = null; - lastProjectId = null; + // 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 + if (!logsByDate[date]) { + logsByDate[date] = []; } + logsByDate[date].push(log); + }); + + // Process each day separately + for (const [date, dayLogs] of Object.entries(logsByDate)) { + // Check if this day has any adjustment logs + const hasAdjustmentLogs = dayLogs.some(log => log.isAdjustment === true); + + 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 + const logsToProcess = hasAdjustmentLogs + ? dayLogs.filter(log => log.isAdjustment === true) + : dayLogs; + + // Sort logs by time for proper pairing (already sorted by query, but ensure) + logsToProcess.sort((a, b) => new Date(a.time) - new Date(b.time)); + + // Calculate work duration for this day + for (const log of logsToProcess) { + const time = new Date(log.time); + + if (log.loggedType === "LOGIN") { + lastLogin = time; + lastProjectId = log.projectId?._id?.toString() || null; + } else if (log.loggedType === "LOGOUT" && lastLogin) { + const diffSec = (time - lastLogin) / 1000; + dayWorkSeconds += diffSec; + + if (lastProjectId) { + projectWork[lastProjectId] = (projectWork[lastProjectId] || 0) + diffSec; + } + + // reset after each pair + lastLogin = null; + lastProjectId = null; + } + } + + totalWorkSeconds += dayWorkSeconds; } // 🕓 Convert total seconds to hh:mm:ss @@ -151,4 +172,4 @@ async function va_log_work_report_callback(req, res) { } } -export { va_log_work_report_callback }; +export { va_log_work_report_callback }; \ No newline at end of file diff --git a/src/controllers/va/vaTimesheetAdjustment/vaTimesheetAdjustment.controller.js b/src/controllers/va/vaTimesheetAdjustment/vaTimesheetAdjustment.controller.js index 6614c71..d659168 100644 --- a/src/controllers/va/vaTimesheetAdjustment/vaTimesheetAdjustment.controller.js +++ b/src/controllers/va/vaTimesheetAdjustment/vaTimesheetAdjustment.controller.js @@ -60,13 +60,17 @@ 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(); + // // 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(Date.UTC(year, month, day, hours, minutes, seconds)); + // // Create UTC date directly + // return new Date(year, month, day, hours, minutes, seconds); + const combined = new Date(dateObj); + combined.setHours(hours, minutes, seconds, 0); + + return combined; }; const loginDateTime = combineDateTime(adjustmentDate, loginTime); diff --git a/src/helper/updateEntityRelationships.js b/src/helper/updateEntityRelationships.js new file mode 100644 index 0000000..5a321f1 --- /dev/null +++ b/src/helper/updateEntityRelationships.js @@ -0,0 +1,290 @@ +import { ClientModel, VAModel, ProjectModel } from "../models/vaModel.js"; + +/** + * Generic function to update relationships between Client, VA, and Project + * @param {Object} data - The update data object + * @returns {Object} - Result of the update operations + */ +async function updateEntityRelationships(data) { + try { + let results = {}; + + // Handle Project updates + if (data.project) { + const { projectId, vaId, clientId } = data.project; + + if (!projectId) { + throw new Error('Project ID is required for project updates'); + } + + // Update Client with projectId + if (clientId) { + const clientUpdate = await ClientModel.findByIdAndUpdate( + clientId, + { + $addToSet: { + projectIds: projectId + } + }, + { new: true } + ); + results.clientUpdate = clientUpdate ? 'Client updated successfully' : 'Client not found'; + } + + // Update VAs with projectId + if (vaId && Array.isArray(vaId) && vaId.length > 0) { + const vaUpdateResult = await VAModel.updateMany( + { _id: { $in: vaId } }, + { + $addToSet: { + projectIds: projectId + } + } + ); + results.vaUpdate = `Updated ${vaUpdateResult.modifiedCount} VAs`; + } + + // Update Project with clientId and vaId + const projectUpdateData = {}; + if (clientId) { + projectUpdateData.clientId = clientId; + } + if (vaId && Array.isArray(vaId)) { + projectUpdateData.vaAssigned = vaId; + } + + if (Object.keys(projectUpdateData).length > 0) { + const projectUpdate = await ProjectModel.findByIdAndUpdate( + projectId, + projectUpdateData, + { new: true } + ); + results.projectUpdate = projectUpdate ? 'Project updated successfully' : 'Project not found'; + } + } + + // Handle VA updates + if (data.va) { + const { vaId, projectId, clientId } = data.va; + + if (!vaId) { + throw new Error('VA ID is required for VA updates'); + } + + // Update Projects with vaId + if (projectId && Array.isArray(projectId) && projectId.length > 0) { + const projectUpdateResult = await ProjectModel.updateMany( + { _id: { $in: projectId } }, + { + $addToSet: { + vaAssigned: vaId + } + } + ); + results.projectUpdate = `Updated ${projectUpdateResult.modifiedCount} projects`; + } + + // Update Clients with vaId + if (clientId && Array.isArray(clientId) && clientId.length > 0) { + const clientUpdateResult = await ClientModel.updateMany( + { _id: { $in: clientId } }, + { + $addToSet: { + vaIds: vaId + } + } + ); + results.clientUpdate = `Updated ${clientUpdateResult.modifiedCount} clients`; + } + + // Update VA with clientId and projectId + const vaUpdateData = {}; + if (clientId && Array.isArray(clientId)) { + vaUpdateData.clientIds = clientId; + } + if (projectId && Array.isArray(projectId)) { + vaUpdateData.projectIds = projectId; + } + + if (Object.keys(vaUpdateData).length > 0) { + const vaUpdate = await VAModel.findByIdAndUpdate( + vaId, + { $addToSet: vaUpdateData }, + { new: true } + ); + results.vaUpdate = vaUpdate ? 'VA updated successfully' : 'VA not found'; + } + } + + // Handle Client updates + if (data.client) { + const { clientId, projectId, vaId } = data.client; + + if (!clientId) { + throw new Error('Client ID is required for client updates'); + } + + // Update Projects with clientId + if (projectId && Array.isArray(projectId) && projectId.length > 0) { + const projectUpdateResult = await ProjectModel.updateMany( + { _id: { $in: projectId } }, + { clientId: clientId } + ); + results.projectUpdate = `Updated ${projectUpdateResult.modifiedCount} projects`; + } + + // Update VAs with clientId + if (vaId && Array.isArray(vaId) && vaId.length > 0) { + const vaUpdateResult = await VAModel.updateMany( + { _id: { $in: vaId } }, + { + $addToSet: { + clientIds: clientId + } + } + ); + results.vaUpdate = `Updated ${vaUpdateResult.modifiedCount} VAs`; + } + + // Update Client with projectId and vaId + const clientUpdateData = {}; + if (projectId && Array.isArray(projectId)) { + clientUpdateData.projectIds = projectId; + } + if (vaId && Array.isArray(vaId)) { + clientUpdateData.vaIds = vaId; + } + + if (Object.keys(clientUpdateData).length > 0) { + const clientUpdate = await ClientModel.findByIdAndUpdate( + clientId, + { $addToSet: clientUpdateData }, + { new: true } + ); + results.clientUpdate = clientUpdate ? 'Client updated successfully' : 'Client not found'; + } + } + + return { + success: true, + message: 'Relationships updated successfully', + results + }; + + } catch (error) { + console.error('Error updating entity relationships:', error); + return { + success: false, + message: error.message, + results: {} + }; + } +} + +/** + * Remove relationships (for deletion cases) + * @param {Object} data - The removal data object + * @returns {Object} - Result of the removal operations + */ +async function removeEntityRelationships(data) { + try { + let results = {}; + + // Handle Project removal + if (data.project) { + const { projectId, vaId, clientId } = data.project; + + if (!projectId) { + throw new Error('Project ID is required for project removal'); + } + + // Remove projectId from Client + if (clientId) { + await ClientModel.findByIdAndUpdate( + clientId, + { $pull: { projectIds: projectId } } + ); + } + + // Remove projectId from VAs + if (vaId && Array.isArray(vaId)) { + await VAModel.updateMany( + { _id: { $in: vaId } }, + { $pull: { projectIds: projectId } } + ); + } + + results.projectRemoval = 'Project relationships removed successfully'; + } + + // Handle VA removal + if (data.va) { + const { vaId, projectId, clientId } = data.va; + + if (!vaId) { + throw new Error('VA ID is required for VA removal'); + } + + // Remove vaId from Projects + if (projectId && Array.isArray(projectId)) { + await ProjectModel.updateMany( + { _id: { $in: projectId } }, + { $pull: { vaAssigned: vaId } } + ); + } + + // Remove vaId from Clients + if (clientId && Array.isArray(clientId)) { + await ClientModel.updateMany( + { _id: { $in: clientId } }, + { $pull: { vaIds: vaId } } + ); + } + + results.vaRemoval = 'VA relationships removed successfully'; + } + + // Handle Client removal + if (data.client) { + const { clientId, projectId, vaId } = data.client; + + if (!clientId) { + throw new Error('Client ID is required for client removal'); + } + + // Remove clientId from Projects + if (projectId && Array.isArray(projectId)) { + await ProjectModel.updateMany( + { _id: { $in: projectId } }, + { clientId: null } + ); + } + + // Remove clientId from VAs + if (vaId && Array.isArray(vaId)) { + await VAModel.updateMany( + { _id: { $in: vaId } }, + { $pull: { clientIds: clientId } } + ); + } + + results.clientRemoval = 'Client relationships removed successfully'; + } + + return { + success: true, + message: 'Relationships removed successfully', + results + }; + + } catch (error) { + console.error('Error removing entity relationships:', error); + return { + success: false, + message: error.message, + results: {} + }; + } +} + +export { updateEntityRelationships, removeEntityRelationships }; \ No newline at end of file diff --git a/src/models/vaModel.js b/src/models/vaModel.js index 5bb7bac..d486ce2 100644 --- a/src/models/vaModel.js +++ b/src/models/vaModel.js @@ -90,13 +90,14 @@ const projectSchema = new Schema({ ref: "VA", }, ], - clientId: { - type: Types.ObjectId, - ref: "Client" - }, dedicatedHours: { type: Number, default: 0 + }, + status: { + type: String, + enum: ["ACTIVE", "INACTIVE"], + default: "ACTIVE" } }); diff --git a/src/routes/v1/client.routes.js b/src/routes/v1/client.routes.js index 960906a..941632f 100644 --- a/src/routes/v1/client.routes.js +++ b/src/routes/v1/client.routes.js @@ -9,6 +9,7 @@ import { client_refresh_token_callback } from "../../controllers/client/clientRe import { client_request_password_reset } from "../../controllers/passwordReset.controllers.js"; import { clientAuthValidator } from "../../middlewares/auth.middleware.js"; import { client_dashboard } from "./../../controllers/client/dashboard/clientDashboard.controller.js"; +import { client_dashboard_va_log } from "../../controllers/client/dashboard/clientDashboardVaLog.controller.js" // clients routes clientRoutes.post("/register", va_register_client); // Register a new VA @@ -22,7 +23,8 @@ clientRoutes.get("/:clientId/projects/:projectId", va_clients_projects_callback) // dashboard API's -clientRoutes.get("/dashboard", clientAuthValidator, client_dashboard); // VA Clients Projects Callback (NC) +clientRoutes.get("/dashboard", clientAuthValidator, client_dashboard); // VA Clients Projects Callback +clientRoutes.post("/dashboard/va-report", clientAuthValidator, client_dashboard_va_log); // VA Clients Projects Callback diff --git a/src/routes/v1/index.js b/src/routes/v1/index.js index 0add865..a426f60 100644 --- a/src/routes/v1/index.js +++ b/src/routes/v1/index.js @@ -9,6 +9,9 @@ import { fileURLToPath } from 'url'; import { dirname } from 'path'; 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"; const v1Routes = express.Router() @@ -24,6 +27,9 @@ v1Routes.use("/auth_callback", authRoutes); v1Routes.use("/va", vaRoutes); v1Routes.use("/client", clientRoutes); +v1Routes.post("/project", authValidator, project_add_sf); +v1Routes.patch("/project/:projectId", authValidator, update_project_sf); + v1Routes.post("/password/reset", process_reset); v1Routes.get("/", function(req, res) { diff --git a/src/server.js b/src/server.js index f13aaac..46c0973 100644 --- a/src/server.js +++ b/src/server.js @@ -39,11 +39,21 @@ app.use(bodyParser.text()); // API Route app.use('/api', apiRoutes); -app.get('/varegister', (req, res) => { +app.get('/varegister/:sf_token?', (req, res) => { + // const { sf_token } = useParams(); + res.render('va_register', { title: "VA Registration" }); + // if(verified) { + // res.render('va_register', { title: "VA Registration" }); + // } else { + // res.render('not_founf', { title: "VA Registration" }); + // } }); -app.get('/clientregister', (req, res) => { +app.get('/clientregister/:sf_token?', (req, res) => { + + // const { sf_token } = useParams(); + res.render('va_client_register', { title: "Client Registration" }); }); @@ -62,25 +72,25 @@ app.get("/client/forgot-password", (req, res) => { // Client Dashboard Routes with new file names app.get('/client/dashboard', async (req, res) => { - try { - // const dashboardData = await dashboardService.getClientDashboard(req.user.id); - // res.render('client/dashboard-dashboard', dashboardData); - res.render('client/dashboard-dashboard'); - } catch (error) { - console.error('Dashboard error:', error); - res.status(500).render('client/dashboard-dashboard', { error: 'Failed to load dashboard' }); - } + try { + // const dashboardData = await dashboardService.getClientDashboard(req.user.id); + // res.render('client/dashboard-dashboard', dashboardData); + res.render('client/dashboard-dashboard'); + } catch (error) { + console.error('Dashboard error:', error); + res.status(500).render('client/dashboard-dashboard', { error: 'Failed to load dashboard' }); + } }); app.get('/client/virtual-assistants', async (req, res) => { - try { - // const vaData = await virtualAssistantService.getClientVAs(req.user.id); - res.render('client/dashboard-virtual-assistants'); - // res.render('client/dashboard-virtual-assistants', vaData); - } catch (error) { - console.error('Virtual assistants error:', error); - res.status(500).render('client/dashboard-virtual-assistants', { error: 'Failed to load virtual assistants' }); - } + try { + // const vaData = await virtualAssistantService.getClientVAs(req.user.id); + res.render('client/dashboard-virtual-assistants'); + // res.render('client/dashboard-virtual-assistants', vaData); + } catch (error) { + console.error('Virtual assistants error:', error); + res.status(500).render('client/dashboard-virtual-assistants', { error: 'Failed to load virtual assistants' }); + } }); // app.get('/client/timesheets', async (req, res) => { @@ -144,7 +154,7 @@ io.use(async (socket, next) => { return next(new Error("Invalid token payload")); } - /* ---------------------- 1⃣ Check if VA ---------------------- */ + /* ---------------------- 1⃣ Check if VA ---------------------- */ let vaUser = null; let clientData = null; @@ -162,7 +172,7 @@ io.use(async (socket, next) => { socket.data.role = "va"; socket.data.vaId = vaUser._id.toString(); socket.data.vaEmail = decoded.email; - + const a = vaProjClientId.toString(); const b = clientData.clientId._id.toString(); const c = JSON.stringify(clientData); @@ -175,7 +185,7 @@ io.use(async (socket, next) => { return next(); } - /* ---------------------- 2⃣ Check if Client ---------------------- */ + /* ---------------------- 2⃣ Check if Client ---------------------- */ // const clientUser = await ClientModel.findOne({ "userDetails.email": decoded.email }); // if (clientUser) { diff --git a/views/client/dashboard-dashboard.ejs b/views/client/dashboard-dashboard.ejs index 091dc6b..c92dce3 100644 --- a/views/client/dashboard-dashboard.ejs +++ b/views/client/dashboard-dashboard.ejs @@ -148,7 +148,8 @@