diff --git a/package-lock.json b/package-lock.json index 6b6caba..5e6a6cc 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", @@ -149,6 +150,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", @@ -2360,6 +2362,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", @@ -2904,6 +2907,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001688", "electron-to-chromium": "^1.5.73", @@ -3741,7 +3745,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", @@ -4232,6 +4237,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", @@ -7993,6 +8003,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", @@ -8376,6 +8387,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" @@ -9084,6 +9096,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", @@ -9131,6 +9144,7 @@ "integrity": "sha512-MfwFQ6SfwinsUVi0rNJm7rHZ31GyTcpVE5pgVA3hwFRb7COD4TzjUUwhGWKfO50+xdc2MQPuEBBJoqIMGt3JDw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@discoveryjs/json-ext": "^0.6.1", "@webpack-cli/configtest": "^3.0.1", @@ -9296,6 +9310,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 5b7e2e0..afe4b12 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "scripts": { "start": "nodemon --inspect src/server.js", "test": "echo \"Error: no test specified\" && exit 1", - "deploy": "curl -X POST http://35.95.209.161:9000/hooks/deploy-va-middleware", + "deploy": "curl -X POST http://35.162.222.18:9000/hooks/deploy-va-middleware", "build": "npx webpack --mode production", "dev": "npx webpack --mode development" }, @@ -25,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 1921186..4b2c186 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); } @@ -1708,4 +1774,167 @@ tr[data-vaid]:hover { .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 1d9372f..19b6cf6 100644 --- a/src/controllers/client/dashboard/clientDashboardProjects.controller.js +++ b/src/controllers/client/dashboard/clientDashboardProjects.controller.js @@ -35,6 +35,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" }); } diff --git a/src/controllers/va/vaAuth/vaLogin.controller.js b/src/controllers/va/vaAuth/vaLogin.controller.js index dcb4b14..64a7469 100644 --- a/src/controllers/va/vaAuth/vaLogin.controller.js +++ b/src/controllers/va/vaAuth/vaLogin.controller.js @@ -42,32 +42,32 @@ import authFetch from "../../../utils/authFetch.js"; // time: currentTime // Use same timestamp for consistent logging // }); -// // 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, -// } + // 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, + // } -// const response = await authFetch(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); diff --git a/src/controllers/va/vaLeaves/vaLeavesApply.controller.js b/src/controllers/va/vaLeaves/vaLeavesApply.controller.js index 7e86e13..719dd23 100644 --- a/src/controllers/va/vaLeaves/vaLeavesApply.controller.js +++ b/src/controllers/va/vaLeaves/vaLeavesApply.controller.js @@ -159,7 +159,7 @@ async function va_leave_apply_function(req, res) { "dateTo": endDate.toISOString().split('T')[0], "totalDays": totalDays, "leaveType": leaveType, - "comment": processedComment.plainText !== "" ? processedComment.plainText : "-", // Send plain text to Salesforce + "comment": processedComment.plainText, // Send plain text to Salesforce "attachments": processedComment.extractedImages, // Send extracted images info "status": "PENDING", "rejectionReason": "" diff --git a/src/controllers/va/vaProfile/vaProfile.controller.js b/src/controllers/va/vaProfile/vaProfile.controller.js index 6417dce..346be7e 100644 --- a/src/controllers/va/vaProfile/vaProfile.controller.js +++ b/src/controllers/va/vaProfile/vaProfile.controller.js @@ -21,12 +21,16 @@ async function va_profile_callback(req, res) { return res.status(400).json({ success: false, message: "Invalid token payload" }); } + console.log(decoded.email , "Line 24") + + const vaUser = await VAModel.findOne({ "userDetails.email": decoded.email }) .select("-userDetails.password") // hide password .populate("clientIds", "userDetails.email userDetails.name") // optional populate .populate("projectIds", "name totalLoggedHours") // optional populate .lean(); + console.log("Line 33" , vaUser) if (!vaUser) { return res.status(404).json({ success: false, message: "VA not found" }); } diff --git a/src/server.js b/src/server.js index 73a8f10..791739e 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({ @@ -114,10 +120,20 @@ app.get('/clientregister/:sf_token?', (req, res) => { app.get('/auth/password/reset-form', render_reset_form); -app.get("/client/login", (req, res) => { - res.render("client_login") +app.get('/health', (req, res) => { + res.status(201).send({ status: "Server is healthy" }); }) + + + +app.get("/client/login", (req, res) => { + res.render("client/client_login", { + layout: "layouts/auth", // ⬅️ override layout + title: "DocVA | Login" + }); +}); + app.get("/client/forgot-password", (req, res) => { res.render("forgot_password") }) @@ -126,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' }); @@ -137,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); @@ -148,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' }); @@ -158,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; @@ -411,6 +440,66 @@ io.on("connection", (socket) => { socket.to(vaSocketRespectToClient.get(`${clientId}-${vaId}-${projectId}-va`)).emit("appToCapture", { appTitle }); }); + socket.on("getVisibleApps", 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("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("requestVisibleApps", async ({ token }) => { try { // Verify token @@ -485,28 +574,15 @@ io.on("connection", (socket) => { ); if (vaSocketId) { - // io.to(vaSocketId).emit("stopSendAppsDetails"); - io.to(vaSocketRespectToClient.get(`${clientId}-${vaId}-${projectId}-va`)).emit("stopSendAppsDetails", { projId: projectId }); + io.to(vaSocketId).emit("stopSendAppsDetails"); } } - + console.log("❌ Client disconnected"); }); }); -app.get("/", (req, res) => { - res.render("client_login") -}) - -app.get("/forgot-password", (req, res) => { - res.render("forgot_password") -}) - -app.get("/health", (req, res) => { - return res.status(200).json({ "success": true, "message": "Server is Healthy" }) -}) - app.use((req, res) => { res.status(404).render("not_found", { title: "404 - Page Not Found" 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 a4d2eed..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 c73a03b..fa30e29 100644 --- a/views/client/dashboard-manage-va-tips.ejs +++ b/views/client/dashboard-manage-va-tips.ejs @@ -1,548 +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 +
+
+
+

Total Tips

+
$0
+
+
+ +
+
+
+
+ + +
+
+ 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 8dcb40b..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

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