diff --git a/package-lock.json b/package-lock.json index ea79ccf..f47863f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,6 +20,7 @@ "dotenv": "^16.4.7", "ejs": "^3.1.10", "express": "^4.19.2", + "express-ejs-layouts": "^2.5.1", "express-minify": "^1.0.0", "helmet": "^8.1.0", "html-minifier": "^4.0.0", @@ -148,6 +149,7 @@ "integrity": "sha512-i1SLeK+DzNnQ3LL/CswPCa/E5u4lh1k6IAEphON8F+cXt0t9euTshDru0q7/IqMa1PMPz5RnHuHscF8/ZJsStg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.26.0", @@ -2359,6 +2361,7 @@ "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -2903,6 +2906,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001688", "electron-to-chromium": "^1.5.73", @@ -3740,7 +3744,8 @@ "version": "0.0.1452169", "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1452169.tgz", "integrity": "sha512-FOFDVMGrAUNp0dDKsAU1TorWJUx2JOU1k9xdgBKKJF3IBh/Uhl2yswG5r3TEAOrCiGY2QRp1e6LVDQrCsTKO4g==", - "license": "BSD-3-Clause" + "license": "BSD-3-Clause", + "peer": true }, "node_modules/dom-serializer": { "version": "0.1.1", @@ -4231,6 +4236,11 @@ "node": ">= 0.10.0" } }, + "node_modules/express-ejs-layouts": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/express-ejs-layouts/-/express-ejs-layouts-2.5.1.tgz", + "integrity": "sha512-IXROv9n3xKga7FowT06n1Qn927JR8ZWDn5Dc9CJQoiiaaDqbhW5PDmWShzbpAa2wjWT1vJqaIM1S6vJwwX11gA==" + }, "node_modules/express-minify": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/express-minify/-/express-minify-1.0.0.tgz", @@ -7986,6 +7996,7 @@ "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -8369,6 +8380,7 @@ "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.5.tgz", "integrity": "sha512-iF+tNDQla22geJdTyJB1wM/qrX9DMRwWrciEPwWLPRWAUEM8sQiyxgckLxWT1f7+9VabJS0jTGGr4QgBuvi6Ww==", "license": "MIT", + "peer": true, "dependencies": { "ip-address": "^9.0.5", "smart-buffer": "^4.2.0" @@ -9077,6 +9089,7 @@ "integrity": "sha512-EksG6gFY3L1eFMROS/7Wzgrii5mBAFe4rIr3r2BTfo7bcc+DWwFZ4OJ/miOuHJO/A85HwyI4eQ0F6IKXesO7Fg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.6", @@ -9124,6 +9137,7 @@ "integrity": "sha512-MfwFQ6SfwinsUVi0rNJm7rHZ31GyTcpVE5pgVA3hwFRb7COD4TzjUUwhGWKfO50+xdc2MQPuEBBJoqIMGt3JDw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@discoveryjs/json-ext": "^0.6.1", "@webpack-cli/configtest": "^3.0.1", @@ -9289,6 +9303,7 @@ "resolved": "https://registry.npmjs.org/winston/-/winston-3.17.0.tgz", "integrity": "sha512-DLiFIXYC5fMPxaRg832S6F5mJYvePtmO5G9v9IgUFPhXm9/GkXarH/TUrBAVzhTCzAj9anE/+GjrgXp/54nOgw==", "license": "MIT", + "peer": true, "dependencies": { "@colors/colors": "^1.6.0", "@dabh/diagnostics": "^2.0.2", diff --git a/package.json b/package.json index 846e8c2..acfe04f 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,7 @@ "scripts": { "start": "nodemon --inspect src/server.js", "test": "echo \"Error: no test specified\" && exit 1", + "deploy": "curl -X POST http://35.162.222.18:9000/hooks/deploy-va-middleware", "build": "npx webpack --mode production", "dev": "npx webpack --mode development" }, @@ -24,6 +25,7 @@ "dotenv": "^16.4.7", "ejs": "^3.1.10", "express": "^4.19.2", + "express-ejs-layouts": "^2.5.1", "express-minify": "^1.0.0", "helmet": "^8.1.0", "html-minifier": "^4.0.0", diff --git a/public/css/dashboard.css b/public/css/dashboard.css index 62e108d..95139cc 100644 --- a/public/css/dashboard.css +++ b/public/css/dashboard.css @@ -15,7 +15,7 @@ --color-gray-700: #374151; --color-gray-800: #1f2937; --color-gray-900: #111827; - + --bg-primary: #ffffff; --bg-secondary: #f8fafc; --bg-card: #ffffff; @@ -213,6 +213,7 @@ body { transform: translateX(-100%); transition: .3s ease; } + .dashboard-sidebar.active { transform: translateX(0); } @@ -260,6 +261,54 @@ body { color: var(--text-primary); } +/* ===== FIX DUPLICATE SELECT ICON ISSUE ===== */ +select, +.select-field { + appearance: none !important; + -webkit-appearance: none !important; + -moz-appearance: none !important; + + background-image: none !important; + background-repeat: no-repeat !important; + background-position: unset !important; +} + +/* Custom select wrapper */ +.select-wrapper { + position: relative; + width: 100%; +} + +.void-btn-a { + background: tomato; + text-decoration: none; + color: #ffffff !important; + border: none; + padding: 0.5rem 1rem; + border-radius: 5px; + font-weight: bold; + text-decoration: none; + cursor: pointer; +} + +/* Add spacing for icon */ +.select-wrapper select { + width: 100%; + padding-right: 44px; +} + +/* Single dropdown icon */ +.select-icon { + position: absolute; + right: 14px; + top: 50%; + transform: translateY(-50%); + pointer-events: none; + color: #aaa; + font-size: 14px; +} + + [data-theme="dark"] .sidebar-link:hover { background-color: var(--color-gray-800); } @@ -699,6 +748,7 @@ body { opacity: 0; transform: scale(0.9) translateY(-20px); } + to { opacity: 1; transform: scale(1) translateY(0); @@ -944,17 +994,17 @@ tr[data-vaid]:hover { 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; } @@ -1215,9 +1265,17 @@ tr[data-vaid]:hover { } @keyframes pulse { - 0% { opacity: 1; } - 50% { opacity: 0.5; } - 100% { opacity: 1; } + 0% { + opacity: 1; + } + + 50% { + opacity: 0.5; + } + + 100% { + opacity: 1; + } } .no-vas-message { @@ -1615,11 +1673,11 @@ tr[data-vaid]:hover { .form-grid { grid-template-columns: 1fr; } - + .modal-actions { flex-direction: column; } - + .logout-actions { flex-direction: column; } @@ -1627,41 +1685,49 @@ tr[data-vaid]:hover { /* Additional Dark Mode Styles for Elements Not Covered */ [data-theme="dark"] { + /* Ensure all text colors are properly set */ - h1, h2, h3, h4, h5, h6 { + h1, + h2, + h3, + h4, + h5, + h6 { color: var(--text-primary); } - - p, span, div { + + p, + span, + div { color: var(--text-primary); } - + /* Form elements */ input::placeholder, textarea::placeholder { color: var(--text-muted); } - + /* Selection color */ ::selection { background: rgba(59, 130, 246, 0.3); color: var(--text-primary); } - + /* Scrollbar styling */ ::-webkit-scrollbar { width: 8px; } - + ::-webkit-scrollbar-track { background: var(--bg-secondary); } - + ::-webkit-scrollbar-thumb { background: var(--color-gray-600); border-radius: 4px; } - + ::-webkit-scrollbar-thumb:hover { background: var(--color-gray-500); } @@ -1688,4 +1754,187 @@ tr[data-vaid]:hover { [data-theme="dark"] .loading { opacity: 0.5; +} + +.va-action { + text-decoration: none; + color: #fff; + background-color: #2563eb; + padding: 0.5rem 1rem; + text-align: center; + border-radius: 5px; + transform: scale(0.96); + transition: all 0.2s ease-in-out; + cursor: pointer; +} + +.va-info { + cursor: pointer; +} + +.va-action:hover { + transform: scale(1); +} + + +/* ================================ + PROFILE DROPDOWN (GLASS STYLE) +================================ */ +.header-profile { + position: relative; + display: flex; + align-items: center; +} + +.profile-trigger { + display: flex; + align-items: center; + gap: 0.5rem; + + padding: 0.70rem 0.75rem; + + border-radius: 999px; + + cursor: pointer; + + /* NEW */ + border: 1px solid var(--border-color); + background: linear-gradient(135deg, rgba(255, 0, 150, 0.3), rgba(0, 204, 255, 0.3)); + + transition: + background-color 0.2s ease, + border-color 0.2s ease, + box-shadow 0.2s ease; +} + +/* Hover effect */ +.profile-trigger:hover { + background-color: rgba(0, 0, 0, 0.06); + border-color: var(--primary-color); +} + +/* Active (clicked/open) state */ +.profile-trigger.active { + background-color: rgba(0, 123, 255, 0.1); + border-color: var(--primary-color); + box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.15); +} + + +[data-theme="dark"] .profile-trigger:hover { + background-color: rgba(255, 255, 255, 0.08); +} + +/* Avatar */ +.profile-avatar { + width: 36px; + height: 36px; + border-radius: 50%; + object-fit: cover; + border: 2px solid #3b82f6; +} + +/* Name */ +.profile-name { + font-size: 0.875rem; + font-weight: 500; + color: var(--text-color, #111827); +} + +[data-theme="dark"] .profile-name { + color: #e5e7eb; +} + +.profile-trigger i { + font-size: 0.7rem; + opacity: 0.7; +} + +/* ================================ + DROPDOWN MENU +================================ */ +.profile-menu { + position: absolute; + top: calc(100% + 0.45rem); + right: 0; + width: 240px; + border-radius: 12px; + backdrop-filter: blur(16px); + background: rgba(255, 255, 255, 0.85); + border: 1px solid var(--border-color); + box-shadow: 0 20px 40px rgba(0, 0, 0, 0.15); + display: none; + flex-direction: column; + overflow: hidden; + z-index: 50; +} + +[data-theme="dark"] .profile-menu { + background: rgba(30, 30, 30, 0.9); +} + +/* Show menu on hover */ +.profile-menu { + display: none; +} + +.profile-menu.active { + display: flex; +} + + +/* Menu items */ +.profile-menu a, +.logout-btn { + padding: 0.75rem 1rem; + display: flex; + align-items: center; + gap: 0.75rem; + font-size: 0.875rem; + color: var(--text-color, #111827); + background: none; + border: none; + cursor: pointer; + text-decoration: none; + transition: background-color 0.15s ease; +} + +.profile-menu a:hover, +.logout-btn:hover { + background-color: rgba(0, 0, 0, 0.05); +} + +[data-theme="dark"] .profile-menu a, +[data-theme="dark"] .logout-btn { + color: #e5e7eb; +} + +[data-theme="dark"] .profile-menu a:hover, +[data-theme="dark"] .logout-btn:hover { + background-color: rgba(255, 255, 255, 0.08); +} + +/* Divider */ +.menu-divider { + height: 1px; + background: var(--border-color); + margin: 0.25rem 0; +} + +/* Logout */ +.logout-btn { + color: #dc2626; +} + +.logout-btn i { + color: #dc2626; +} + +/* ================================ + RESPONSIVE +================================ */ +@media (max-width: 768px) { + .profile-name { + display: none; + } } \ No newline at end of file diff --git a/public/images/favicon.ico b/public/images/favicon.ico new file mode 100644 index 0000000..582df3a Binary files /dev/null and b/public/images/favicon.ico differ diff --git a/public/images/favicon.svg b/public/images/favicon.svg new file mode 100644 index 0000000..feb5a41 --- /dev/null +++ b/public/images/favicon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/js/common/auth.js b/public/js/common/auth.js new file mode 100644 index 0000000..825bc31 --- /dev/null +++ b/public/js/common/auth.js @@ -0,0 +1,29 @@ +window.getAuthToken = function () { + return localStorage.getItem("knoptpq"); +}; + +window.fetchWithAuth = async function (url, options = {}) { + const token = getAuthToken(); + + if (!token) { + window.location.href = "/client/login"; + return; + } + + const res = await fetch(url, { + ...options, + headers: { + token, + "Content-Type": "application/json", + ...options.headers, + }, + }); + + if (res.status === 401) { + localStorage.clear(); + window.location.href = "/client/login"; + return; + } + + return res; +}; diff --git a/public/js/common/header.js b/public/js/common/header.js new file mode 100644 index 0000000..7939feb --- /dev/null +++ b/public/js/common/header.js @@ -0,0 +1,29 @@ +document.addEventListener('DOMContentLoaded', () => { + const profileTrigger = document.querySelector('.profile-trigger'); + const profileMenu = document.querySelector('.profile-menu'); + + if (!profileTrigger || !profileMenu) return; + + // Toggle on click + profileTrigger.addEventListener('click', (e) => { + e.stopPropagation(); + profileMenu.classList.toggle('active'); + }); + + // Close on outside click + document.addEventListener('click', () => { + profileMenu.classList.remove('active'); + }); + + // Prevent closing when clicking inside menu + profileMenu.addEventListener('click', (e) => { + e.stopPropagation(); + }); + + // Close on ESC + document.addEventListener('keydown', (e) => { + if (e.key === 'Escape') { + profileMenu.classList.remove('active'); + } + }); +}); \ No newline at end of file diff --git a/public/js/common/logout.js b/public/js/common/logout.js new file mode 100644 index 0000000..76289c7 --- /dev/null +++ b/public/js/common/logout.js @@ -0,0 +1,23 @@ +function openLogoutModal() { + document.getElementById("logoutModal").style.display = "flex"; + document.body.style.overflow = "hidden"; +} + +function closeLogoutModal() { + document.getElementById("logoutModal").style.display = "none"; + document.body.style.overflow = "auto"; +} + +async function handleLogout() { + try { + await fetchWithAuth("/api/v1/client/logout", { method: "POST" }); + } finally { + localStorage.clear(); + window.location.href = "/client/login"; + } +} + +document.addEventListener("DOMContentLoaded", () => { + document.getElementById("logoutButton")?.addEventListener("click", openLogoutModal); + document.getElementById("confirmLogoutBtn")?.addEventListener("click", handleLogout); +}); diff --git a/public/js/common/sidebar.js b/public/js/common/sidebar.js new file mode 100644 index 0000000..c6922e5 --- /dev/null +++ b/public/js/common/sidebar.js @@ -0,0 +1,90 @@ +const currentPath = window.location.pathname; +document.querySelectorAll('.nav-button').forEach(link => { + if (link.getAttribute('href') === currentPath) { + link.classList.add('active'); + } else { + link.classList.remove('active'); + } + }); + + + document.querySelectorAll('.sidebar-link').forEach(link => { + if (link.getAttribute('href') === currentPath) { + link.classList.add('active'); + } else { + link.classList.remove('active'); + } + }); + +// Auth helpers +function getAuthToken() { + return localStorage.getItem("knoptpq"); +} + +async function fetchWithAuth(url, options = {}) { + const token = getAuthToken(); + if (!token) { + window.location.href = "/client/login"; + return; + } + + const res = await fetch(url, { + ...options, + headers: { + "token": token, + "Content-Type": "application/json", + ...options.headers, + } + }); + + if (res.status === 401) { + localStorage.clear(); + window.location.href = "/client/login"; + return; + } + + return res; +} + +// Logout modal +function openLogoutModal() { + document.getElementById("logoutModal").style.display = "flex"; + document.body.style.overflow = "hidden"; +} + +function closeLogoutModal() { + document.getElementById("logoutModal").style.display = "none"; + document.body.style.overflow = "auto"; +} + +// Final logout +async function handleLogout() { + try { + await fetchWithAuth("/api/v1/client/logout", { method: "POST" }); + } catch (e) { + console.error("Logout error", e); + } finally { + localStorage.clear(); + window.location.href = "/client/login"; + } +} + +// DOM bindings +document.addEventListener("DOMContentLoaded", () => { + + // Logout button + const logoutBtn = document.getElementById("logoutButton"); + if (logoutBtn) logoutBtn.addEventListener("click", openLogoutModal); + + // Confirm logout + const confirmBtn = document.getElementById("confirmLogoutBtn"); + if (confirmBtn) confirmBtn.addEventListener("click", handleLogout); + + // Click outside modal + const modal = document.getElementById("logoutModal"); + if (modal) { + modal.addEventListener("click", (e) => { + if (e.target === modal) closeLogoutModal(); + }); + } +}); diff --git a/public/js/common/theme.js b/public/js/common/theme.js new file mode 100644 index 0000000..554ff85 --- /dev/null +++ b/public/js/common/theme.js @@ -0,0 +1,24 @@ +(function () { + const root = document.documentElement; + + function applyTheme(theme) { + root.setAttribute("data-theme", theme); + localStorage.setItem("theme", theme); + } + + function getSystemTheme() { + return window.matchMedia("(prefers-color-scheme: dark)").matches + ? "dark" + : "light"; + } + + function initTheme() { + const savedTheme = localStorage.getItem("theme"); + applyTheme(savedTheme || getSystemTheme()); + } + + // expose globally + window.setTheme = applyTheme; + + document.addEventListener("DOMContentLoaded", initTheme); +})(); diff --git a/public/js/pages/dashboard-virtual-assistants.js b/public/js/pages/dashboard-virtual-assistants.js new file mode 100644 index 0000000..9a702c1 --- /dev/null +++ b/public/js/pages/dashboard-virtual-assistants.js @@ -0,0 +1,263 @@ +const API_BASE = "/api/v1/client"; + +// Format time function +function formatTime(seconds) { + if (!seconds) return 'No Record Found'; + + const hours = Math.floor(seconds / 3600); + const minutes = Math.floor((seconds % 3600) / 60); + + if (hours > 0) { + return `${hours}h ${minutes}m`; + } else if (minutes > 0) { + return `${minutes}m`; + } else { + return 'Just started'; + } +} + +const vaModal = document.getElementById('vaModal'); + +function openVAModal(va) { + // Set basic VA info + document.getElementById('modalVAName').textContent = va.name; + + // Set projects + const projects = va.projects || []; + document.getElementById('projectsCountBadge').textContent = projects.length; + + const projectsTableBody = document.getElementById('modalProjectsTableBody'); + const noProjectsMessage = document.getElementById('modalNoProjects'); + + if (projects.length > 0) { + projectsTableBody.innerHTML = projects.map(project => createProjectTableRow(project)).join(''); + projectsTableBody.style.display = ''; + noProjectsMessage.style.display = 'none'; + } else { + projectsTableBody.innerHTML = ''; + projectsTableBody.style.display = 'none'; + noProjectsMessage.style.display = 'block'; + } + + // Set action button + const modalActions = document.getElementById('modalActions'); + const viewSessionBtn = document.getElementById('viewSessionBtn'); + + if (va.isCurrentlyActive && va.lastProjectId) { + viewSessionBtn.href = `/live/${va.lastProjectId}/${va.id}`; + modalActions.style.display = 'block'; + } else { + modalActions.style.display = 'none'; + } + + // Show modal + vaModal.style.display = 'flex'; + document.body.style.overflow = 'hidden'; +} + +function closeVAModal() { + vaModal.style.display = 'none'; + document.body.style.overflow = 'auto'; +} + +// Create project table row for modal +function createProjectTableRow(project) { + const statusClass = project.isCurrentlyWorking ? 'active' : 'inactive'; + const statusText = project.isCurrentlyWorking ? 'Working' : 'Not Active'; + + return ` + + +
+
${project.name}
+
+ + + $${project.hourlyRate}/hr + + + ${project.totalWorkedTime} + + +
+ ${statusText} +
+ + + `; +} + +// Create VA table row for main table +function createVATableRow(va) { + const statusClass = va.isCurrentlyActive ? 'active' : 'inactive'; + const statusText = va.isCurrentlyActive ? 'Active' : 'Inactive'; + const activeProject = va?.lastProject?.name ? va.lastProject.name : '-'; + const workedTime = formatTime(va.currentSessionDuration); + const projectsAssigned = va.projectsAssigned || 0; + const viewLink = va.isCurrentlyActive ? `Live Tracking` : '

Logs

'; + + return ` + + +
+ ${statusText} +
+ + +
+
${va.name}
+
${va.email || 'N/A'}
+
+ + + ${activeProject} + + + ${projectsAssigned} Projects + + + ${workedTime} + + + ${viewLink} + + + `; +} + +// Load dashboard data +async function loadDashboardData() { + try { + const response = await fetchWithAuth(`${API_BASE}/dashboard`); + if (!response) return; + const result = await response.json(); + + if (result.success) { + const data = result.data; + const summary = result.summary; + + // Update stats cards + document.getElementById('activeVaCount').textContent = summary.activeVAs || 0; + document.getElementById('totalVaCount').textContent = summary.totalVAs || 0; + document.getElementById('totalProjects').textContent = summary.totalProjects || 0; + + // Update VA list count + document.getElementById('vaListCount').textContent = `${summary.totalVAs || 0} VAs`; + + // Update VA table + const vaTableBody = document.getElementById('vaTableBody'); + const vaEmptyState = document.getElementById('vaEmptyState'); + const allVAs = data.allVAs || []; + + if (allVAs.length > 0) { + vaTableBody.innerHTML = allVAs.map(va => createVATableRow(va)).join(''); + vaTableBody.style.display = ''; + vaEmptyState.style.display = 'none'; + + // Store VA data globally for modal access + window.vaData = allVAs; + + // Add click event listeners to VA rows + document.querySelectorAll('.va-row').forEach((row, index) => { + row.addEventListener('click', function (e) { + // Don't open modal if clicking on view link + if (!e.target.closest('.view-link')) { + const va = allVAs[index]; + openVAModal(va); + } + }); + }); + } else { + vaTableBody.innerHTML = ''; + vaTableBody.style.display = 'none'; + vaEmptyState.style.display = 'block'; + } + + } else { + console.error('Failed to load dashboard data:', result.message); + showError('Failed to load dashboard data'); + } + + } catch (error) { + console.error('Error loading dashboard data:', error); + showError('Error loading dashboard data'); + } +} + +// Show error message +function showError(message) { + const vaTableBody = document.getElementById('vaTableBody'); + const vaEmptyState = document.getElementById('vaEmptyState'); + + vaTableBody.style.display = 'none'; + vaEmptyState.innerHTML = ` +
+ +

Error

+

${message}

+
+ `; + vaEmptyState.style.display = 'block'; +} + +// Close modal when clicking outside +vaModal.addEventListener('click', (e) => { + if (e.target === vaModal) { + closeVAModal(); + } +}); + +// Close modal with escape key +document.addEventListener('keydown', (e) => { + if (e.key === 'Escape' && vaModal.style.display === 'flex') { + closeVAModal(); + } +}); + +// Optional: Auto-refresh data every 30 seconds +setInterval(loadDashboardData, 30000); + +// Logout Modal Functions +function openLogoutModal() { + const modal = document.getElementById('logoutModal'); + modal.style.display = 'flex'; + document.body.style.overflow = 'hidden'; +} + +function closeLogoutModal() { + const modal = document.getElementById('logoutModal'); + modal.style.display = 'none'; + document.body.style.overflow = 'auto'; +} + +document.addEventListener('DOMContentLoaded', function () { + // Load dashboard data + loadDashboardData(); + + // Mobile menu toggle + const menuToggle = document.querySelector('.mobile-menu-button'); + const sidebar = document.querySelector('.dashboard-sidebar'); + menuToggle.addEventListener('click', () => { + sidebar.classList.toggle('active'); + }); + + // Close modal with escape key + document.addEventListener('keydown', function (e) { + if (e.key === 'Escape') { + closeLogoutModal(); + } + }); + + document.getElementById('logoutModal').addEventListener('click', function (e) { + if (e.target === this) { + closeLogoutModal(); + } + }); + + // Logout button - opens confirmation modal instead of directly logging out + document.getElementById('logoutButton').addEventListener('click', openLogoutModal); + + // Confirm logout button + document.getElementById('confirmLogoutBtn').addEventListener('click', handleLogout); +}); + diff --git a/public/js/pages/dashboard.js b/public/js/pages/dashboard.js new file mode 100644 index 0000000..38ae32c --- /dev/null +++ b/public/js/pages/dashboard.js @@ -0,0 +1,142 @@ +const API_BASE = "/api/v1/client"; + +/* ------------------ Utils ------------------ */ + +function formatTime(seconds) { + if (!seconds) return "No Record"; + const h = Math.floor(seconds / 3600); + const m = Math.floor((seconds % 3600) / 60); + return h ? `${h}h ${m}m` : `${m}m`; +} + +/* ------------------ DOM Elements ------------------ */ + +const vaModal = document.getElementById("vaModal"); +const modalVAName = document.getElementById("modalVAName"); +const modalProjectsTableBody = document.getElementById("modalProjectsTableBody"); +const modalNoProjects = document.getElementById("modalNoProjects"); + +const vaTableBody = document.getElementById("vaTableBody"); +const vaEmptyState = document.getElementById("vaEmptyState"); + +/* ------------------ Modal Controls ------------------ */ + +function openVAModal(va) { + modalVAName.textContent = va.name || "VA Details"; + + const projects = va.projects || []; + + if (projects.length === 0) { + modalProjectsTableBody.innerHTML = ""; + modalNoProjects.style.display = "block"; + } else { + modalNoProjects.style.display = "none"; + modalProjectsTableBody.innerHTML = projects + .map(createProjectRow) + .join(""); + } + + vaModal.style.display = "flex"; + document.body.style.overflow = "hidden"; +} + +function closeVAModal() { + vaModal.style.display = "none"; + document.body.style.overflow = "auto"; +} + +/* Outside click close */ +vaModal.addEventListener("click", (e) => { + if (e.target === vaModal) closeVAModal(); +}); + +/* ESC key close */ +document.addEventListener("keydown", (e) => { + if (e.key === "Escape") closeVAModal(); +}); + +/* ------------------ Table Builders ------------------ */ + +function createProjectRow(project) { + return ` + + ${project.name} + $${project.hourlyRate}/hr + ${project.totalWorkedTime} + ${project.isCurrentlyWorking ? "Working" : "Inactive"} + + `; +} + +function createVATableRow(va, index) { + return ` + + + + ${va.isCurrentlyActive ? "Active" : "Inactive"} + + + ${va.name} + ${va.lastProject?.name || "-"} + ${va.projectsAssigned || 0} + ${va.currentSessionDuration || "No Record Found"} + + ${ + va.isCurrentlyActive + ? `Live` + : "-" + } + + + `; +} + + + setInterval(loadDashboardData, 30000); + +/* ------------------ Load Dashboard ------------------ */ + +async function loadDashboardData() { + const res = await fetchWithAuth(`${API_BASE}/dashboard`); + if (!res) return; + + const result = await res.json(); + if (!result.success) return; + + const { summary, data } = result; + + document.getElementById("activeVaCount").textContent = summary.activeVAs; + document.getElementById("totalVaCount").textContent = summary.totalVAs; + document.getElementById("totalProjects").textContent = summary.totalProjects; + document.getElementById("vaListCount").textContent = `${summary.totalVAs} VAs`; + + const allVAs = data.allVAs || []; + + if (allVAs.length === 0) { + vaTableBody.innerHTML = ""; + vaEmptyState.style.display = "block"; + return; + } + + vaEmptyState.style.display = "none"; + + /* store globally for modal */ + window.allVAs = allVAs; + + vaTableBody.innerHTML = allVAs + .map((va, index) => createVATableRow(va, index)) + .join(""); + + /* Row click handlers */ + document.querySelectorAll(".va-row").forEach(row => { + row.addEventListener("click", (e) => { + if (e.target.tagName === "A") return; // ignore Live click + const index = row.dataset.index; + openVAModal(window.allVAs[index]); + }); + }); +} + +/* ------------------ Init ------------------ */ + +document.addEventListener("DOMContentLoaded", loadDashboardData); diff --git a/public/js/pages/manage-tips.js b/public/js/pages/manage-tips.js new file mode 100644 index 0000000..0017a55 --- /dev/null +++ b/public/js/pages/manage-tips.js @@ -0,0 +1,248 @@ + + + // API base URL + const API_BASE = '/api/v1/client'; + + + + function openTipModal() { + const modal = document.getElementById('tipModal'); + modal.style.display = 'flex'; + document.body.style.overflow = 'hidden'; + + // Clear form + document.getElementById('tipForm').reset(); + clearErrorMessages(); + } + + function closeTipModal() { + const modal = document.getElementById('tipModal'); + modal.style.display = 'none'; + document.body.style.overflow = 'auto'; + + // Clear form and errors + document.getElementById('tipForm').reset(); + clearErrorMessages(); + } + + function clearErrorMessages() { + document.getElementById('vaForTipError').textContent = ''; + document.getElementById('vaTipAmountError').textContent = ''; + document.getElementById('vaTipDescriptionError').textContent = ''; + } + + document.addEventListener('DOMContentLoaded', function () { + + // Mobile menu toggle + const menuToggle = document.querySelector('.mobile-menu-button'); + const sidebar = document.querySelector('.dashboard-sidebar'); + menuToggle.addEventListener('click', () => { + sidebar.classList.toggle('active'); + }); + + document.getElementById('tipVaAddBtn').addEventListener('click', openTipModal); + + document.getElementById('tipModal').addEventListener('click', function (e) { + if (e.target === this) { + closeTipModal(); + } + }); + + // Close modal with escape key + document.addEventListener('keydown', function (e) { + if (e.key === 'Escape') { + closeLogoutModal(); + closeTipModal(); + } + }); + + document.getElementById('logoutModal').addEventListener('click', function (e) { + if (e.target === this) { + closeLogoutModal(); + } + }); + + // Logout button - opens confirmation modal instead of directly logging out + document.getElementById('logoutButton').addEventListener('click', openLogoutModal); + + // Confirm logout button + document.getElementById('confirmLogoutBtn').addEventListener('click', handleLogout); + }); + + // ------------------------------ + // TIP FORM SUBMISSION HANDLER + // ------------------------------ + + document.getElementById('tipForm').addEventListener('submit', async function (e) { + e.preventDefault(); + + // clearErrorMessages(); + + const vaId = document.getElementById('vaForTip').value; + const amount = document.getElementById('vaTipAmount').value.trim(); + const description = document.getElementById('vaTipDescription').value.trim(); + + let isValid = true; + + // Validation + if (!vaId) { + document.getElementById('vaForTipError').textContent = "Please select a virtual assistant."; + isValid = false; + } + + if (!amount || isNaN(amount) || Number(amount) <= 0) { + document.getElementById('vaTipAmountError').textContent = "Enter a valid tip amount."; + isValid = false; + } + + if (!isValid) return; + + // Prepare API body + const payload = { + vaId, + amount: Number(amount), + description + }; + + try { + document.getElementById('submitTipBtn').disabled = true; + document.getElementById('submitTipBtn').textContent = "Submitting..."; + + const response = await fetchWithAuth(`/api/v1/client/tips`, { + method: "POST", + body: JSON.stringify(payload) + }); + + const data = await response.json(); + + if (!response.ok) { + alert(data.message || "Failed to submit tip."); + return; + } + + alert("Tip submitted successfully!"); + closeTipModal(); + + // Optionally refresh page or reload tips list + window.location.reload(); + + } catch (err) { + console.error("Tip submit error:", err); + alert("Something went wrong while submitting tip."); + } finally { + document.getElementById('submitTipBtn').disabled = false; + document.getElementById('submitTipBtn').textContent = "Submit Tip"; + } + }); + + + document.addEventListener('DOMContentLoaded', function () { + // Load VAs for tip selection + async function loadVAs() { + try { + const response = await fetchWithAuth(`${API_BASE}/dashboard`); + const data = await response.json(); + + if (!response.ok) { + console.error("Failed to load VAs:", data.message); + return; + } + + const vaSelect = document.getElementById('vaForTip'); + data.data.allVAs.forEach(va => { + const option = document.createElement('option'); + option.value = va.id; + option.textContent = va.name; + vaSelect.appendChild(option); + }); + + } catch (error) { + console.error("Error loading VAs:", error); + } + } + + loadVAs(); + }); + + document.addEventListener('DOMContentLoaded', function () { + // Load VAs for tip selection + async function loadTips() { + try { + const response = await fetchWithAuth(`${API_BASE}/tips`); + + if (!response.ok) { + console.error("Failed to load VAs:", data.message); + return; + } + const data = await response.json(); + + const vaSelect = document.getElementById('vaTipTableBody'); + data.data.forEach(tip => { + const row = document.createElement('tr'); + const cellVoid = document.createElement('td'); + const cellStatus = document.createElement('td'); + const cellName = document.createElement('td'); + const cellDescription = document.createElement('td'); + const cellDate = document.createElement('td'); + const cellAmount = document.createElement('td'); + + cellVoid.innerHTML = ``; + cellVoid.classList.add('void-btn'); + + cellStatus.textContent = tip.status; + cellName.textContent = tip.vaName; + cellDescription.textContent = tip.description; + cellDate.textContent = tip.createdAt ? new Date(tip.createdAt).toLocaleDateString() : ''; + cellAmount.textContent = `$${parseFloat(tip.amount).toFixed(2)}`; + + row.appendChild(cellVoid); + row.appendChild(cellStatus); + row.appendChild(cellName); + row.appendChild(cellDescription); + row.appendChild(cellDate); + row.appendChild(cellAmount); + + vaSelect.appendChild(row); + }); + + document.querySelectorAll('.void-btn-a').forEach(button => { + button.addEventListener('click', async function () { + const tipId = this.getAttribute('data-id'); + if (confirm('Are you sure you want to void this tip?')) { + try { + const response = await fetchWithAuth(`${API_BASE}/tips/${tipId}`, { + method: 'DELETE' + }); + const result = await response.json(); + + if (!response.ok) { + alert(result.message || 'Failed to void tip.'); + return; + } + + alert('Tip voided successfully!'); + window.location.reload(); + } catch (error) { + console.error('Error voiding tip:', error); + alert('Something went wrong while voiding the tip.'); + } + } + }); + }); + + } catch (error) { + console.error("Error loading VAs:", error); + } + } + + loadTips(); + + + + + + }); + + + + diff --git a/public/js/pages/profile.js b/public/js/pages/profile.js new file mode 100644 index 0000000..24b0ad1 --- /dev/null +++ b/public/js/pages/profile.js @@ -0,0 +1,195 @@ +// API base URL +const API_BASE = '/api/v1/client'; + + +// Format empty fields +function formatEmptyFields() { + const inputs = document.querySelectorAll('.form-input'); + inputs.forEach(input => { + if (!input.value.trim()) { + input.value = 'Not provided'; + input.classList.add('empty-field'); + } else { + input.classList.remove('empty-field'); + } + }); +} + +// Load profile data +async function loadProfileData() { + try { + const response = await fetchWithAuth(`${API_BASE}/profile`); + if (!response) return; + + const result = await response.json(); + if (result.success) { + const data = result.data; + + // Populate only personal and address information + const fieldsToShow = ['name', 'email', 'position', 'address', 'city', 'state', 'zip']; + + fieldsToShow.forEach(key => { + const element = document.getElementById(key); + if (element && data[key]) { + element.value = data[key]; + } + }); + + formatEmptyFields(); + + } else { + console.error('Failed to load profile data:', result.message); + alert('Failed to load profile data'); + } + } catch (error) { + console.error('Error loading profile data:', error); + alert('Error loading profile data'); + } +} + +// Password Modal Functions +function openPasswordModal() { + const modal = document.getElementById('passwordModal'); + modal.style.display = 'flex'; + document.body.style.overflow = 'hidden'; + + // Clear form + document.getElementById('passwordForm').reset(); + clearErrorMessages(); +} + +function closePasswordModal() { + const modal = document.getElementById('passwordModal'); + modal.style.display = 'none'; + document.body.style.overflow = 'auto'; + + // Clear form and errors + document.getElementById('passwordForm').reset(); + clearErrorMessages(); +} + +function clearErrorMessages() { + document.getElementById('currentPasswordError').textContent = ''; + document.getElementById('newPasswordError').textContent = ''; + document.getElementById('confirmPasswordError').textContent = ''; +} + +// Change password +async function changePassword(event) { + event.preventDefault(); + + const currentPassword = document.getElementById('currentPassword').value; + const newPassword = document.getElementById('newPassword').value; + const confirmPassword = document.getElementById('confirmPassword').value; + + // Reset errors + clearErrorMessages(); + + let hasError = false; + + if (!currentPassword) { + document.getElementById('currentPasswordError').textContent = 'Current password is required'; + hasError = true; + } + + if (!newPassword) { + document.getElementById('newPasswordError').textContent = 'New password is required'; + hasError = true; + } else if (newPassword.length < 6) { + document.getElementById('newPasswordError').textContent = 'Password must be at least 6 characters'; + hasError = true; + } + + if (!confirmPassword) { + document.getElementById('confirmPasswordError').textContent = 'Please confirm your password'; + hasError = true; + } else if (newPassword !== confirmPassword) { + document.getElementById('confirmPasswordError').textContent = 'Passwords do not match'; + hasError = true; + } + + if (hasError) return; + + try { + const saveBtn = document.getElementById('savePasswordBtn'); + saveBtn.disabled = true; + saveBtn.textContent = 'Changing Password...'; + + const response = await fetchWithAuth(`${API_BASE}/profile`, { + method: 'PATCH', + body: JSON.stringify({ + userDetails: { + currentPassword, + newPassword, + confirmPassword + } + }) + }); + + if (!response) { + saveBtn.disabled = false; + saveBtn.textContent = 'Change Password'; + return; + } + + const result = await response.json(); + if (result.success) { + alert('Password changed successfully!'); + closePasswordModal(); + } else { + alert(result.message || 'Failed to change password'); + } + } catch (error) { + console.error('Error changing password:', error); + alert('Error changing password'); + } finally { + const saveBtn = document.getElementById('savePasswordBtn'); + saveBtn.disabled = false; + saveBtn.textContent = 'Change Password'; + } +} + +// Event listeners +document.addEventListener('DOMContentLoaded', function () { + loadProfileData(); + + // Change password button + document.getElementById('changePasswordBtn').addEventListener('click', openPasswordModal); + + // Password form submission + document.getElementById('passwordForm').addEventListener('submit', changePassword); + + // Close modal when clicking outside + document.getElementById('passwordModal').addEventListener('click', function (e) { + if (e.target === this) { + closePasswordModal(); + } + }); + + // Close modal with escape key + document.addEventListener('keydown', function (e) { + if (e.key === 'Escape') { + closePasswordModal(); + closeLogoutModal(); + } + }); + + document.getElementById('logoutModal').addEventListener('click', function (e) { + if (e.target === this) { + closeLogoutModal(); + } + }); + + // Logout button - opens confirmation modal instead of directly logging out + document.getElementById('logoutButton').addEventListener('click', openLogoutModal); + + // Confirm logout button + document.getElementById('confirmLogoutBtn').addEventListener('click', handleLogout); + + // Mobile menu toggle + const menuToggle = document.querySelector('.mobile-menu-button'); + const sidebar = document.querySelector('.dashboard-sidebar'); + menuToggle.addEventListener('click', () => { + sidebar.classList.toggle('active'); + }); +}); \ No newline at end of file diff --git a/public/js/pages/projects.js b/public/js/pages/projects.js new file mode 100644 index 0000000..d26b0b6 --- /dev/null +++ b/public/js/pages/projects.js @@ -0,0 +1,240 @@ +// API base URL +const API_BASE = '/api/v1/client'; + +// Modal management +const projectModal = document.getElementById('projectModal'); +const modalClose = document.getElementById('modalClose'); +const modalProjectName = document.getElementById('modalProjectName'); +const modalVATableBody = document.getElementById('modalVATableBody'); +const modalNoVAs = document.getElementById('modalNoVAs'); +const vaCountBadge = document.getElementById('vaCountBadge'); + + +function openProjectModal(project) { + // Set project name + modalProjectName.textContent = project.name || 'Unnamed Project'; + + // Set project info + const hourlyRate = project.hourlyRate ? `$${project.hourlyRate}/hr` : 'Not set'; + const dedicatedHours = project.dedicatedHours || 'Not set'; + const statusText = project.status === 'ACTIVE' ? 'Active' : 'Inactive'; + const statusClass = project.status === 'ACTIVE' ? 'active' : 'inactive'; + + // Set VA details + const assignedVAs = project.assignedVADetails || []; + vaCountBadge.textContent = assignedVAs.length; + + if (assignedVAs.length > 0) { + modalVATableBody.innerHTML = assignedVAs.map(va => createVATableRow(va)).join(''); + modalVATableBody.style.display = ''; + modalNoVAs.style.display = 'none'; + } else { + modalVATableBody.innerHTML = ''; + modalVATableBody.style.display = 'none'; + modalNoVAs.style.display = 'block'; + } + + // Show modal + projectModal.style.display = 'block'; + document.body.style.overflow = 'hidden'; +} + +function createVATableRow(va) { + const statusClass = va.status === 'FULLTIME' ? 'active' : 'inactive'; + const statusText = va.status === 'FULLTIME' ? 'Full Time' : + va.status === 'PARTTIME' ? 'Part Time' : 'Inactive'; + + const activityClass = va.isActive ? 'active' : 'inactive'; + const activityText = va.isActive ? 'Currently Active' : 'Not Active'; + + // Get initials for avatar + const initials = (va.firstName?.charAt(0) || '') + (va.lastName?.charAt(0) || '') || 'VA'; + + return ` + + +
+
${initials}
+
+ ${va.firstName} ${va.lastName} + ${va.email} +
+
+ + +
+ ${statusText} +
+ + +
+
+ ${activityText} +
+ + + `; +} + +function closeProjectModal() { + projectModal.style.display = 'none'; + document.body.style.overflow = 'auto'; +} + +// Close modal when clicking outside +projectModal.addEventListener('click', (e) => { + if (e.target === projectModal) { + closeProjectModal(); + } +}); + +// Close modal with escape key +document.addEventListener('keydown', (e) => { + if (e.key === 'Escape' && projectModal.style.display === 'block') { + closeProjectModal(); + } +}); + +function createProjectTableRow(project) { + const statusClass = project.status === 'ACTIVE' ? 'active' : 'inactive'; + const statusText = project.status === 'ACTIVE' ? 'Active' : 'Inactive'; + const hourlyRate = project.hourlyRate ? `$${project.hourlyRate}/hr` : '-'; + const dedicatedHours = project.dedicatedHours ? `${project.dedicatedHours}` : '-'; + + return ` + + +
+
${project.name || 'Unnamed Project'}
+
+ + + ${hourlyRate} + + + ${project.vaAssigned || 0} + + + ${dedicatedHours} + + + `; +} + +// Load dashboard data +async function loadDashboardData() { + try { + const response = await fetchWithAuth(`${API_BASE}/dashboard-projects`); + if (!response) return; + + const result = await response.json(); + if (result.success) { + const data = result.data; + const counts = result.counts; + + // Update counts + document.getElementById('projectsCount').textContent = `${counts.totalProjects || 0} Projects`; + + // Update Projects table + const projectsTableBody = document.getElementById('projectsTableBody'); + const projectsEmptyState = document.getElementById('projectsEmptyState'); + const projects = data.projects || []; + + if (projects.length > 0) { + projectsTableBody.innerHTML = projects.map(project => createProjectTableRow(project)).join(''); + projectsTableBody.style.display = ''; + projectsEmptyState.style.display = 'none'; + + // Add click event listeners to project rows + document.querySelectorAll('.project-row').forEach((row, index) => { + row.addEventListener('click', function () { + const project = projects[index]; + openProjectModal(project); + }); + }); + } else { + projectsTableBody.innerHTML = ''; + projectsTableBody.style.display = 'none'; + projectsEmptyState.style.display = 'block'; + } + + } else { + console.error('Failed to load dashboard data:', result.message); + showError('Failed to load dashboard data'); + } + + } catch (error) { + console.error('Error loading dashboard data:', error); + showError('Error loading dashboard data'); + } +} + +// Show error message +function showError(message) { + const projectsTableBody = document.getElementById('projectsTableBody'); + const projectsEmptyState = document.getElementById('projectsEmptyState'); + const vaTableBody = document.getElementById('vaTableBody'); + const vaEmptyState = document.getElementById('vaEmptyState'); + + // Hide tables + projectsTableBody.style.display = 'none'; + vaTableBody.style.display = 'none'; + + // Show error states + projectsEmptyState.innerHTML = ` +
+ +

Error

+

${message}

+
+ `; + projectsEmptyState.style.display = 'block'; + + vaEmptyState.innerHTML = ` +
+ +

Error

+

${message}

+
+ `; + vaEmptyState.style.display = 'block'; +} + + +// Optional: Auto-refresh data every 30 seconds +setInterval(loadDashboardData, 30000); + + + +document.addEventListener('DOMContentLoaded', function () { + // Load dashboard data + loadDashboardData(); + + // Mobile menu toggle + const menuToggle = document.querySelector('.mobile-menu-button'); + const sidebar = document.querySelector('.dashboard-sidebar'); + menuToggle.addEventListener('click', () => { + sidebar.classList.toggle('active'); + }); + + // Close modal with escape key + document.addEventListener('keydown', function (e) { + if (e.key === 'Escape') { + closeLogoutModal(); + } + }); + + document.getElementById('logoutModal').addEventListener('click', function (e) { + if (e.target === this) { + closeLogoutModal(); + } + }); + + // Logout button - opens confirmation modal instead of directly logging out + document.getElementById('logoutButton').addEventListener('click', openLogoutModal); + + // Confirm logout button + document.getElementById('confirmLogoutBtn').addEventListener('click', handleLogout); +}); + +modalClose.addEventListener('click', closeProjectModal); \ No newline at end of file diff --git a/src/controllers/client/dashboard/clientDashboardProjects.controller.js b/src/controllers/client/dashboard/clientDashboardProjects.controller.js index 9344dd2..30a9b7c 100644 --- a/src/controllers/client/dashboard/clientDashboardProjects.controller.js +++ b/src/controllers/client/dashboard/clientDashboardProjects.controller.js @@ -34,6 +34,8 @@ async function client_dashboard_projects(req, res) { // STEP 1: Get client ID from token const clientUser = await ClientModel.findOne({ "userDetails.email": clientEmail }); + + console.log("Line 38" , clientUser) if (!clientUser) { return res.status(404).json({ success: false, message: "Client not found" }); } @@ -58,7 +60,7 @@ async function client_dashboard_projects(req, res) { // STEP 3: Get all VAs assigned to these projects const allAssignedVAs = await VAModel.find({ - _id: { $in: allProjectVaIds } + _id: { $in: allProjectVaIds.vaId } }) .select('userDetails.firstName userDetails.lastName userDetails.email status') .lean() diff --git a/src/controllers/va/vaAuth/vaLogin.controller.js b/src/controllers/va/vaAuth/vaLogin.controller.js index cff77bc..7e00699 100644 --- a/src/controllers/va/vaAuth/vaLogin.controller.js +++ b/src/controllers/va/vaAuth/vaLogin.controller.js @@ -42,54 +42,54 @@ async function closePreviousSession(vaId, lastLog) { }); // console.log(`Created logout log: ${logoutLog._id}`); - try { - const salesforceUrl = `${process.env.SF_DOCTRACKER_API_URL}services/apexrest/docva/api/va-project-log`; - const sfDataSync = { - "_id": logoutLog._id, - "loggedType": "LOGOUT", - "vaId": vaId, - "vaEmail": lastLog.vaEmail, - "projectId": lastLog.projectId, - "time": logoutLog.time, - } + // try { + // const salesforceUrl = `${process.env.SF_DOCTRACKER_API_URL}services/apexrest/docva/api/va-project-log`; + // const sfDataSync = { + // "_id": logoutLog._id, + // "loggedType": "LOGOUT", + // "vaId": vaId, + // "vaEmail": lastLog.vaEmail, + // "projectId": lastLog.projectId, + // "time": logoutLog.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(sfDataSync) - }); + // 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(sfDataSync) + // }); - if (!response.ok) { - const errorText = await response.text(); - throw new Error(`Salesforce API responded with status ${response.status}: ${errorText}`); - } + // if (!response.ok) { + // const errorText = await response.text(); + // throw new Error(`Salesforce API responded with status ${response.status}: ${errorText}`); + // } - const result = await response.json(); - // console.log("✅ Successfully synced VA data to Salesforce:", result); + // const result = await response.json(); + // // console.log("✅ Successfully synced VA data to Salesforce:", result); - return { - closedWorkTracks: openWorkTracks.length, - logoutLogId: logoutLog._id, - SyncSalesforce: result - }; - } catch (error) { - console.error("Failed to sync with Salesforce:", error.message); - console.log(error) - - // Create a custom error to distinguish Salesforce errors - const salesforceError = new Error(`Salesforce sync failed: ${error.message}`); - salesforceError.isSalesforceError = true; - throw salesforceError; - } - - // return { + // return { // closedWorkTracks: openWorkTracks.length, // logoutLogId: logoutLog._id, + // SyncSalesforce: result // }; + // } catch (error) { + // console.error("Failed to sync with Salesforce:", error.message); + // console.log(error) + + // // Create a custom error to distinguish Salesforce errors + // const salesforceError = new Error(`Salesforce sync failed: ${error.message}`); + // salesforceError.isSalesforceError = true; + // throw salesforceError; + // } + + return { + closedWorkTracks: openWorkTracks.length, + logoutLogId: logoutLog._id, + }; } catch (error) { console.error("Error closing previous session:", error); diff --git a/src/controllers/va/vaLeaves/vaLeavesApply.controller.js b/src/controllers/va/vaLeaves/vaLeavesApply.controller.js index 35ba56d..45a6451 100644 --- a/src/controllers/va/vaLeaves/vaLeavesApply.controller.js +++ b/src/controllers/va/vaLeaves/vaLeavesApply.controller.js @@ -5,7 +5,7 @@ import { VAModel, LeavesModel } from "../../../models/vaModel.js"; async function va_leave_apply_function(req, res) { try { const accessToken = req.headers.token; - + if (!accessToken) { return res.status(401).json({ success: false, message: "Missing token" }); } @@ -25,9 +25,9 @@ async function va_leave_apply_function(req, res) { const { dateFrom, dateTo, leaveType, comment } = req.body; if (!dateFrom || !dateTo || !leaveType || !comment) { - return res.status(400).json({ - success: false, - message: "dateFrom, dateTo, leaveType, and comment are required" + return res.status(400).json({ + success: false, + message: "dateFrom, dateTo, leaveType, and comment are required" }); } @@ -36,49 +36,49 @@ async function va_leave_apply_function(req, res) { try { startDate = new Date(dateFrom); endDate = new Date(dateTo); - + // Set to start of day for accurate calculation startDate.setHours(0, 0, 0, 0); endDate.setHours(0, 0, 0, 0); - + if (isNaN(startDate.getTime()) || isNaN(endDate.getTime())) { - return res.status(400).json({ - success: false, - message: "Invalid date format. Use valid date format (YYYY-MM-DD)" + return res.status(400).json({ + success: false, + message: "Invalid date format. Use valid date format (YYYY-MM-DD)" }); } } catch (error) { - return res.status(400).json({ - success: false, - message: "Invalid date format" + return res.status(400).json({ + success: false, + message: "Invalid date format" }); } // Ensure start date is before or equal to end date if (startDate > endDate) { - return res.status(400).json({ - success: false, - message: "Start date must be before or equal to end date" + return res.status(400).json({ + success: false, + message: "Start date must be before or equal to end date" }); } // Ensure dates are not in the past const today = new Date(); today.setHours(0, 0, 0, 0); - + if (startDate < today) { - return res.status(400).json({ - success: false, - message: "Cannot apply for leave for past dates" + return res.status(400).json({ + success: false, + message: "Cannot apply for leave for past dates" }); } // Validate leave type const validLeaveTypes = ["SICK", "PTO"]; if (!validLeaveTypes.includes(leaveType)) { - return res.status(400).json({ - success: false, - message: "Invalid leave type. Must be SICK or PTO" + return res.status(400).json({ + success: false, + message: "Invalid leave type. Must be SICK or PTO" }); } @@ -87,9 +87,9 @@ async function va_leave_apply_function(req, res) { const totalDays = Math.floor(timeDifference / (1000 * 60 * 60 * 24)) + 1; // +1 to include both start and end dates if (totalDays <= 0) { - return res.status(400).json({ - success: false, - message: "Invalid date range" + return res.status(400).json({ + success: false, + message: "Invalid date range" }); } @@ -106,9 +106,9 @@ async function va_leave_apply_function(req, res) { }); if (existingLeave) { - return res.status(400).json({ - success: false, - message: "You already have a pending leave request that overlaps with these dates" + return res.status(400).json({ + success: false, + message: "You already have a pending leave request that overlaps with these dates" }); } @@ -125,9 +125,9 @@ async function va_leave_apply_function(req, res) { }); if (existingApprovedLeave) { - return res.status(400).json({ - success: false, - message: "You already have an approved leave that overlaps with these dates" + return res.status(400).json({ + success: false, + message: "You already have an approved leave that overlaps with these dates" }); } @@ -147,94 +147,95 @@ async function va_leave_apply_function(req, res) { await leaveRequest.save(); try { - // const salesforceUrl = `${process.env.SF_DOCTRACKER_API_URL}services/apexrest/docva/api/timesheet-adjustment`; - // const sfData = { - // "id": leaveRequest._id, - // "va": { - // "id": vaId - // }, - // "dateFrom": startDate.toISOString().split('T')[0], - // "dateTo": endDate.toISOString().split('T')[0], - // "totalDays": totalDays, - // "leaveType": leaveType, - // "comment": processedComment.plainText, // Send plain text to Salesforce - // "attachments": processedComment.extractedImages, // Send extracted images info - // "status": "PENDING", - // "rejectionReason": "" - // } + const salesforceUrl = `${process.env.SF_DOCTRACKER_API_URL}services/apexrest/docva/api/leave-request`; + const sfData = { + "leaveId": leaveRequest._id.toString(), + // "va": { + // "id": vaId + // }, + "va": vaId.toString(), + "dateFrom": startDate.toISOString().split('T')[0], + "dateTo": endDate.toISOString().split('T')[0], + "totalDays": totalDays, + "leaveType": leaveType, + "comment": processedComment.plainText, // Send plain text to Salesforce + "attachments": processedComment.extractedImages, // Send extracted images info + "status": "PENDING", + "rejectionReason": "" + } - // const response = await fetch(salesforceUrl, { - // method: 'POST', - // headers: { - // 'Content-Type': 'application/json', - // // Add any required headers for Salesforce authentication - // // 'Authorization': 'Bearer ' + process.env.SF_ACCESS_TOKEN - // }, - // body: JSON.stringify({"data": sfData}) - // }); - - // if (!response.ok) { - // const errorText = await response.text(); - // throw new Error(`Salesforce API responded with status ${response.status}: ${errorText}`); - // } + const response = await fetch(salesforceUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + // Add any required headers for Salesforce authentication + // 'Authorization': 'Bearer ' + process.env.SF_ACCESS_TOKEN + }, + body: JSON.stringify({ "data": sfData }) + }); + + if (!response.ok) { + const errorText = await response.text(); + throw new Error(`Salesforce API responded with status ${response.status}: ${errorText}`); + } // const result = await response.json(); - // // console.log("✅ Successfully synced VA data to Salesforce:", result); - - return res.status(201).json({ - success: true, - message: "Leave request submitted successfully", - data: { - leaveId: leaveRequest._id, - dateFrom: startDate.toISOString().split('T')[0], - dateTo: endDate.toISOString().split('T')[0], - totalDays: totalDays, - leaveType, - comment: processedComment.processedHTML, - // attachments: processedComment.extractedImages, - status: "PENDING" - }, - // SyncSalesforce: result - }); + // console.log("✅ Successfully synced VA data to Salesforce:", result); + + // return res.status(201).json({ + // success: true, + // message: "Leave request submitted successfully", + // data: { + // leaveId: leaveRequest._id, + // dateFrom: startDate.toISOString().split('T')[0], + // dateTo: endDate.toISOString().split('T')[0], + // totalDays: totalDays, + // leaveType, + // comment: processedComment.processedHTML, + // // attachments: processedComment.extractedImages, + // status: "PENDING" + // }, + // // SyncSalesforce: result + // }); } 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; } - // return res.status(201).json({ - // success: true, - // message: "Leave request submitted successfully", - // data: { - // leaveId: leaveRequest._id, - // dateFrom: startDate.toISOString().split('T')[0], - // dateTo: endDate.toISOString().split('T')[0], - // totalDays: totalDays, - // leaveType, - // comment, - // status: "PENDING" - // } - // }); + return res.status(201).json({ + success: true, + message: "Leave request submitted successfully", + data: { + leaveId: leaveRequest._id, + dateFrom: startDate.toISOString().split('T')[0], + dateTo: endDate.toISOString().split('T')[0], + totalDays: totalDays, + leaveType, + comment, + status: "PENDING" + } + }); } catch (error) { console.error("Leave apply error:", error); - + if (error.name === 'JsonWebTokenError') { return res.status(401).json({ success: false, message: "Invalid token" }); } - + if (error.name === 'TokenExpiredError') { return res.status(401).json({ success: false, message: "Token expired" }); } - - return res.status(500).json({ - success: false, + + return res.status(500).json({ + success: false, message: "Internal Server Error", - error: error.message + error: error.message }); } } @@ -277,7 +278,7 @@ function extractPlainTextFromHTML(html) { .replace(/<[^>]*>/g, ' ') // Replace tags with spaces .replace(/\s+/g, ' ') // Collapse multiple spaces .trim(); - + // Decode common HTML entities plainText = plainText .replace(/ /g, ' ') @@ -304,7 +305,7 @@ function extractAndProcessBase64Images(html) { try { // Regex to find base64 images in HTML (various formats) const base64Regex = /]+src="data:image\/([a-zA-Z0-9+]+);base64,([^"]+)"[^>]*>/gi; - + let match; let imageIndex = 0; @@ -316,7 +317,7 @@ function extractAndProcessBase64Images(html) { // Create a unique filename const timestamp = Date.now(); const filename = `leave_image_${timestamp}_${imageIndex}.${imageType}`; - + // Calculate approximate file size const size = Math.floor((base64Data.length * 3) / 4); // Base64 size calculation @@ -332,7 +333,7 @@ function extractAndProcessBase64Images(html) { // Replace base64 with a reference in HTML const replacementHTML = `Leave evidence ${imageIndex + 1}`; - + processedHTML = processedHTML.replace(fullMatch, replacementHTML); imageIndex++; diff --git a/src/controllers/va/vaTimesheetAdjustment/vaManageTimesheetAdjustment.controller.js b/src/controllers/va/vaTimesheetAdjustment/vaManageTimesheetAdjustment.controller.js index f114406..bc4e6a7 100644 --- a/src/controllers/va/vaTimesheetAdjustment/vaManageTimesheetAdjustment.controller.js +++ b/src/controllers/va/vaTimesheetAdjustment/vaManageTimesheetAdjustment.controller.js @@ -31,7 +31,7 @@ async function va_timesheet_manage_adjustment_request(req, res) { }); } - if (action === "approve") { + if (action === "APPROVE") { // Create new log entries in LoggedModel const loginLog = new LoggedModel({ loggedType: "LOGIN", @@ -78,7 +78,7 @@ async function va_timesheet_manage_adjustment_request(req, res) { } }); - } else if (action === "reject") { + } else if (action === "REJECT") { // Update adjustment record as rejected adjustment.status = "REJECTED"; adjustment.reviewedAt = new Date(); diff --git a/src/controllers/va/vaTimesheetAdjustment/vaTimesheetAdjustment.controller.js b/src/controllers/va/vaTimesheetAdjustment/vaTimesheetAdjustment.controller.js index a252386..5ca4a5a 100644 --- a/src/controllers/va/vaTimesheetAdjustment/vaTimesheetAdjustment.controller.js +++ b/src/controllers/va/vaTimesheetAdjustment/vaTimesheetAdjustment.controller.js @@ -138,12 +138,12 @@ async function va_timesheet_adjustment_function(req, res) { try { const salesforceUrl = `${process.env.SF_DOCTRACKER_API_URL}services/apexrest/docva/api/timesheet-adjustment`; const sfData = { - "id": adjustmentRequest._id, + "id": adjustmentRequest._id.toString(), "va": { - "id": vaId + "id": vaId.toString() }, "project": { - "id": projectId + "id": projectId.toString() }, "date": adjustmentDate.toISOString().split('T')[0], "loginTime": loginDateTime.toISOString(), diff --git a/src/controllers/va/vaTips/vaTipsManage.controller.js b/src/controllers/va/vaTips/vaTipsManage.controller.js new file mode 100644 index 0000000..bcaf10b --- /dev/null +++ b/src/controllers/va/vaTips/vaTipsManage.controller.js @@ -0,0 +1,143 @@ +import { VAModel, TipsModel, ClientModel } from "../../../models/vaModel.js"; + +// Approve or reject leave +async function va_tips_manage_function(req, res) { + + const { clientId, amount, description, vaId } = req.body; + + try { + const vaUser = await VAModel.findById(vaId); + if (!vaUser) { + return res.status(404).json({ success: false, message: "Virtual Assistant not found" }); + } + const clUser = await ClientModel.findById(clientId); + if (!clUser) { + return res.status(404).json({ success: false, message: "Client not found" }); + } + + // Create new tip entry + const newTip = { + clientId, + status: "Unbilled", + amount: parseFloat(amount).toFixed(2), + description: description || "", + vaName: vaUser.userDetails.firstName ? vaUser.userDetails.firstName : "" + " " + vaUser.userDetails.lastName ? vaUser.userDetails.lastName : "", + vaId, + }; + + const createdTip = await TipsModel.create(newTip); + + const salesforceUrl = `${process.env.SF_DOCTRACKER_API_URL}services/apexrest/docva/api/va-tips`; + + const sfData = { + "id": createdTip._id.toString(), + "vaId": createdTip.vaId.toString(), + "status": "Unbilled", + "clientId": createdTip.clientId.toString(), + "comment": createdTip.description || "", + "tipAmount": Number(parseFloat(createdTip.amount).toFixed(2)), // Convert to cents + } + + const response = await fetch(salesforceUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + // Add any required headers for Salesforce authentication + // 'Authorization': 'Bearer ' + process.env.SF_ACCESS_TOKEN + }, + body: JSON.stringify({ ...sfData }) + }); + + if (response.ok) { + console.log("✅ Successfully synced Tip data to Salesforce"); + return res.status(201).json({ success: true, data: createdTip }); + } else { + TipsModel.findByIdAndDelete(createdTip._id); + const errorText = await response.text(); + throw new Error(`Salesforce API responded with status ${response.status}: ${errorText}`); + } + + } catch (error) { + console.log(error); + res.status(500).json({ success: false, message: "Internal server error" }); + } +} + +async function va_tips_get(req, res) { + + const { clientId } = req.body; + + try { + const clUser = await ClientModel.findById(clientId); + if (!clUser) { + return res.status(404).json({ success: false, message: "Client not found" }); + } + const data = await TipsModel.find({ clientId: clientId, status: "Unbilled" }); + return res.status(200).json({ success: true, data: data }); + } catch (error) { + console.log(error); + res.status(500).json({ success: false, message: "Internal server error" }); + } +} + +async function va_tips_update(req, res) { + const { tipId } = req.params; + const { update } = req.body; + + try { + const tipData = await TipsModel.findById(tipId); + if (!tipData) { + return res.status(404).json({ success: false, message: "Tip not found" }); + } + + await TipsModel.updateOne({ _id: tipId }, update); + return res.status(200).json({ success: true, message: "Tip updated successfully" }); + + } catch (error) { + console.log(error); + res.status(500).json({ success: false, message: "Internal server error" }); + } +} + +async function va_tips_delete(req, res) { + const { tipId } = req.params; + + try { + + + const salesforceUrl = `${process.env.SF_DOCTRACKER_API_URL}services/apexrest/docva/api/va-tips`; + + // const sfData = { + // "id": createdTip._id.toString(), + // "vaId": createdTip.vaId.toString(), + // "status": "Unbilled", + // "clientId": createdTip.clientId.toString(), + // "comment": createdTip.description, + // "tipAmount": Number(parseFloat(createdTip.amount).toFixed(2)), // Convert to cents + // } + + const response = await fetch(salesforceUrl, { + method: 'DELETE', + headers: { + 'Content-Type': 'application/json', + // Add any required headers for Salesforce authentication + // 'Authorization': 'Bearer ' + process.env.SF_ACCESS_TOKEN + }, + body: JSON.stringify({ "id": tipId}) + }); + + if (response.ok) { + await TipsModel.findOneAndDelete({ _id: tipId }); + return res.status(200).json({ success: true, message: "Tip updated successfully" }); + } else { + const errorText = await response.text(); + throw new Error(`Salesforce API responded with status ${response.status}: ${errorText}`); + } + } catch (error) { + console.log(error); + res.status(500).json({ success: false, message: "Internal server error" }); + } +} + + +export { va_tips_manage_function, va_tips_get, va_tips_update, va_tips_delete }; \ No newline at end of file diff --git a/src/models/vaModel.js b/src/models/vaModel.js index e6794e1..3b797f3 100644 --- a/src/models/vaModel.js +++ b/src/models/vaModel.js @@ -89,9 +89,16 @@ const projectSchema = new Schema({ }, vaAssigned: [ { - type: Types.ObjectId, - ref: "VA", - }, + _id: false, + vaId: { + type: Types.ObjectId, + ref: "VA", + }, + isFlexibleAllowed: { + type: Boolean, + default: true + } + } ], dedicatedHours: { type: String, @@ -110,7 +117,7 @@ const projectSchema = new Schema({ type: String, default: null }, - isFlexibleAllowedPt: { + isFlexibleAllowed: { type: Boolean, default: true }, @@ -189,7 +196,7 @@ const clientDetailsSchema = new Schema({ enum: ["CHECKING", "SAVINGS"], type: String, }, - + accountHolderName: { type: String, }, @@ -363,36 +370,36 @@ const vaWorkTrackSchema = new Schema({ ) const passwordResetSchema = new mongoose.Schema({ - email: { - type: String, - required: true, - lowercase: true, - index: true - }, - token: { - type: String, - required: true, - unique: true, - index: true - }, - userType: { - type: String, - required: true, - enum: ['va', 'client'], - index: true - }, - expiresAt: { - type: Date, - required: true, - index: true, - expires: 3600 // TTL index for 1 hour - }, - used: { - type: Boolean, - default: false - } + email: { + type: String, + required: true, + lowercase: true, + index: true + }, + token: { + type: String, + required: true, + unique: true, + index: true + }, + userType: { + type: String, + required: true, + enum: ['va', 'client'], + index: true + }, + expiresAt: { + type: Date, + required: true, + index: true, + expires: 3600 // TTL index for 1 hour + }, + used: { + type: Boolean, + default: false + } }, { - timestamps: true + timestamps: true }); passwordResetSchema.index({ email: 1, used: 1 }); @@ -511,6 +518,37 @@ const leavesSchema = new Schema({ } }, { timestamps: true }); + +/* ---------------------- Tips Schema ---------------------- */ +const tipsSchema = new Schema({ + clientId: { + type: Types.ObjectId, + ref: "Client" + }, + vaId: { + type: Types.ObjectId, + ref: "VA" + }, + vaName: { + type: String, + required: true + }, + status: { + type: String, + enum: ["Unbilled", "Billed"], + default: "Unbilled", + required: true + }, + amount: { + type: String, + required: true + }, + description: { + type: String, + } +}, { timestamps: true }) + + /* ---------------------- Model Exports ---------------------- */ const VAModel = model("VA", vaSchema); const ClientModel = model("Client", clientSchema); @@ -520,5 +558,6 @@ const WorkTrackModel = model("TrackWork", vaWorkTrackSchema); const PasswordResetModel = model("PasswordReset", passwordResetSchema); const AdjustedHoursModel = model("AdjustedHours", adjustedHoursSchema); const LeavesModel = model("Leaves", leavesSchema); +const TipsModel = model("Tips", tipsSchema); -export { VAModel, ClientModel, ProjectModel, LoggedModel, WorkTrackModel, PasswordResetModel, AdjustedHoursModel, LeavesModel }; \ No newline at end of file +export { VAModel, ClientModel, ProjectModel, LoggedModel, WorkTrackModel, PasswordResetModel, AdjustedHoursModel, LeavesModel, TipsModel }; \ No newline at end of file diff --git a/src/routes/v1/client.routes.js b/src/routes/v1/client.routes.js index 9e2c6e6..97f95e1 100644 --- a/src/routes/v1/client.routes.js +++ b/src/routes/v1/client.routes.js @@ -13,6 +13,7 @@ import { client_dashboard_va_log } from "../../controllers/client/dashboard/clie import { client_verify_email } from "../../controllers/client/vaClientEmailVerify.controller.js"; import { client_dashboard_projects } from "../../controllers/client/dashboard/clientDashboardProjects.controller.js"; import { client_logout_callback } from "../../controllers/client/dashboard/clientLogout.controller.js"; +import { va_tips_delete, va_tips_get, va_tips_manage_function, va_tips_update } from "../../controllers/va/vaTips/vaTipsManage.controller.js"; // clients routes clientRoutes.post("/register", va_register_client); // Register a new VA @@ -22,11 +23,14 @@ clientRoutes.post("/forgot-password", client_request_password_reset); clientRoutes.post("/verify-client-email", client_verify_email); clientRoutes.post("/logout", clientAuthValidator, client_logout_callback); +clientRoutes.post("/tips", clientAuthValidator, va_tips_manage_function); +clientRoutes.get("/tips", clientAuthValidator, va_tips_get); +clientRoutes.patch("/tips/:tipId", clientAuthValidator, va_tips_update); +clientRoutes.delete("/tips/:tipId", clientAuthValidator, va_tips_delete); + // clientRoutes.get("/:clientID?", va_clients_callback); // VA Clients Callback for getting all VA's or specific client by ID (NC) clientRoutes.get("/:clientId/projects/:projectId", va_clients_projects_callback); // VA Clients Projects Callback (NC) - - // dashboard API's clientRoutes.get("/dashboard", clientAuthValidator, client_dashboard); // VA Clients Projects Callback clientRoutes.post("/dashboard/va-report", clientAuthValidator, client_dashboard_va_log); // VA details diff --git a/src/server.js b/src/server.js index 93c25a3..370bbc7 100644 --- a/src/server.js +++ b/src/server.js @@ -18,6 +18,7 @@ import apiRoutes from './routes/index.js' import { render_reset_form } from "./controllers/passwordReset.controllers.js"; +import expressLayouts from "express-ejs-layouts"; import jwt from "jsonwebtoken"; const app = express(); @@ -39,6 +40,11 @@ app.on('certificate-error', (event, webContents, url, error, certificate, callba }); app.set("view engine", "ejs"); + +app.use(expressLayouts); + +// DEFAULT layout for all pages +app.set("layout", "layouts/dashboard"); const PORT = process.env.PORT || 8000; app.use(cors({ @@ -118,9 +124,16 @@ app.get('/health', (req, res) => { res.status(201).send({ status: "Server is healthy" }); }) + + + app.get("/client/login", (req, res) => { - res.render("client_login") -}) + res.render("client/client_login", { + layout: "layouts/auth", // ⬅️ override layout + title: "DocVA | Login" + }); +}); + app.get("/client/forgot-password", (req, res) => { res.render("forgot_password") }) @@ -129,8 +142,11 @@ app.get("/client/forgot-password", (req, res) => { 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'); + res.render('client/dashboard-dashboard', { + title: "Client Dashboard", + pageScript: "dashboard.js" + }); + // res.render('layouts/dashboard'); } catch (error) { console.error('Dashboard error:', error); res.status(500).render('client/dashboard-dashboard', { error: 'Failed to load dashboard' }); @@ -140,7 +156,11 @@ app.get('/client/dashboard', async (req, res) => { 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', { + title: "Virtual Assistants", + pageScript:"dashboard-virtual-assistants.js" + + }); // res.render('client/dashboard-virtual-assistants', vaData); } catch (error) { console.error('Virtual assistants error:', error); @@ -151,7 +171,11 @@ app.get('/client/virtual-assistants', async (req, res) => { app.get('/client/projects', async (req, res) => { try { // const vaData = await virtualAssistantService.getClientVAs(req.user.id); - res.render('client/dashboard-projects'); + res.render('client/dashboard-projects', { + title: "Projects", + pageScript:"projects.js" + + }); } catch (error) { console.error('Projects error:', error); res.status(500).render('client/dashboard-projects', { error: 'Failed to load projects' }); @@ -161,23 +185,25 @@ app.get('/client/projects', async (req, res) => { app.get('/client/profile', async (req, res) => { try { // const vaData = await virtualAssistantService.getClientVAs(req.user.id); - res.render('client/dashboard-profile'); + res.render('client/dashboard-profile',{ + title: "Profile", + pageScript:"profile.js" + + }); } catch (error) { console.error('Projects error:', error); res.status(500).render('client/dashboard-profile', { error: 'Failed to load profile' }); } }); -app.get('/client/virtual-assistants/manage-tips', async (req, res) => { - try { - // const vaData = await virtualAssistantService.getClientVAs(req.user.id); - res.render('client/dashboard-manage-va-tips'); - } catch (error) { - console.error('Projects error:', error); - res.status(500).render('client/dashboard-manage-va-tips', { error: 'Failed to load profile' }); - } +app.get('/client/virtual-assistants/manage-tips', (req, res) => { + res.render('client/dashboard-manage-va-tips', { + title: 'Manage VA Tips', + pageScript: 'manage-tips.js' + }); }); + app.get("/live/:projectId/:vaId", async (req, res) => { const { projectId, vaId } = req.params; @@ -400,6 +426,36 @@ io.on("connection", (socket) => { socket.to(vaSocketRespectToClient.get(`${clientId}-${vaId}-${projectId}-va`)).emit("appToCapture", { appTitle }); }); + socket.on("requestVisibleApps", async ({ token }) => { + try { + // Verify token + const decoded = jwt.verify(token, process.env.JWT_SECRET_ACCESS_TOKEN); + const vaEmail = decoded.email; + + // Find VA in DB + const vaUser = await VAModel.findOne({ "userDetails.email": vaEmail }); + if (!vaUser) { + console.log("❌ Unauthorized VA"); + return; + } + + const vaId = vaUser._id.toString(); + console.log("🟢 Received Apps from VA:", vaId); + + // Broadcast image only to allowed clients + for (let [_, clientSocket] of io.sockets.sockets) { + if ( + clientSocket.data.role === "client" && + clientSocket.data.allowedVAs?.includes(vaId) + ) { + clientSocket.emit("captureAppsTracker"); + } + } + } catch (error) { + console.error("❌ Token verification failed:", err.message); + } + }); + socket.on("getVisibleApps", async ({ token, apps }) => { try { // Verify token @@ -431,9 +487,27 @@ io.on("connection", (socket) => { }); - socket.on('disconnect', () => { - console.log('a user Disconnected'); + // socket.on('disconnect', () => { + // console.log('a user Disconnected'); + // }); + + // When client leaves or closes tab + socket.on("disconnect", () => { + if (socket.data.role === "client") { + const { clientId, vaId, projectId } = socket.data; + + const vaSocketId = vaSocketRespectToClient.get( + `${clientId}-${vaId}-${projectId}-va` + ); + + if (vaSocketId) { + io.to(vaSocketId).emit("stopSendAppsDetails"); + } + } + + console.log("❌ Client disconnected"); }); + }); app.use((req, res) => { diff --git a/views/client_login.ejs b/views/client/client_login.ejs similarity index 100% rename from views/client_login.ejs rename to views/client/client_login.ejs diff --git a/views/client/dashboard-dashboard.ejs b/views/client/dashboard-dashboard.ejs index e637a04..7e1e9e4 100644 --- a/views/client/dashboard-dashboard.ejs +++ b/views/client/dashboard-dashboard.ejs @@ -1,557 +1,72 @@ - - - - - - DocVA Client | Dashboard - - - - -
- -
- -
-
- -
-
-
-
-

Active VA

-
0
-
-
- -
-
-
-
-
-
-

Total VA's

-
0
-
-
- -
-
-
- -
-
-
-

Total Projects

-
0
-
-
- -
-
-
-
- - -
-
-

Virtual Assistants 0 VAs

-
-
-
- - - - - - - - - - - - - - -
StatusVirtual AssistantActive ProjectTotal ProjectsToday Logged HoursAction
-
- -
-
-
-
-
- - - - - - \ No newline at end of file + +
+
+

Virtual Assistants 0 VAs

+
+
+
+ + + + + + + + + + + + + + +
StatusVirtual AssistantActive ProjectTotal ProjectsToday Logged HoursAction
+
+ +
+
\ No newline at end of file diff --git a/views/client/dashboard-manage-va-tips.ejs b/views/client/dashboard-manage-va-tips.ejs index bc9580f..fa30e29 100644 --- a/views/client/dashboard-manage-va-tips.ejs +++ b/views/client/dashboard-manage-va-tips.ejs @@ -1,350 +1,59 @@ - - - - - - DocVA Client | Dashboard - - - - -
- -
- -
-
- -
-
-
-
-

Unbilled Tips

-
$0
-
-
- -
-
-
-
-
-
-

Total Tips

-
$0
-
-
- -
-
-
-
- - -
-
- Back - Tip Virtual Assistant -
-
-
- - - - - - - - - - - - - - -
VoidStatusVirtual Assistant NameDescriptionDate AddedAmount
-
- -
-
-
-
-
- - - - - - \ No newline at end of file + +
+
+ Back + Tip Virtual Assistant +
+
+
+ + + + + + + + + + + + + + +
VoidStatusVirtual Assistant NameDescriptionDate AddedAmount
+
+ +
+
\ No newline at end of file diff --git a/views/client/dashboard-profile.ejs b/views/client/dashboard-profile.ejs index 4355b94..934137b 100644 --- a/views/client/dashboard-profile.ejs +++ b/views/client/dashboard-profile.ejs @@ -1,478 +1,65 @@ - - - - - - DocVA Client | Profile - - - - - -
- -
- -
-
-
-
-

Client Profile

-
- -
- -
-
- -

Personal Information

-
-
-
- - -
-
- - -
-
- - -
-
-
- - -
-
- -

Address Information

-
-
-
- - -
-
- - -
-
- - -
-
- - -
-
-
- - -
-
- -
-
-
-
+
+
+ +
-
-
- - -