From 5f50082f87bebe4794c606d829ecce0857a0c674 Mon Sep 17 00:00:00 2001 From: Aakash Date: Mon, 24 Nov 2025 16:47:53 +0530 Subject: [PATCH] Added projects in dashboard --- public/css/dashboard.css | 319 ++++++++++++++---- public/js/va-register.js | 8 + .../clientDashboardProjects.controller.js | 144 +++++--- src/server.js | 10 +- views/client/dashboard-dashboard.ejs | 11 - views/client/dashboard-projects.ejs | 178 +++++++--- views/client/dashboard-virtual-assistants.ejs | 11 - views/va_register.ejs | 12 +- 8 files changed, 523 insertions(+), 170 deletions(-) diff --git a/public/css/dashboard.css b/public/css/dashboard.css index cbb0fa6..d73ce47 100644 --- a/public/css/dashboard.css +++ b/public/css/dashboard.css @@ -871,7 +871,6 @@ tr[data-vaid]:hover { .project-name { font-weight: 600; - color: #1f2937; font-size: 0.9rem; } @@ -963,7 +962,6 @@ tr[data-vaid]:hover { } } -/* Modal Styles */ .modal-overlay { display: none; position: fixed; @@ -973,125 +971,332 @@ tr[data-vaid]:hover { height: 100%; background: rgba(0, 0, 0, 0.5); z-index: 1000; - justify-content: center; - align-items: center; + backdrop-filter: blur(5px); } .modal-content { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); background: white; - border-radius: 8px; - padding: 20px; - max-width: 500px; + border-radius: 12px; + box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3); width: 90%; + max-width: 800px; max-height: 80vh; - overflow-y: auto; - box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); + overflow: hidden; + display: flex; + flex-direction: column; } .modal-header { + padding: 20px 24px; + border-bottom: 1px solid #e5e7eb; + background: #f8fafc; display: flex; justify-content: between; align-items: center; - margin-bottom: 15px; - border-bottom: 1px solid #e5e7eb; - padding-bottom: 10px; } .modal-title { - font-size: 1.25rem; + font-size: 1.5rem; font-weight: 600; + color: #1f2937; margin: 0; } -.close-modal { +.modal-close { background: none; border: none; font-size: 1.5rem; cursor: pointer; color: #6b7280; + padding: 4px; + border-radius: 4px; + transition: all 0.2s; } -.close-modal:hover { +.modal-close:hover { color: #374151; + background: #e5e7eb; } -.va-list { +.modal-body { + padding: 24px; + overflow-y: auto; + flex-grow: 1; +} + +.project-info-section { + margin-bottom: 24px; + padding: 16px; + background: #f8fafc; + border-radius: 8px; + border-left: 4px solid #3b82f6; +} + +.project-info-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 16px; + margin-bottom: 16px; +} + +.project-info-item { display: flex; flex-direction: column; - gap: 10px; } -.va-item { +.project-info-label { + font-size: 0.875rem; + color: #6b7280; + margin-bottom: 4px; +} + +.project-info-value { + font-size: 1rem; + font-weight: 500; + color: #1f2937; +} + +.va-section-title { + font-size: 1.25rem; + font-weight: 600; + color: #1f2937; + margin-bottom: 16px; display: flex; - justify-content: space-between; align-items: center; - padding: 10px; - border: 1px solid #e5e7eb; - border-radius: 6px; + gap: 8px; +} + +.va-count-badge { + background: #3b82f6; + color: white; + padding: 2px 8px; + border-radius: 12px; + font-size: 0.75rem; + font-weight: 500; +} + +.va-table { + width: 100%; + border-collapse: collapse; + background: white; + border-radius: 8px; + overflow: hidden; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); +} + +.va-table th { + background: #f8fafc; + padding: 12px 16px; + text-align: left; + font-weight: 600; + color: #374151; + border-bottom: 1px solid #e5e7eb; + font-size: 0.875rem; +} + +.va-table td { + padding: 16px; + border-bottom: 1px solid #f3f4f6; +} + +.va-table tr:last-child td { + border-bottom: none; +} + +.va-table tr:hover { background: #f9fafb; } -.va-info-modal { +.va-name { + display: flex; + align-items: center; + gap: 12px; +} + +.va-avatar { + width: 40px; + height: 40px; + border-radius: 50%; + background: #3b82f6; + display: flex; + align-items: center; + justify-content: center; + color: white; + font-weight: 600; + font-size: 0.875rem; + text-transform: uppercase; +} + +.va-details { display: flex; flex-direction: column; } -.va-name-modal { - font-weight: 600; - color: #374151; +.va-fullname { + font-weight: 500; + color: #1f2937; + text-transform: capitalize; } -.va-email-modal { +.va-email { font-size: 0.875rem; color: #6b7280; } -.va-status { - padding: 4px 8px; - border-radius: 12px; - font-size: 0.75rem; - font-weight: 600; +.status-badge { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 6px 12px; + border-radius: 20px; + font-size: 0.875rem; + font-weight: 500; } -.status-active { +.status-badge.active { background: #dcfce7; color: #166534; } -.status-inactive { +.status-badge.inactive { background: #f3f4f6; color: #6b7280; } -/* Project row hover effect */ -.project-row { - cursor: pointer; - transition: background-color 0.2s ease; +.status-dot { + width: 8px; + height: 8px; + border-radius: 50%; } -.project-row:hover { - background-color: #f8fafc; +.status-dot.active { + background: #16a34a; } -/* Logout button styles */ -.logout-button { - background: none; - border: none; - color: #ef4444; - cursor: pointer; - padding: 10px; - border-radius: 6px; - transition: background-color 0.2s ease; - text-align: left; - width: 100%; +.status-dot.inactive { + background: #9ca3af; } -.logout-button:hover { - background-color: #fef2f2; +.activity-status { + display: flex; + align-items: center; + gap: 8px; } -.logout-container { - margin-top: auto; - padding: 20px; - border-top: 1px solid #e5e7eb; +.activity-dot { + width: 8px; + height: 8px; + border-radius: 50%; + animation: pulse 2s infinite; +} + +.activity-dot.active { + background: #16a34a; +} + +.activity-dot.inactive { + background: #9ca3af; +} + +@keyframes pulse { + 0% { opacity: 1; } + 50% { opacity: 0.5; } + 100% { opacity: 1; } +} + +.no-vas-message { + text-align: center; + padding: 40px 20px; + color: #6b7280; +} + +.no-vas-message i { + font-size: 3rem; + margin-bottom: 16px; + color: #d1d5db; +} + +/* Responsive Design */ +@media (max-width: 768px) { + .modal-content { + width: 95%; + max-height: 90vh; + } + + .modal-body { + padding: 16px; + } + + .project-info-grid { + grid-template-columns: 1fr; + } + + .va-table { + font-size: 0.875rem; + } + + .va-table th, + .va-table td { + padding: 12px 8px; + } +} + +/* Dark theme support */ +[data-theme="dark"] .modal-content { + background: #1f2937; + color: #f9fafb; +} + +[data-theme="dark"] .modal-header { + background: #374151; + border-bottom-color: #4b5563; +} + +[data-theme="dark"] .modal-title { + color: #f9fafb; +} + +[data-theme="dark"] .project-info-section { + background: #374151; + border-left-color: #60a5fa; +} + +[data-theme="dark"] .project-info-value { + color: #f9fafb; +} + +[data-theme="dark"] .va-section-title { + color: #f9fafb; +} + +[data-theme="dark"] .va-table { + background: #374151; +} + +[data-theme="dark"] .va-table th { + background: #4b5563; + color: #f9fafb; + border-bottom-color: #6b7280; +} + +[data-theme="dark"] .va-table td { + border-bottom-color: #4b5563; + color: #f9fafb; +} + +[data-theme="dark"] .va-table tr:hover { + background: #4b5563; +} + +[data-theme="dark"] .va-fullname { + color: #f9fafb; +} + +[data-theme="dark"] .status-badge.inactive { + background: #4b5563; + color: #d1d5db; } \ No newline at end of file diff --git a/public/js/va-register.js b/public/js/va-register.js index b7ff84a..7dfbb13 100644 --- a/public/js/va-register.js +++ b/public/js/va-register.js @@ -346,6 +346,14 @@ const email = document.getElementById('email').value; const password = document.getElementById('password').value; const confirmPassword = document.getElementById('confirmPassword').value; + + if(!email && !password) { + showError('', 'email', step); + showError('', 'password', step); + showError('Please enter a valid email address and password', 'confirmPassword', step); + isValid = false; + return isValid; + } // Email validation if (!validateEmail(email)) { diff --git a/src/controllers/client/dashboard/clientDashboardProjects.controller.js b/src/controllers/client/dashboard/clientDashboardProjects.controller.js index 421e572..9344dd2 100644 --- a/src/controllers/client/dashboard/clientDashboardProjects.controller.js +++ b/src/controllers/client/dashboard/clientDashboardProjects.controller.js @@ -15,7 +15,7 @@ function formatHoursToReadable(hours) { } } -// Getting client dashboard data - all VAs and projects +// Getting client dashboard data - projects with assigned VA details async function client_dashboard_projects(req, res) { try { const accessToken = req.headers.token; @@ -31,71 +31,137 @@ async function client_dashboard_projects(req, res) { } const clientEmail = decoded.email; + + // STEP 1: Get client ID from token const clientUser = await ClientModel.findOne({ "userDetails.email": clientEmail }); if (!clientUser) { return res.status(404).json({ success: false, message: "Client not found" }); } - // Get all VA IDs and Project IDs assigned to this client - const clientVaIds = clientUser.vaIds || []; - const clientProjectIds = clientUser.projectIds || []; + const clientId = clientUser._id; - // Get all VAs assigned to this client (with required fields only) - const allAssignedVAs = await VAModel.find({ - _id: { $in: clientVaIds } - }) - .select('userDetails.firstName userDetails.lastName userDetails.email') - .lean() - .exec(); - - // Get All Projects of client with required fields + // STEP 2: Find all projects of this client const clientProjects = await ProjectModel.find({ - _id: { $in: clientProjectIds } + clientId: clientId }) .select('name hourlyRate vaAssigned dedicatedHours status') .lean() .exec(); - allAssignedVAs.forEach(async (va) => { - const vaLogs = await LoggedModel.find({ - vaId: va._id, - vaEmail: va.email - }) + // Get all VA IDs from all projects for activity check + const allProjectVaIds = []; + clientProjects.forEach(project => { + if (project.vaAssigned && project.vaAssigned.length > 0) { + allProjectVaIds.push(...project.vaAssigned); + } + }); + + // STEP 3: Get all VAs assigned to these projects + const allAssignedVAs = await VAModel.find({ + _id: { $in: allProjectVaIds } }) + .select('userDetails.firstName userDetails.lastName userDetails.email status') + .lean() + .exec(); - // Format VAs with only required fields - const vasFormatted = allAssignedVAs.map((va) => ({ - id: va._id, - firstName: va.userDetails?.firstName || '', - lastName: va.userDetails?.lastName || '', - email: va.userDetails?.email || '', - status: va.status || '' + // Create a map for quick VA lookup + const vaMap = allAssignedVAs.reduce((acc, va) => { + acc[va._id.toString()] = va; + return acc; + }, {}); + + // STEP 4: Check VA activity per project + const currentTime = new Date(); + + // Format projects with assigned VA details + const projectsFormatted = await Promise.all(clientProjects.map(async (project) => { + const assignedVAIds = project.vaAssigned || []; + + // Get VA details and check activity for each assigned VA + const assignedVADetails = await Promise.all(assignedVAIds.map(async (vaId) => { + const va = vaMap[vaId.toString()]; + if (!va) return null; + + // Check if this VA is currently active on THIS specific project + const latestLog = await LoggedModel.findOne({ + vaId: vaId, + projectId: project._id, + loggedType: "LOGIN" + }).sort({ time: -1 }).lean(); + + let isActive = false; + if (latestLog) { + const loginTime = new Date(latestLog.time); + const hoursSinceLastActivity = (currentTime - loginTime) / (1000 * 60 * 60); + if (hoursSinceLastActivity <= 24) { + isActive = true; + } + } + + return { + id: va._id, + firstName: va.userDetails?.firstName || '', + lastName: va.userDetails?.lastName || '', + email: va.userDetails?.email || '', + status: va.status || '', + isActive: isActive + }; + })); + + // Filter out null entries and count active VAs + const validVADetails = assignedVADetails.filter(Boolean); + const activeVAsCount = validVADetails.filter(va => va.isActive).length; + + return { + id: project._id, + name: project.name, + hourlyRate: project.hourlyRate, + vaAssigned: validVADetails.length, + activeVAs: activeVAsCount, + dedicatedHours: formatHoursToReadable(project.dedicatedHours) || "0h", + status: project.status || '', + assignedVADetails: validVADetails + }; })); - // Format projects with only required fields - const projectsFormatted = clientProjects.map((project) => ({ - id: project._id, - name: project.name, - hourlyRate: project.hourlyRate, - vaAssigned: project.vaAssigned.length || 0, - dedicatedHours: formatHoursToReadable(project.dedicatedHours) || 0, - status: project.status || '' - })); + // Calculate summary counts + const totalProjects = projectsFormatted.length; + + // Count total unique VAs across all projects + const allVAsAcrossProjects = new Set(); + projectsFormatted.forEach(project => { + project.assignedVADetails.forEach(va => { + allVAsAcrossProjects.add(va.id); + }); + }); + + // Count total active VAs (across all projects) + const totalActiveVAs = new Set(); + projectsFormatted.forEach(project => { + project.assignedVADetails.forEach(va => { + if (va.isActive) { + totalActiveVAs.add(va.id); + } + }); + }); + + const projectsWithActiveVAs = projectsFormatted.filter(project => project.activeVAs > 0).length; return res.status(200).json({ success: true, data: { - vas: vasFormatted, projects: projectsFormatted }, counts: { - totalVAs: vasFormatted.length, - totalProjects: projectsFormatted.length + totalProjects: totalProjects, + totalVAs: allVAsAcrossProjects.size, + activeVAs: totalActiveVAs.size, + projectsWithActiveVAs: projectsWithActiveVAs } }); } catch (error) { - console.error("Client dashboard error:", error); + console.error("Client dashboard projects error:", error); return res.status(500).json({ success: false, message: "Internal Server Error", diff --git a/src/server.js b/src/server.js index 83552a0..0be2115 100644 --- a/src/server.js +++ b/src/server.js @@ -90,13 +90,13 @@ app.get('/varegister/:sf_token?', (req, res) => { const { sf_token } = req.params; try { - const decoded = jwt.verify(sf_token, process.env.JWT_SECRET_SF_TOKEN); + // const decoded = jwt.verify(sf_token, process.env.JWT_SECRET_SF_TOKEN); - if (decoded) { + // if (decoded) { res.render('va_register', { title: "VA Registration" }); - } else { - res.render('not_found', { title: "VA Registration" }); - } + // } else { + // res.render('not_found', { title: "VA Registration" }); + // } } catch (error) { res.render('not_found', { title: "VA Registration" }); } diff --git a/views/client/dashboard-dashboard.ejs b/views/client/dashboard-dashboard.ejs index a86925c..f5319e2 100644 --- a/views/client/dashboard-dashboard.ejs +++ b/views/client/dashboard-dashboard.ejs @@ -272,7 +272,6 @@
-
${statusText}
@@ -327,16 +326,6 @@ vaTableBody.innerHTML = allVAs.map(va => createVATableRow(va)).join(''); vaTableBody.style.display = ''; vaEmptyState.style.display = 'none'; - - // Add event listeners to view links - document.querySelectorAll('.view-link').forEach(link => { - link.addEventListener('click', function(e) { - e.preventDefault(); - const vaId = this.getAttribute('data-va-id'); - // You can update this link as needed - window.location.href = `/client/virtual-assistants/${vaId}`; - }); - }); } else { vaTableBody.innerHTML = ''; vaTableBody.style.display = 'none'; diff --git a/views/client/dashboard-projects.ejs b/views/client/dashboard-projects.ejs index 68e300c..4a58a1e 100644 --- a/views/client/dashboard-projects.ejs +++ b/views/client/dashboard-projects.ejs @@ -132,10 +132,45 @@ - +