diff --git a/package-lock.json b/package-lock.json index 6b6caba..98c8b48 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", @@ -4232,6 +4233,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", diff --git a/package.json b/package.json index 5b7e2e0..080fdae 100644 --- a/package.json +++ b/package.json @@ -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 71fea48..6879380 100644 --- a/public/css/dashboard.css +++ b/public/css/dashboard.css @@ -213,6 +213,7 @@ body { transform: translateX(-100%); transition: .3s ease; } + .dashboard-sidebar.active { transform: translateX(0); } @@ -260,6 +261,53 @@ 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 +747,7 @@ body { opacity: 0; transform: scale(0.9) translateY(-20px); } + to { opacity: 1; transform: scale(1) translateY(0); @@ -1214,10 +1263,19 @@ tr[data-vaid]:hover { background: var(--color-gray-400); } + @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 { @@ -1627,12 +1685,20 @@ 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); } @@ -1710,6 +1776,164 @@ tr[data-vaid]: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; +} + +@media (max-width: 768px) { + .profile-name { + display: none; + } +} /* Accordion Styles */ .accordion { @@ -1803,6 +2027,8 @@ tr[data-vaid]:hover { border-radius: 20px; font-size: 0.875rem; font-weight: 600; + display: inline-block; + text-align: center; } .loading-spinner { 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..c6ac760 --- /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..d29a0c3 --- /dev/null +++ b/public/js/pages/profile.js @@ -0,0 +1,1241 @@ +// 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(); +} + +// Bank Details Modal Functions +function openBankDetailsModal() { + const modal = document.getElementById('bankDetailsModal'); + modal.style.display = 'flex'; + document.body.style.overflow = 'hidden'; + + // Clear form and errors + document.getElementById('payment-form').reset(); + document.querySelectorAll('.payment-option').forEach(option => { + option.classList.remove('selected'); + }); + + document.querySelectorAll('.payment-details').forEach(details => { + details.classList.remove('active'); + // Remove required attributes from all payment fields + details.querySelectorAll('[required]').forEach(field => { + field.removeAttribute('required'); + }); + }); +} + +function closeBankDetailsModal() { + const modal = document.getElementById('bankDetailsModal'); + modal.style.display = 'none'; + document.body.style.overflow = 'auto'; + + // Clear form and errors + document.getElementById('payment-form').reset(); + document.querySelectorAll('.payment-option').forEach(option => { + option.classList.remove('selected'); + }); + + document.querySelectorAll('.payment-details').forEach(details => { + details.classList.remove('active'); + // Remove required attributes from all payment fields + details.querySelectorAll('[required]').forEach(field => { + field.removeAttribute('required'); + }); + }); +} + +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'; + } +} + +// Change bank details +async function changeBankDetails(event) { + event.preventDefault(); + + const formData = { + userDetails: {} + }; + + const activePaymentDetails = document.querySelector('.payment-details.active'); + if (activePaymentDetails) { + const requiredFields = activePaymentDetails.querySelectorAll('[required]'); + let isValid = true; + formData.userDetails.paymentMethod = activePaymentDetails.id === 'card-details' ? 'card' : 'bank'; + + // Validate card payment + if (formData.userDetails.paymentMethod === 'card') { + // Card number validation + const cardNumber = document.getElementById('card-number').value; + if (!validateCardNumber(cardNumber)) { + isValid = false; + showBankError('card-number'); + return isValid; + } else { + // Show card type + const cardType = detectCardType(cardNumber); + if (cardType !== 'unknown') { + // You can display card type to user if needed + console.log('Detected card type:', cardType); + } + } + + // Expiry date validation + const expiryDate = document.getElementById('expiry-date').value; + if (!validateExpiryDate(expiryDate)) { + isValid = false; + showBankError('expiry-date'); + return isValid; + } + + // CVV validation + const cvv = document.getElementById('security-code').value; + const cardType = detectCardType(cardNumber); + if (!validateCVV(cvv, cardType)) { + isValid = false; + showBankError('security-code'); + return isValid; + } + + // ZIP code validation + const zipCode = document.getElementById('card-zip').value; + if (!validateZipCodeRegex(zipCode)) { + isValid = false; + showBankError('card-zip'); + return isValid; + } + + } + // Validate bank payment + else if (formData.userDetails.paymentMethod === 'bank') { + // Account number validation + const accountNumber = document.getElementById('account-number').value; + if (!validateBankAccount(accountNumber)) { + isValid = false; + showBankError('account-number'); + return isValid; + } + + // Routing number validation + const routingNumber = document.getElementById('routing-number').value; + if (!validateRoutingNumber(routingNumber)) { + isValid = false; + showBankError('routing-number'); + return isValid; + } + + // Basic validation for other bank fields + requiredFields.forEach(field => { + if (!field.value.trim()) { + isValid = false; + field.classList.add('field-error'); + if (isValid === false) { + showBankError(field.id); + } + } else { + field.classList.remove('field-error'); + } + }); + } + } + + let accountCardNumber = document.getElementById('card-number'); + let accountExpiryDate = document.getElementById('expiry-date'); + let accountSecurityCode = document.getElementById('security-code'); + let accountCardZip = document.getElementById('card-zip'); + let accountBankName = document.getElementById('bank-name'); + let accountAccountNumber = document.getElementById('account-number'); + let accountRoutingNumber = document.getElementById('routing-number'); + let accountAccountType = document.getElementById('account-type'); + let accountAccountHolderName = document.getElementById('account-holder-name'); + + if(accountCardNumber.value) { + formData.userDetails.cardNumber = accountCardNumber.value; + } + + if(accountExpiryDate.value) { + formData.userDetails.expiryDate = accountExpiryDate.value; + } + + if(accountSecurityCode.value) { + formData.userDetails.securityCode = accountSecurityCode.value; + } + + if(accountCardZip.value) { + formData.userDetails.cardZip = accountCardZip.value; + } + + if(accountBankName.value) { + formData.userDetails.bankName = accountBankName.value; + } + + if(accountAccountNumber.value) { + formData.userDetails.accountNumber = accountAccountNumber.value; + } + + if(accountRoutingNumber.value) { + formData.userDetails.routingNumber = accountRoutingNumber.value; + } + + if(accountAccountType.value) { + formData.userDetails.accountType = accountAccountType.value; + } + + if(accountAccountHolderName.value) { + formData.userDetails.accountHolderName = accountAccountHolderName.value; + } + + try { + const saveBtn = document.getElementById('saveBankDetailsBtn'); + saveBtn.disabled = true; + saveBtn.textContent = 'Updatind Details...'; + + const response = await fetchWithAuth(`${API_BASE}/profile`, { + method: 'POST', + body: JSON.stringify(formData) + }); + + if (!response) { + saveBtn.disabled = false; + saveBtn.textContent = 'Update Bank Details'; + return; + } + + const result = await response.json(); + if (result.success) { + alert('Bank details updated successfully!'); + closeBankDetailsModal(); + } else { + alert(result.message || 'Failed to update bank details'); + } + } catch (error) { + console.error('Error changing Bank Details:', error); + alert('Error changing bank details'); + } finally { + const saveBtn = document.getElementById('saveBankDetailsBtn'); + saveBtn.disabled = false; + saveBtn.textContent = 'Update Bank Details'; + } +} + +// Bank account validation +function validateBankAccount(accountNumber) { + return /^\d{8,17}$/.test(accountNumber.replace(/\s/g, '')); +} + +// Routing number validation (basic US routing number check) +function validateRoutingNumber(routingNumber) { + const cleanRouting = routingNumber.replace(/\s/g, ''); + + if (!/^\d{9}$/.test(cleanRouting)) { + return false; + } + + // Check for invalid patterns + if (isInvalidRoutingNumberPattern(cleanRouting)) { + return false; + } + + // ABA routing number checksum validation + return isValidABAChecksum(cleanRouting); +} + +function isInvalidRoutingNumberPattern(routingNumber) { + const invalidPatterns = [ + /^0+$/, // All zeros + /^1+$/, // All ones + /^2+$/, // All twos + /^3+$/, // All threes + /^4+$/, // All fours + /^5+$/, // All fives + /^6+$/, // All sixes + /^7+$/, // All sevens + /^8+$/, // All eights + /^9+$/, // All nines + /^012345678/, // Sequential + /^876543210/, // Reverse sequential + ]; + + return invalidPatterns.some(pattern => pattern.test(routingNumber)); +} + +function isValidABAChecksum(routingNumber) { + const digits = routingNumber.split('').map(Number); + + // ABA routing number checksum formula + const sum = (3 * (digits[0] + digits[3] + digits[6])) + + (7 * (digits[1] + digits[4] + digits[7])) + + (1 * (digits[2] + digits[5] + digits[8])); + + return sum % 10 === 0; +} + +// Expiry date validation +function validateExpiryDate(expiryDate) { + const expiryRegex = /^(0[1-9]|1[0-2])\/([0-9]{2})$/; + if (!expiryRegex.test(expiryDate)) { + return false; + } + + const [month, year] = expiryDate.split('/'); + const expiry = new Date(2000 + parseInt(year), parseInt(month)); + const currentDate = new Date(); + const currentMonth = new Date(currentDate.getFullYear(), currentDate.getMonth()); + + return expiry >= currentMonth; +} + +// CVV validation +function validateCVV(cvv, cardType) { + if (!/^\d+$/.test(cvv)) { + return false; + } + + const cvvLengths = { + visa: 3, + mastercard: 3, + amex: 4, + discover: 3, + diners: 3, + jcb: 3 + }; + + const expectedLength = cvvLengths[cardType] || 3; + return cvv.length === expectedLength; +} + +// ZIP code validation +function validateZipCodeRegex(zipCode) { + return /^\d{5}(-\d{4})?$/.test(zipCode); +} + +function validateCardNumber(cardNumber) { + const cleanNumber = cardNumber.replace(/[\s-]/g, ''); + + // Check if it's all digits and has proper length + if (!/^\d+$/.test(cleanNumber) || cleanNumber.length < 13 || cleanNumber.length > 19) { + return false; + } + + // Check for invalid patterns + if (isInvalidCardPattern(cleanNumber)) { + return false; + } + + // Check for invalid BIN ranges + if (hasInvalidBIN(cleanNumber)) { + return false; + } + + // Luhn algorithm + return isValidLuhn(cleanNumber); +} + +// Enhanced Luhn algorithm +function isValidLuhn(cardNumber) { + let sum = 0; + let isEven = false; + + for (let i = cardNumber.length - 1; i >= 0; i--) { + let digit = parseInt(cardNumber.charAt(i)); + + if (isEven) { + digit *= 2; + if (digit > 9) { + digit -= 9; + } + } + + sum += digit; + isEven = !isEven; + } + + return sum % 10 === 0; +} + +// Function to check for invalid card patterns +function isInvalidCardPattern(cardNumber) { + const invalidPatterns = [ + /^0+$/, // All zeros + /^1+$/, // All ones + /^2+$/, // All twos + /^3+$/, // All threes + /^4+$/, // All fours + /^5+$/, // All fives + /^6+$/, // All sixes + /^7+$/, // All sevens + /^8+$/, // All eights + /^9+$/, // All nines + /^0123/, // Sequential + /^1234/, // Sequential + ]; + + return invalidPatterns.some(pattern => pattern.test(cardNumber)); +} + +// Function to check for invalid BIN ranges +function hasInvalidBIN(cardNumber) { + const firstSix = cardNumber.substring(0, 6); + const firstFour = cardNumber.substring(0, 4); + const firstTwo = cardNumber.substring(0, 2); + + // Invalid BIN ranges + const invalidBINs = [ + '000000', '000001', '000002', '000003', // Test cards should be handled separately + '111111', '222222', '333333', '444444', + '555555', '666666', '777777', '888888', + '999999', '123456', '654321' + ]; + + // Check specific invalid ranges + if (invalidBINs.includes(firstSix)) { + return true; + } + + // Check for known invalid starting digits + if (['0', '3', '6', '7', '8'].includes(firstTwo)) { + // 0 - Reserved + // 3 - Should only be 34, 37 (AMEX) + // 6 - Should follow Discover patterns + // 7, 8 - Not currently assigned to major networks + if (!isValidCardPrefix(cardNumber)) { + return true; + } + } + + return false; +} + +function isValidCardPrefix(cardNumber) { + const type = detectCardType(cardNumber); + return type !== 'unknown'; +} + +// Event listeners +document.addEventListener('DOMContentLoaded', function () { + loadProfileData(); + + // Change password button + document.getElementById('changePasswordBtn').addEventListener('click', openPasswordModal); + + document.getElementById('updateBankDetailsBtn').addEventListener('click', openBankDetailsModal); + + // Password form submission + document.getElementById('passwordForm').addEventListener('submit', changePassword); + + document.getElementById('payment-form').addEventListener('submit', changeBankDetails); + + // Close modal when clicking outside + document.getElementById('passwordModal').addEventListener('click', function (e) { + if (e.target === this) { + closePasswordModal(); + } + }); + + document.getElementById('bankDetailsModal').addEventListener('click', function(e) { + if (e.target === this) { + closeBankDetailsModal(); + } + }); + + // Close modal with escape key + document.addEventListener('keydown', function (e) { + if (e.key === 'Escape') { + closePasswordModal(); + closeLogoutModal(); + closeBankDetailsModal(); + } + }); + + 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'); + }); +}); + +// Function to select payment option +function selectPaymentOption(type) { + // Remove selected class from all options + document.querySelectorAll('.payment-option').forEach(option => { + option.classList.remove('selected'); + }); + + document.querySelectorAll('.payment-details').forEach(details => { + details.classList.remove('active'); + // Remove required attributes from all payment fields + details.querySelectorAll('[required]').forEach(field => { + field.removeAttribute('required'); + }); + }); + + // Add selected class to clicked option + event.currentTarget.classList.add('selected'); + + if (type === 'card') { + document.getElementById('card-details').classList.add('active'); + // Add required attributes only to card fields + document.getElementById('card-number').setAttribute('required', 'true'); + document.getElementById('expiry-date').setAttribute('required', 'true'); + document.getElementById('security-code').setAttribute('required', 'true'); + document.getElementById('card-zip').setAttribute('required', 'true'); + + } else if (type === 'bank') { + document.getElementById('bank-details').classList.add('active'); + // Add required attributes only to bank fields + document.getElementById('bank-name').setAttribute('required', 'true'); + document.getElementById('account-number').setAttribute('required', 'true'); + document.getElementById('routing-number').setAttribute('required', 'true'); + document.getElementById('account-type').setAttribute('required', 'true'); + document.getElementById('account-holder-name').setAttribute('required', 'true'); + } +} + + +// Bank Name validation +function validateBankName(input) { + const errorElement = document.getElementById('bank-name-error'); + const value = input.value; + + // Remove numbers and invalid special characters + const cleanValue = value.replace(/[^a-zA-Z\s\-'&.,()]/g, ''); + + if (value !== cleanValue) { + input.value = cleanValue; + } + + // Check content + const contentCheck = value.replace(/[\s\-'&.,()]/g, ''); + const hasOnlyNumbers = /^[0-9]+$/.test(contentCheck) && contentCheck.length > 0; + const hasOnlySpecialChars = /^[^a-zA-Z0-9]+$/.test(contentCheck) && contentCheck.length > 0; + const hasLetters = /[a-zA-Z]/.test(value); + + if (value !== cleanValue && value.length > 0) { + errorElement.textContent = 'Bank name can only contain letters, spaces, and common punctuation'; + errorElement.style.display = 'block'; + input.classList.add('field-error'); + return false; + } else if (hasOnlyNumbers) { + errorElement.textContent = 'Bank name cannot be only numbers'; + errorElement.style.display = 'block'; + input.classList.add('field-error'); + return false; + } else if (hasOnlySpecialChars) { + errorElement.textContent = 'Bank name cannot be only special characters'; + errorElement.style.display = 'block'; + input.classList.add('field-error'); + return false; + } else if (!hasLetters && value.trim().length > 0) { + errorElement.textContent = 'Bank name must include letters'; + errorElement.style.display = 'block'; + input.classList.add('field-error'); + return false; + } else { + errorElement.style.display = 'none'; + input.classList.remove('field-error'); + return true; + } +} + +function preventInvalidBankNameChars(event) { + const charCode = event.which ? event.which : event.keyCode; + const key = event.key; + + if ((charCode >= 65 && charCode <= 90) || + (charCode >= 97 && charCode <= 122) || + charCode === 32 || charCode === 45 || charCode === 39 || + charCode === 38 || charCode === 46 || charCode === 44 || + charCode === 40 || charCode === 41 || + charCode === 8 || charCode === 9 || charCode === 37 || + charCode === 39 || charCode === 46 || charCode === 16 || + charCode === 17 || charCode === 18 || charCode === 91 || + /^[a-zA-Z\s\-'&.,()]$/.test(key)) { + return true; + } + + event.preventDefault(); + return false; +} + +// Enhanced Account Number validation +function validateAccountNumber(input) { + const errorElement = document.getElementById('account-number-error'); + const value = input.value.replace(/\s/g, ''); + + // Remove any non-digit characters + const cleanValue = value.replace(/\D/g, ''); + + if (value !== cleanValue) { + input.value = cleanValue; + } + + // Validate account number + const isValid = validateAccountNumberStrict(cleanValue); + + if (!isValid && cleanValue.length > 0) { + errorElement.textContent = getAccountNumberErrorMessage(cleanValue); + errorElement.style.display = 'block'; + input.classList.add('field-error'); + return false; + } else { + errorElement.style.display = 'none'; + input.classList.remove('field-error'); + return true; + } +} + +function preventInvalidAccountNumberChars(event) { + const charCode = event.which ? event.which : event.keyCode; + + if ((charCode >= 48 && charCode <= 57) || + charCode === 8 || charCode === 9 || charCode === 37 || + charCode === 39 || charCode === 46 || charCode === 16 || + charCode === 17 || charCode === 18 || charCode === 91) { + return true; + } + + event.preventDefault(); + return false; +} + +function validateAccountNumberStrict(accountNumber) { + if (!accountNumber || !/^\d+$/.test(accountNumber)) { + return false; + } + + // Check length + if (accountNumber.length < 8 || accountNumber.length > 17) { + return false; + } + + // Check for invalid patterns + if (isInvalidAccountNumberPattern(accountNumber)) { + return false; + } + + // Simple checksum validation (basic example) + if (!isValidAccountNumberChecksum(accountNumber)) { + return false; + } + + return true; +} + +function isInvalidAccountNumberPattern(accountNumber) { + const invalidPatterns = [ + /^0+$/, // All zeros + /^1+$/, // All ones + /^2+$/, // All twos + /^3+$/, // All threes + /^4+$/, // All fours + /^5+$/, // All fives + /^6+$/, // All sixes + /^7+$/, // All sevens + /^8+$/, // All eights + /^9+$/, // All nines + /^12345678/, // Sequential + /^87654321/, // Reverse sequential + ]; + + return invalidPatterns.some(pattern => pattern.test(accountNumber)); +} + +function isValidAccountNumberChecksum(accountNumber) { + // Basic checksum validation - you might want to implement + // a more specific algorithm based on bank requirements + let sum = 0; + for (let i = 0; i < accountNumber.length; i++) { + sum += parseInt(accountNumber.charAt(i)); + } + + // Simple check: sum shouldn't be too low (e.g., all zeros) + return sum > 0; +} + +function getAccountNumberErrorMessage(accountNumber) { + if (!accountNumber) { + return 'Account number is required'; + } + + if (!/^\d+$/.test(accountNumber)) { + return 'Account number must contain only numbers'; + } + + if (accountNumber.length < 8) { + return 'Account number must be at least 8 digits'; + } + + if (accountNumber.length > 17) { + return 'Account number cannot exceed 17 digits'; + } + + if (isInvalidAccountNumberPattern(accountNumber)) { + return 'Please enter a valid account number'; + } + + return 'Please enter a valid account number'; +} + +// Account Holder Name validation +function validateAccountHolderName(input) { + const errorElement = document.getElementById('account-holder-name-error'); + const value = input.value; + + // Remove numbers and special characters (keep only letters and spaces) + const cleanValue = value.replace(/[^a-zA-Z\s]/g, ''); + + if (value !== cleanValue) { + input.value = cleanValue; + } + + // Check content + const hasOnlyNumbers = /^[0-9]+$/.test(value.replace(/\s/g, '')); + const hasOnlySpecialChars = /^[^a-zA-Z0-9]+$/.test(value.replace(/\s/g, '')); + const hasLetters = /[a-zA-Z]/.test(value); + + if (value !== cleanValue && value.length > 0) { + errorElement.textContent = 'Account holder name can only contain letters and spaces'; + errorElement.style.display = 'block'; + input.classList.add('field-error'); + return false; + } else if (hasOnlyNumbers) { + errorElement.textContent = 'Account holder name cannot contain only numbers'; + errorElement.style.display = 'block'; + input.classList.add('field-error'); + return false; + } else if (hasOnlySpecialChars) { + errorElement.textContent = 'Account holder name cannot contain only special characters'; + errorElement.style.display = 'block'; + input.classList.add('field-error'); + return false; + } else if (!hasLetters && value.trim().length > 0) { + errorElement.textContent = 'Account holder name must include letters'; + errorElement.style.display = 'block'; + input.classList.add('field-error'); + return false; + } else { + errorElement.style.display = 'none'; + input.classList.remove('field-error'); + return true; + } +} + +function preventInvalidAccountHolderChars(event) { + const charCode = event.which ? event.which : event.keyCode; + const key = event.key; + + if ((charCode >= 65 && charCode <= 90) || + (charCode >= 97 && charCode <= 122) || + charCode === 32 || + charCode === 8 || charCode === 9 || charCode === 37 || + charCode === 39 || charCode === 46 || charCode === 16 || + charCode === 17 || charCode === 18 || charCode === 91 || + /^[a-zA-Z\s]$/.test(key)) { + return true; + } + + event.preventDefault(); + return false; +} + + +function preventNonNumericZip(event) { + const charCode = event.which ? event.which : event.keyCode; + const key = event.key; + const currentValue = event.target.value; + + // Allow: numbers, hyphens (only in specific positions), and editing keys + if ((charCode >= 48 && charCode <= 57) || // 0-9 + (charCode === 45 && currentValue.length === 5 && !currentValue.includes('-')) || // hyphen only after 5 digits + charCode === 8 || // backspace + charCode === 9 || // tab + charCode === 37 || // left arrow + charCode === 39 || // right arrow + charCode === 46 || // delete + charCode === 16 || // shift + charCode === 17 || // ctrl + charCode === 18 || // alt + charCode === 91) { // cmd (Mac) + return true; + } + + // Prevent all other characters including letters and special characters + event.preventDefault(); + return false; +} + +// Function to validate ZIP code in real-time +function validateZipCode(input) { + let errorElement; + + // Determine which error element to use based on input ID + if (input.id === 'billing-zip') { + errorElement = document.getElementById('billing-zip-error'); + } else if (input.id === 'card-zip') { + errorElement = document.getElementById('card-zip-error'); + } else { + errorElement = document.getElementById('zip-error'); + } + + const value = input.value; + + // Remove any non-numeric characters except hyphens + const cleanValue = value.replace(/[^0-9\-]/g, ''); + + // If invalid characters were removed, update the input value + if (value !== cleanValue) { + input.value = cleanValue; + } + + // Format ZIP code as user types (add hyphen after 5 digits) + if (cleanValue.length === 5 && !cleanValue.includes('-')) { + input.value = cleanValue; + } else if (cleanValue.length > 5 && !cleanValue.includes('-')) { + input.value = cleanValue.substring(0, 5) + '' + cleanValue.substring(5, 9); + } + + + if (input.value.length < 4) { + errorElement.style.display = 'block'; + input.classList.add('field-error'); + return false; + } else { + errorElement.style.display = 'none'; + input.classList.remove('field-error'); + return true; + } +} + + +// Function to validate CVV in real-time +function validateCVVVV(input) { + const errorElement = document.getElementById('security-code-error'); + const value = input.value; + + // Remove any non-digit characters + const cleanValue = value.replace(/\D/g, ''); + + // If non-digits were removed, update the input value + if (value !== cleanValue) { + input.value = cleanValue; + } + + // Get card type to determine expected CVV length + const cardNumber = document.getElementById('card-number').value; + const cardType = detectCardType(cardNumber); + + // Validate CVV format and content + const isValid = validateCVVStrict(cleanValue, cardType); + + if (!isValid && cleanValue.length > 0) { + errorElement.textContent = getCVVErrorMessage(cleanValue, cardType); + errorElement.style.display = 'block'; + input.classList.add('field-error'); + return false; + } else { + errorElement.style.display = 'none'; + input.classList.remove('field-error'); + return true; + } +} + +// Card type detection +function detectCardType(cardNumber) { + // Remove spaces and dashes + const cleanNumber = cardNumber.replace(/[\s-]/g, ''); + + const cardPatterns = { + visa: /^4[0-9]{12}(?:[0-9]{3})?$/, + mastercard: /^5[1-5][0-9]{14}$/, + amex: /^3[47][0-9]{13}$/, + discover: /^6(?:011|5[0-9]{2})[0-9]{12}$/, + diners: /^3(?:0[0-5]|[68][0-9])[0-9]{11}$/, + jcb: /^(?:2131|1800|35\d{3})\d{11}$/ + }; + + for (const [type, pattern] of Object.entries(cardPatterns)) { + if (pattern.test(cleanNumber)) { + return type; + } + } + return 'unknown'; +} + +// Function to prevent invalid character input in CVV field +function preventInvalidCVVChars(event) { + const charCode = event.which ? event.which : event.keyCode; + const key = event.key; + + // Allow only digits and common editing keys + if ((charCode >= 48 && charCode <= 57) || // 0-9 + charCode === 8 || // backspace + charCode === 9 || // tab + charCode === 37 || // left arrow + charCode === 39 || // right arrow + charCode === 46 || // delete + charCode === 16 || // shift + charCode === 17 || // ctrl + charCode === 18 || // alt + charCode === 91) { // cmd (Mac) + return true; + } + + // Prevent all non-digit characters + event.preventDefault(); + return false; +} + +// Enhanced CVV validation function +function validateCVVStrict(cvv, cardType) { + if (!cvv || !/^\d+$/.test(cvv)) { + return false; + } + + // Check for invalid patterns + if (isInvalidCVVPattern(cvv)) { + return false; + } + + const cvvLengths = { + visa: 3, + mastercard: 3, + amex: 4, + discover: 3, + diners: 3, + jcb: 3 + }; + + const expectedLength = cvvLengths[cardType] || 3; + + // Check length + if (cvv.length !== expectedLength) { + return false; + } + + return true; +} + +// Function to check for invalid CVV patterns +function isInvalidCVVPattern(cvv) { + const invalidPatterns = [ + /^0+$/, // All zeros: 000, 0000 + /^1+$/, // All ones: 111, 1111 + /^2+$/, // All twos: 222, 2222 + /^3+$/, // All threes: 333, 3333 + /^4+$/, // All fours: 444, 4444 + /^5+$/, // All fives: 555, 5555 + /^6+$/, // All sixes: 666, 6666 + /^7+$/, // All sevens: 777, 7777 + /^8+$/, // All eights: 888, 8888 + /^9+$/, // All nines: 999, 9999 + ]; + + return invalidPatterns.some(pattern => pattern.test(cvv)); +} + +// Function to get appropriate CVV error message +function getCVVErrorMessage(cvv, cardType) { + if (!cvv) { + return 'Security code is required'; + } + + if (!/^\d+$/.test(cvv)) { + return 'Security code must contain only numbers'; + } + + if (isInvalidCVVPattern(cvv)) { + return 'Please enter a valid security code'; + } + + const cvvLengths = { + visa: 3, + mastercard: 3, + amex: 4, + discover: 3, + diners: 3, + jcb: 3 + }; + + const expectedLength = cvvLengths[cardType] || 3; + + if (cvv.length !== expectedLength) { + if (cardType === 'amex') { + return 'American Express cards require a 4-digit security code'; + } else { + return `Security code must be ${expectedLength} digits`; + } + } + + return 'Please enter a valid security code'; +} + +document.addEventListener('DOMContentLoaded', function() { + // Real-time card number formatting and validation + const cardNumberInput = document.getElementById('card-number'); + if (cardNumberInput) { + cardNumberInput.addEventListener('input', function(e) { + const cursorPosition = e.target.selectionStart; + const originalLength = e.target.value.length; + // Format card number with spaces + let value = e.target.value.replace(/\s/g, ''); + let formattedValue = ''; + + for (let i = 0; i < value.length; i++) { + if (i > 0 && i % 4 === 0) { + formattedValue += ' '; + } + formattedValue += value[i]; + } + + e.target.value = formattedValue; + + let newCursorPosition = cursorPosition; + + if (formattedValue.length > originalLength) { + const addedChars = formattedValue.length - originalLength; + + // Count how many spaces were added before the cursor position + let spacesAddedBeforeCursor = 0; + for (let i = 0; i < cursorPosition && i < formattedValue.length; i++) { + if (formattedValue[i] === ' ' && (i < cursorPosition || + (i === cursorPosition && formattedValue[i] === ' '))) { + spacesAddedBeforeCursor++; + } + } + + newCursorPosition = cursorPosition + spacesAddedBeforeCursor; + } else if (formattedValue.length < originalLength) { + const removedChars = originalLength - formattedValue.length; + + // Count how many spaces were removed before the cursor position + let spacesRemovedBeforeCursor = 0; + const oldValue = e.target.getAttribute('data-old-value') || ''; + for (let i = 0; i < cursorPosition && i < oldValue.length; i++) { + if (oldValue[i] === ' ' && i < cursorPosition) { + spacesRemovedBeforeCursor++; + } + } + + newCursorPosition = Math.max(0, cursorPosition - spacesRemovedBeforeCursor); + } + + e.target.setAttribute('data-old-value', formattedValue); + e.target.setSelectionRange(newCursorPosition, newCursorPosition); + + // Real-time card type detection + const cardType = detectCardType(value); + if (cardType !== 'unknown') { + // You can add visual feedback for card type here + console.log('Card type:', cardType); + } + }); + + cardNumberInput.addEventListener('keydown', function(e) { + // Allow: backspace, delete, tab, escape, enter, arrows, home, end + if ([8, 9, 13, 27, 37, 38, 39, 40, 46].includes(e.keyCode) || + // Allow: Ctrl+A, Ctrl+C, Ctrl+V, Ctrl+X + (e.keyCode === 65 && e.ctrlKey === true) || + (e.keyCode === 67 && e.ctrlKey === true) || + (e.keyCode === 86 && e.ctrlKey === true) || + (e.keyCode === 88 && e.ctrlKey === true) || + // Allow: numbers and numpad numbers + (e.keyCode >= 48 && e.keyCode <= 57) || + (e.keyCode >= 96 && e.keyCode <= 105)) { + return; + } + + // Prevent all other keys + e.preventDefault(); + }); + } + + // Real-time expiry date formatting + const expiryInput = document.getElementById('expiry-date'); + if (expiryInput) { + expiryInput.addEventListener('input', function(e) { + let value = e.target.value.replace(/\D/g, ''); + if (value.length >= 2) { + value = value.substring(0, 2) + '/' + value.substring(2, 4); + } + e.target.value = value; + }); + } + + // Real-time CVV validation + const cvvInput = document.getElementById('security-code'); + if (cvvInput) { + cvvInput.addEventListener('input', function(e) { + e.target.value = e.target.value.replace(/\D/g, '').substring(0, 4); + }); + } + + +}); + +function showBankError(fieldId = null) { + // Highlight specific field if provided + if (fieldId) { + const field = document.getElementById(fieldId); + if (field) { + field.classList.add('field-error'); + // Remove error after 5 seconds + setTimeout(() => { + field.classList.remove('field-error'); + }, 5000); + } + } +} \ 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/public/js/pages/va-logs.js b/public/js/pages/va-logs.js new file mode 100644 index 0000000..75ce8ef --- /dev/null +++ b/public/js/pages/va-logs.js @@ -0,0 +1,300 @@ +// API base URL +const API_BASE = '/api/v1/client'; + +function toggleAccordion(accordionId) { + const accordion = document.getElementById(accordionId); + accordion.classList.toggle('active'); + + // const allAccordions = document.querySelectorAll('.accordion'); + // allAccordions.forEach(acc => { + // if (acc.id !== accordionId && acc.classList.contains('active')) { + // acc.classList.remove('active'); + // } + // }); +} + +function formatTime(milliseconds) { + if (!milliseconds) return '0m'; + + const seconds = Math.floor(milliseconds / 1000); + const hours = Math.floor(seconds / 3600); + const minutes = Math.floor((seconds % 3600) / 60); + + if (hours > 0) { + return `${hours}h ${minutes}m ${seconds % 60}s`; + } + return `${minutes}m ${seconds % 60}s`; +} + +function formatDate(dateString) { + const date = new Date(dateString); + return date.toLocaleDateString('en-US', { + year: 'numeric', + month: 'short', + day: 'numeric' + }); +} + +function formatTimeOnly(dateString) { + const date = new Date(dateString); + return date.toLocaleTimeString('en-US', { + hour: '2-digit', + minute: '2-digit', + second: '2-digit' + }); +} + +function calculateTotalIndividualLogsHours(workTrackLogs) { + let total = 0; + workTrackLogs.forEach(workTrack => { + if (workTrack.logs && Array.isArray(workTrack.logs)) { + workTrack.logs.forEach(log => { + total += log.totalTime || 0; + }); + } + }); + return total; +} + +function countIndividualLogs(workTrackLogs) { + let count = 0; + workTrackLogs.forEach(workTrack => { + if (workTrack.logs && Array.isArray(workTrack.logs)) { + count += workTrack.logs.length; + } + }); + return count; +} + +function calculateTotalHours(logs) { + return logs.reduce((total, log) => total + (log.totalTime || 0), 0); +} + +// Create project accordion +function createProjectAccordion(projectData, index) { + const workTrackLogs = projectData.workTrackLogs || []; + const totalHours = calculateTotalIndividualLogsHours(workTrackLogs); + const individualLogsCount = countIndividualLogs(workTrackLogs); + const projectName = projectData.projectDetails?.name || `Project ${index + 1}`; + + const accordionId = `project-${index}`; + + return ` +
+
+
+ ${projectName} + ${individualLogsCount} activities +
+
+
+
${formatTime(totalHours)}
+
+ +
+
+
+ ${individualLogsCount > 0 ? createIndividualLogsTable(workTrackLogs) : createNoLogsMessage()} +
+
+ `; +} + +// Create table for individual log entries +function createIndividualLogsTable(workTrackLogs) { + // Collect all individual logs from all work tracks + const allIndividualLogs = []; + workTrackLogs.forEach(workTrack => { + if (workTrack.logs && Array.isArray(workTrack.logs)) { + workTrack.logs.forEach(log => { + allIndividualLogs.push(log); + }); + } + }); + + return ` +
+ + + + + + + + + + + + ${allIndividualLogs.map((log, index) => createIndividualLogRow(log, index)).join('')} + +
TitleApp NameFromToTotal Time
+
+ `; +} + +// Create a single row for individual log entry +function createIndividualLogRow(log, index) { + const title = log.title || `Activity ${index + 1}`; + const appName = log.appName || 'Unknown App'; + + // Parse dates + const fromDate = log.from; + const toDate = log.to || log.from; + + // Calculate duration + let duration = log.totalTime || 0; + if (!duration && fromDate && toDate) { + const from = new Date(fromDate); + const to = new Date(toDate); + duration = Math.floor((to - from) / 1000); + } + + return ` + + + ${title} + + ${appName} + +
+
${formatDate(fromDate)}
+
${formatTimeOnly(fromDate)}
+
+ + +
+
${formatDate(toDate)}
+
${formatTimeOnly(toDate)}
+
+ + + ${formatTime(duration)} + + + `; +} + +// Create no logs message +function createNoLogsMessage() { + return ` +
+ +

No Activity Logs

+

No detailed activity logs available for this project.

+
+ `; +} + +// Load VA logs +async function loadVALogs() { + const loadingState = document.getElementById('loadingState'); + const emptyState = document.getElementById('emptyState'); + const projectsContainer = document.getElementById('projectsContainer'); + + try { + // Get VA ID from URL + const urlParams = new URLSearchParams(window.location.search); + const vaId = urlParams.get('va'); + + if (!vaId) { + window.location.href = '/client/virtual-assistants'; + return; + } + + // Fetch VA logs data + const response = await fetchWithAuth(`${API_BASE}/vaLogsData/${vaId}`); + + if (!response || !response.ok) { + throw new Error('Failed to load VA logs'); + } + + const data = await response.json(); + + if (!data.success) { + throw new Error(data.message || 'Failed to load VA logs'); + } + + // Hide loading state + loadingState.style.display = 'none'; + + const projectLogs = data.data?.projectLogs || []; + + // Handle empty state + if (projectLogs.length === 0) { + emptyState.style.display = 'block'; + projectsContainer.style.display = 'none'; + return; + } + + // Show projects container + projectsContainer.style.display = 'block'; + emptyState.style.display = 'none'; + + // Create accordions for each project + let accordionsHTML = ''; + projectLogs.forEach((project, index) => { + accordionsHTML += createProjectAccordion(project, index); + }); + + projectsContainer.innerHTML = accordionsHTML; + + // Auto-open first accordion + if (projectLogs.length > 0) { + setTimeout(() => { + const firstAccordion = document.getElementById('project-0'); + if (firstAccordion) { + firstAccordion.classList.add('active'); + } + }, 100); + } + + } catch (error) { + console.error("Error loading VA logs:", error); + loadingState.style.display = 'none'; + projectsContainer.style.display = 'none'; + + // Show error in empty state + emptyState.innerHTML = ` + +

Error Loading Data

+

${error.message}

+ + `; + emptyState.style.display = 'block'; + } +} + +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'); + }); + + // 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); + + // Load VA logs on page load + loadVALogs(); +}); + diff --git a/public/js/va-client-register.js b/public/js/va-client-register.js index 23b6c77..4cafb07 100644 --- a/public/js/va-client-register.js +++ b/public/js/va-client-register.js @@ -495,7 +495,7 @@ async function validateStep(step) { const lastName = document.getElementById('last-name').value; const password = document.getElementById('password').value; const confirmPassword = document.getElementById('confirmPassword').value; - if (!validateEmail(email) && !firstName && !lastName && !password && !confirmPassword) { + if (!firstName && !lastName && !password && !confirmPassword) { isValid = false; showError('Fields are Required', null, step); let requiredFields1 = form.querySelectorAll('[required]'); @@ -521,12 +521,12 @@ async function validateStep(step) { } if (!firstName) { isValid = false; - showError('First Name is Required', null, step); + showError('First Name is Required', 'first-name', step); return isValid; } if (!lastName) { isValid = false; - showError('Last Name is Required', null, step); + showError('Last Name is Required', 'last-name', step); return isValid; } @@ -555,6 +555,25 @@ async function validateStep(step) { field.classList.remove('field-error'); } }); + } else if (step === 2) { + const companyName = document.getElementById('company-name').value; + + if (!companyName) { + isValid = false; + showError('Company Name is Required', 'company-name', step); + return isValid; + } + + // Validate other required fields + const requiredFields = form.querySelectorAll('[required]'); + requiredFields.forEach(field => { + if (!field.value.trim()) { + isValid = false; + field.classList.add('field-error'); + } else if (field.id !== 'email') { + field.classList.remove('field-error'); + } + }); } else if (step === 3) { const email = document.getElementById('billing-email').value; if (!validateEmail(email)) { @@ -639,6 +658,7 @@ function saveStepData(step) { formData.userDetails.password = document.getElementById('password').value; break; case 2: + formData.userDetails.name = document.getElementById('company-name').value; formData.userDetails.address = document.getElementById('address').value; formData.userDetails.city = document.getElementById('city').value; formData.userDetails.state = document.getElementById('state').value; @@ -882,28 +902,29 @@ async function detectLocation() { await populateStates(countryCode, 'state'); await populateStates(countryCode, 'billing-state'); - const stateSelect = document.getElementById('state'); - const billingStateSelect = document.getElementById('billing-state'); + // // auto populate state if region matches + // const stateSelect = document.getElementById('state'); + // const billingStateSelect = document.getElementById('billing-state'); - if (stateSelect && region) { - const matchedState = Array.from(stateSelect.options).find( - opt => opt.text.toLowerCase() === region.toLowerCase() - ); - if (matchedState) { - stateSelect.value = matchedState.value; - stateSelect.dispatchEvent(new Event('change')); - } - } + // if (stateSelect && region) { + // const matchedState = Array.from(stateSelect.options).find( + // opt => opt.text.toLowerCase() === region.toLowerCase() + // ); + // if (matchedState) { + // stateSelect.value = matchedState.value; + // stateSelect.dispatchEvent(new Event('change')); + // } + // } - if (billingStateSelect && region) { - const matchedBillingState = Array.from(billingStateSelect.options).find( - opt => opt.text.toLowerCase() === region.toLowerCase() - ); - if (matchedBillingState) { - billingStateSelect.value = matchedBillingState.value; - billingStateSelect.dispatchEvent(new Event('change')); - } - } + // if (billingStateSelect && region) { + // const matchedBillingState = Array.from(billingStateSelect.options).find( + // opt => opt.text.toLowerCase() === region.toLowerCase() + // ); + // if (matchedBillingState) { + // billingStateSelect.value = matchedBillingState.value; + // billingStateSelect.dispatchEvent(new Event('change')); + // } + // } } catch (error) { console.error('Error detecting location:', error); @@ -1092,7 +1113,7 @@ document.addEventListener('DOMContentLoaded', function() { // Handle use business address checkbox document.getElementById('use-business-address').addEventListener('change', function() { if (this.checked) { - document.getElementById('billing-name').value = formData.userDetails.firstName || ''; + document.getElementById('billing-name').value = formData.userDetails.name || ''; document.getElementById('billing-address').value = formData.userDetails.address || ''; document.getElementById('billing-city').value = formData.userDetails.city || ''; document.getElementById('billing-state').value = formData.userDetails.state || ''; @@ -1439,6 +1460,93 @@ function preventInvalidLastNameChars(event) { return false; } +// Function to validate company name +function validateCompanyName(input) { + const errorElement = document.getElementById('company-name-error'); + const value = input.value; + + // Check various invalid patterns + const validationResult = validateCompanyNameContent(value); + + if (!validationResult.isValid && value.trim().length > 0) { + errorElement.textContent = validationResult.message; + errorElement.style.display = 'block'; + input.classList.add('field-error'); + return false; + } else { + errorElement.style.display = 'none'; + input.classList.remove('field-error'); + return true; + } +} + +function validateCompanyNameContent(companyName) { + const trimmed = companyName.trim(); + + if (trimmed.length === 0) { + return { isValid: false, message: 'Company name is required' }; + } + + // Check minimum length + if (trimmed.length < 2) { + return { isValid: false, message: 'Company name must be at least 2 characters' }; + } + + // Remove whitespace for content analysis + const content = trimmed.replace(/\s/g, ''); + + // Check if contains only special characters + if (/^[^a-zA-Z0-9]+$/.test(content)) { + return { isValid: false, message: 'Company name cannot be only special characters' }; + } + + // Check if contains at least some valid content (letters or numbers) + if (!/[a-zA-Z0-9]/.test(content)) { + return { isValid: false, message: 'Company name must contain letters or numbers' }; + } + + // Optional: Check for excessive special characters (more than 50%) + const specialCharCount = (content.match(/[^a-zA-Z0-9]/g) || []).length; + if (specialCharCount > content.length / 2) { + return { isValid: false, message: 'Company name contains too many special characters' }; + } + + return { isValid: true, message: '' }; +} + +// Function to prevent invalid character input in real-time +function preventInvalidCompanyNameChars(event) { + const charCode = event.which ? event.which : event.keyCode; + const key = event.key; + + // Allow: letters, numbers, spaces, hyphens, apostrophes, ampersands, periods, commas + // Also allow common editing keys + if ((charCode >= 65 && charCode <= 90) || // A-Z + (charCode >= 97 && charCode <= 122) || // a-z + (charCode >= 48 && charCode <= 57) || // 0-9 + charCode === 32 || // space + charCode === 45 || // hyphen + charCode === 39 || // apostrophe + charCode === 38 || // ampersand (&) + charCode === 46 || // period (.) + charCode === 44 || // comma (,) + charCode === 8 || // backspace + charCode === 9 || // tab + charCode === 37 || // left arrow + charCode === 39 || // right arrow + charCode === 16 || // shift + charCode === 17 || // ctrl + charCode === 18 || // alt + charCode === 91 || // cmd (Mac) + /^[a-zA-Z0-9\s\-'&.,]$/.test(key)) { // allowed characters + return true; + } + + // Prevent invalid characters + event.preventDefault(); + return false; +} + // Function to validate ZIP code in real-time function validateZipCode(input) { let errorElement; diff --git a/src/controllers/client/vaClient.controller.js b/src/controllers/client/vaClient.controller.js index 87695cc..3ab3184 100644 --- a/src/controllers/client/vaClient.controller.js +++ b/src/controllers/client/vaClient.controller.js @@ -25,10 +25,10 @@ async function va_register_client(req, res) { try { const payload = req.body || {}; const { userDetails } = payload; - if (!userDetails || !userDetails.email || !userDetails.firstName || !userDetails.lastName || !userDetails.password) { + if (!userDetails || !userDetails.email || !userDetails.firstName || !userDetails.lastName || !userDetails.password || !userDetails.name) { return res.status(400).json({ success: false, - message: "Missing required fields: firstName, lastName, email, and password are required" + message: "Missing required fields: firstName, lastName, email, password, and company name are required" }); } diff --git a/src/controllers/va/vaProjects/vaProjectReport.controller.js b/src/controllers/va/vaProjects/vaProjectReport.controller.js index 985be53..92a1d68 100644 --- a/src/controllers/va/vaProjects/vaProjectReport.controller.js +++ b/src/controllers/va/vaProjects/vaProjectReport.controller.js @@ -49,9 +49,38 @@ async function va_projects_report_function(req, res) { // Default: current day range const now = new Date(); - const fromDate = from && from !== "undefined" ? new Date(from) : new Date(now.setHours(0, 0, 0, 0)); - const toDate = to && to !== "undefined" ? new Date(to) : new Date(); - toDate.setHours(23, 59, 59, 999); + let fromDate, toDate; + + if (from && from !== "undefined" && from !== "null") { + fromDate = new Date(from); + // If only date string provided (YYYY-MM-DD), set to start of day + if (from.length === 10) { + fromDate.setHours(0, 0, 0, 0); + } + } else { + // Default: 15 days ago + fromDate = new Date(); + fromDate.setHours(0, 0, 0, 0); + } + + if (to && to !== "undefined" && to !== "null") { + toDate = new Date(to); + // If only date string provided (YYYY-MM-DD), set to end of day + if (to.length === 10) { + toDate.setHours(23, 59, 59, 999); + } + } else { + // Default: today end of day + toDate = new Date(); + toDate.setHours(23, 59, 59, 999); + } + + if (fromDate > toDate) { + return res.status(400).json({ + success: false, + message: "Invalid date range: 'from' date must be before 'to' date" + }); + } // Get all VA login/logout logs for this project in range const logs = await LoggedModel.find({ @@ -61,7 +90,16 @@ async function va_projects_report_function(req, res) { }).sort({ time: 1 }).lean(); if (!logs.length) { - return res.json({ success: true, message: "No logs found for this project", data: [] }); + return res.json({ + success: true, + message: "No logs found for this project in the specified date range", + data: [], + dateRange: { + from: fromDate.toISOString().split('T')[0], + to: toDate.toISOString().split('T')[0], + days: Math.ceil((toDate - fromDate) / (1000 * 60 * 60 * 24)) + } + }); } // Get all work tracks for this VA and project in the date range @@ -70,7 +108,13 @@ async function va_projects_report_function(req, res) { vaId, $or: [ { from: { $gte: fromDate, $lte: toDate } }, - { to: { $gte: fromDate, $lte: toDate } } + { to: { $gte: fromDate, $lte: toDate } }, + { + $and: [ + { from: { $lte: fromDate } }, + { to: { $gte: fromDate } } + ] + } ] }) .sort({ from: 1 }) diff --git a/src/server.js b/src/server.js index 0a2a1b0..d2843be 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({ @@ -139,7 +145,11 @@ 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") + // res.render("client_login") + res.render("client/client_login", { + layout: "layouts/auth", // ⬅️ override layout + title: "DocVA | Login" + }); }) app.get("/client/forgot-password", (req, res) => { @@ -155,7 +165,11 @@ 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'); + res.render('client/dashboard-dashboard', { + title: "Client Dashboard", + pageScript: "dashboard.js" + }); } catch (error) { console.error('Dashboard error:', error); res.status(500).render('client/dashboard-dashboard', { error: 'Failed to load dashboard' }); @@ -165,7 +179,12 @@ 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'); + 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); @@ -176,7 +195,12 @@ 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'); + 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' }); @@ -186,7 +210,11 @@ 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'); + 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' }); @@ -196,7 +224,11 @@ app.get('/client/profile', async (req, res) => { 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'); + // res.render('client/dashboard-manage-va-tips'); + res.render('client/dashboard-manage-va-tips', { + title: 'Manage VA Tips', + pageScript: 'manage-tips.js' + }); } catch (error) { console.error('Projects error:', error); res.status(500).render('client/dashboard-manage-va-tips', { error: 'Failed to load profile' }); @@ -205,7 +237,11 @@ app.get('/client/virtual-assistants/manage-tips', async (req, res) => { app.get('/client/virtual-assistants/view-logs', async (req, res) => { try { - res.render('client/dashboard-va-logs'); + // res.render('client/dashboard-va-logs'); + res.render('client/dashboard-va-logs', { + title: 'VA Logs', + pageScript: 'va-logs.js' + }); } catch (error) { console.error('Projects error:', error); res.status(500).render('client/dashboard-va-logs', { error: 'Failed to load profile' }); @@ -542,13 +578,13 @@ io.on("connection", (socket) => { }); -app.get("/", (req, res) => { - res.render("client_login") -}) +// app.get("/", (req, res) => { +// res.render("client_login") +// }) -app.get("/forgot-password", (req, res) => { - res.render("forgot_password") -}) +// 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" }) diff --git a/src/services/salesforce.service.js b/src/services/salesforce.service.js index b2dcab6..c367f74 100644 --- a/src/services/salesforce.service.js +++ b/src/services/salesforce.service.js @@ -47,6 +47,7 @@ class SalesforceService { return { userDetails: { _id: _id, + name: userDetails.name, firstName: userDetails.firstName, lastName: userDetails.lastName, email: userDetails.email.toLowerCase(), diff --git a/src/services/vaClient.service.js b/src/services/vaClient.service.js index a55deff..d9b99d0 100644 --- a/src/services/vaClient.service.js +++ b/src/services/vaClient.service.js @@ -75,7 +75,7 @@ class VaClientService { // Define allowed fields for update (excluding password) const allowedFields = [ - "firstName", "lastName", "address", "city", "state", "zip", + "name", "firstName", "lastName", "address", "city", "state", "zip", "billingName", "billingEmail", "billingAddress", "billingCity", "billingState", "billingZip", "position", "signatoryName", "signatoryEmail", "paymentMethod", "bankName", "accountNumber", @@ -273,7 +273,7 @@ class VaClientService { userDetails: { firstName: userDetails.firstName, lastName: userDetails.lastName, - name: `${userDetails.firstName} ${userDetails.lastName}`, + name: userDetails.name, email: userDetails.email.toLowerCase(), password: hashedPassword, address: userDetails.address || null, diff --git a/views/client_login.ejs b/views/client/client_login.ejs similarity index 99% rename from views/client_login.ejs rename to views/client/client_login.ejs index d56945b..fe3bf3f 100644 --- a/views/client_login.ejs +++ b/views/client/client_login.ejs @@ -222,7 +222,7 @@
- Forgot password? + Forgot password? diff --git a/views/client/dashboard-dashboard.ejs b/views/client/dashboard-dashboard.ejs index 8c65c78..7d28eba 100644 --- a/views/client/dashboard-dashboard.ejs +++ b/views/client/dashboard-dashboard.ejs @@ -1,557 +1,69 @@ - - - - - - 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 +
+
+
+

Total Projects

+
0
+
+
+ +
+
+
+
+ +
+
+

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..285e987 100644 --- a/views/client/dashboard-manage-va-tips.ejs +++ b/views/client/dashboard-manage-va-tips.ejs @@ -1,548 +1,58 @@ - - - - - - - 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 b656b2a..9176d28 100644 --- a/views/client/dashboard-profile.ejs +++ b/views/client/dashboard-profile.ejs @@ -1,1258 +1,69 @@ - - - - - - DocVA Client | Profile - - - - - -
- -
- -
-
-
-
-

Client Profile

-
- -
- -
-
- -

Personal Information

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

Address Information

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