From 68a248c25a3818b940c5f83805c30e79332c27d4 Mon Sep 17 00:00:00 2001 From: Aakash Date: Fri, 19 Dec 2025 18:10:39 +0530 Subject: [PATCH] merge ejs updated layout, fixed api --- package-lock.json | 6 + package.json | 1 + public/css/dashboard.css | 236 ++- public/images/favicon.ico | Bin 0 -> 445405 bytes public/images/favicon.svg | 1 + public/js/common/auth.js | 29 + public/js/common/header.js | 29 + public/js/common/logout.js | 23 + public/js/common/sidebar.js | 90 ++ public/js/common/theme.js | 24 + .../js/pages/dashboard-virtual-assistants.js | 263 ++++ public/js/pages/dashboard.js | 142 ++ public/js/pages/manage-tips.js | 248 ++++ public/js/pages/profile.js | 1241 ++++++++++++++++ public/js/pages/projects.js | 240 +++ public/js/pages/va-logs.js | 300 ++++ public/js/va-client-register.js | 156 +- src/controllers/client/vaClient.controller.js | 4 +- .../vaProjects/vaProjectReport.controller.js | 54 +- src/server.js | 62 +- src/services/salesforce.service.js | 1 + src/services/vaClient.service.js | 4 +- views/{ => client}/client_login.ejs | 2 +- views/client/dashboard-dashboard.ejs | 614 +------- views/client/dashboard-manage-va-tips.ejs | 598 +------- views/client/dashboard-profile.ejs | 1299 +---------------- views/client/dashboard-projects.ejs | 532 +------ views/client/dashboard-va-logs.ejs | 534 +------ views/client/dashboard-virtual-assistants.ejs | 696 +-------- views/layouts/auth.ejs | 3 +- views/layouts/dashboard.ejs | 54 + views/partials/bankDetails-modal.ejs | 105 ++ views/partials/header.ejs | 64 + views/partials/logout-modal.ejs | 21 + views/partials/password-modal.ejs | 33 + views/partials/projects-modal.ejs | 39 + views/partials/sidebar.ejs | 46 + views/partials/tip-modal.ejs | 36 + views/partials/va-modal.ejs | 26 + views/partials/virtual-assistants-modal.ejs | 49 + views/va_client_register.ejs | 9 + 41 files changed, 3870 insertions(+), 4044 deletions(-) create mode 100644 public/images/favicon.ico create mode 100644 public/images/favicon.svg create mode 100644 public/js/common/auth.js create mode 100644 public/js/common/header.js create mode 100644 public/js/common/logout.js create mode 100644 public/js/common/sidebar.js create mode 100644 public/js/common/theme.js create mode 100644 public/js/pages/dashboard-virtual-assistants.js create mode 100644 public/js/pages/dashboard.js create mode 100644 public/js/pages/manage-tips.js create mode 100644 public/js/pages/profile.js create mode 100644 public/js/pages/projects.js create mode 100644 public/js/pages/va-logs.js rename views/{ => client}/client_login.ejs (99%) create mode 100644 views/layouts/dashboard.ejs create mode 100644 views/partials/bankDetails-modal.ejs create mode 100644 views/partials/header.ejs create mode 100644 views/partials/logout-modal.ejs create mode 100644 views/partials/password-modal.ejs create mode 100644 views/partials/projects-modal.ejs create mode 100644 views/partials/sidebar.ejs create mode 100644 views/partials/tip-modal.ejs create mode 100644 views/partials/va-modal.ejs create mode 100644 views/partials/virtual-assistants-modal.ejs 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 0000000000000000000000000000000000000000..582df3a22865c7b880075cf24dc9f5133a280bc3 GIT binary patch literal 445405 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
    7ch?)xIPxHn!WVt+$?Ze9-fUw~+zLJ7|I&HZom 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 literal 0 HcmV?d00001 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

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