diff --git a/package-lock.json b/package-lock.json
index 6b6caba..98c8b48 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -20,6 +20,7 @@
"dotenv": "^16.4.7",
"ejs": "^3.1.10",
"express": "^4.19.2",
+ "express-ejs-layouts": "^2.5.1",
"express-minify": "^1.0.0",
"helmet": "^8.1.0",
"html-minifier": "^4.0.0",
@@ -4232,6 +4233,11 @@
"node": ">= 0.10.0"
}
},
+ "node_modules/express-ejs-layouts": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/express-ejs-layouts/-/express-ejs-layouts-2.5.1.tgz",
+ "integrity": "sha512-IXROv9n3xKga7FowT06n1Qn927JR8ZWDn5Dc9CJQoiiaaDqbhW5PDmWShzbpAa2wjWT1vJqaIM1S6vJwwX11gA=="
+ },
"node_modules/express-minify": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/express-minify/-/express-minify-1.0.0.tgz",
diff --git a/package.json b/package.json
index 5b7e2e0..080fdae 100644
--- a/package.json
+++ b/package.json
@@ -25,6 +25,7 @@
"dotenv": "^16.4.7",
"ejs": "^3.1.10",
"express": "^4.19.2",
+ "express-ejs-layouts": "^2.5.1",
"express-minify": "^1.0.0",
"helmet": "^8.1.0",
"html-minifier": "^4.0.0",
diff --git a/public/css/dashboard.css b/public/css/dashboard.css
index 71fea48..6879380 100644
--- a/public/css/dashboard.css
+++ b/public/css/dashboard.css
@@ -213,6 +213,7 @@ body {
transform: translateX(-100%);
transition: .3s ease;
}
+
.dashboard-sidebar.active {
transform: translateX(0);
}
@@ -260,6 +261,53 @@ body {
color: var(--text-primary);
}
+/* ===== FIX DUPLICATE SELECT ICON ISSUE ===== */
+select,
+.select-field {
+ appearance: none !important;
+ -webkit-appearance: none !important;
+ -moz-appearance: none !important;
+
+ background-image: none !important;
+ background-repeat: no-repeat !important;
+ background-position: unset !important;
+}
+
+/* Custom select wrapper */
+.select-wrapper {
+ position: relative;
+ width: 100%;
+}
+
+.void-btn-a {
+ background: tomato;
+ text-decoration: none;
+ color: #ffffff !important;
+ border: none;
+ padding: 0.5rem 1rem;
+ border-radius: 5px;
+ font-weight: bold;
+ text-decoration: none;
+ cursor: pointer;
+}
+
+/* Add spacing for icon */
+.select-wrapper select {
+ width: 100%;
+ padding-right: 44px;
+}
+
+/* Single dropdown icon */
+.select-icon {
+ position: absolute;
+ right: 14px;
+ top: 50%;
+ transform: translateY(-50%);
+ pointer-events: none;
+ color: #aaa;
+ font-size: 14px;
+}
+
[data-theme="dark"] .sidebar-link:hover {
background-color: var(--color-gray-800);
}
@@ -699,6 +747,7 @@ body {
opacity: 0;
transform: scale(0.9) translateY(-20px);
}
+
to {
opacity: 1;
transform: scale(1) translateY(0);
@@ -1214,10 +1263,19 @@ tr[data-vaid]:hover {
background: var(--color-gray-400);
}
+
@keyframes pulse {
- 0% { opacity: 1; }
- 50% { opacity: 0.5; }
- 100% { opacity: 1; }
+ 0% {
+ opacity: 1;
+ }
+
+ 50% {
+ opacity: 0.5;
+ }
+
+ 100% {
+ opacity: 1;
+ }
}
.no-vas-message {
@@ -1627,12 +1685,20 @@ tr[data-vaid]:hover {
/* Additional Dark Mode Styles for Elements Not Covered */
[data-theme="dark"] {
+
/* Ensure all text colors are properly set */
- h1, h2, h3, h4, h5, h6 {
+ h1,
+ h2,
+ h3,
+ h4,
+ h5,
+ h6 {
color: var(--text-primary);
}
- p, span, div {
+ p,
+ span,
+ div {
color: var(--text-primary);
}
@@ -1710,6 +1776,164 @@ tr[data-vaid]:hover {
transform: scale(1);
}
+/* ================================
+ PROFILE DROPDOWN (GLASS STYLE)
+================================ */
+.header-profile {
+ position: relative;
+ display: flex;
+ align-items: center;
+}
+
+.profile-trigger {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+
+ padding: 0.70rem 0.75rem;
+
+ border-radius: 999px;
+
+ cursor: pointer;
+
+ /* NEW */
+ border: 1px solid var(--border-color);
+ background: linear-gradient(135deg, rgba(255, 0, 150, 0.3), rgba(0, 204, 255, 0.3));
+
+ transition:
+ background-color 0.2s ease,
+ border-color 0.2s ease,
+ box-shadow 0.2s ease;
+}
+
+/* Hover effect */
+.profile-trigger:hover {
+ background-color: rgba(0, 0, 0, 0.06);
+ border-color: var(--primary-color);
+}
+
+/* Active (clicked/open) state */
+.profile-trigger.active {
+ background-color: rgba(0, 123, 255, 0.1);
+ border-color: var(--primary-color);
+ box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.15);
+}
+
+
+[data-theme="dark"] .profile-trigger:hover {
+ background-color: rgba(255, 255, 255, 0.08);
+}
+
+/* Avatar */
+.profile-avatar {
+ width: 36px;
+ height: 36px;
+ border-radius: 50%;
+ object-fit: cover;
+ border: 2px solid #3b82f6;
+}
+
+/* Name */
+.profile-name {
+ font-size: 0.875rem;
+ font-weight: 500;
+ color: var(--text-color, #111827);
+}
+
+[data-theme="dark"] .profile-name {
+ color: #e5e7eb;
+}
+
+.profile-trigger i {
+ font-size: 0.7rem;
+ opacity: 0.7;
+}
+
+/* ================================
+ DROPDOWN MENU
+================================ */
+.profile-menu {
+ position: absolute;
+ top: calc(100% + 0.45rem);
+ right: 0;
+ width: 240px;
+ border-radius: 12px;
+ backdrop-filter: blur(16px);
+ background: rgba(255, 255, 255, 0.85);
+ border: 1px solid var(--border-color);
+ box-shadow: 0 20px 40px rgba(0, 0, 0, 0.15);
+ display: none;
+ flex-direction: column;
+ overflow: hidden;
+ z-index: 50;
+}
+
+[data-theme="dark"] .profile-menu {
+ background: rgba(30, 30, 30, 0.9);
+}
+
+/* Show menu on hover */
+.profile-menu {
+ display: none;
+}
+
+.profile-menu.active {
+ display: flex;
+}
+
+
+/* Menu items */
+.profile-menu a,
+.logout-btn {
+ padding: 0.75rem 1rem;
+ display: flex;
+ align-items: center;
+ gap: 0.75rem;
+ font-size: 0.875rem;
+ color: var(--text-color, #111827);
+ background: none;
+ border: none;
+ cursor: pointer;
+ text-decoration: none;
+ transition: background-color 0.15s ease;
+}
+
+.profile-menu a:hover,
+.logout-btn:hover {
+ background-color: rgba(0, 0, 0, 0.05);
+}
+
+[data-theme="dark"] .profile-menu a,
+[data-theme="dark"] .logout-btn {
+ color: #e5e7eb;
+}
+
+[data-theme="dark"] .profile-menu a:hover,
+[data-theme="dark"] .logout-btn:hover {
+ background-color: rgba(255, 255, 255, 0.08);
+}
+
+/* Divider */
+.menu-divider {
+ height: 1px;
+ background: var(--border-color);
+ margin: 0.25rem 0;
+}
+
+/* Logout */
+.logout-btn {
+ color: #dc2626;
+}
+
+.logout-btn i {
+ color: #dc2626;
+}
+
+@media (max-width: 768px) {
+ .profile-name {
+ display: none;
+ }
+}
/* Accordion Styles */
.accordion {
@@ -1803,6 +2027,8 @@ tr[data-vaid]:hover {
border-radius: 20px;
font-size: 0.875rem;
font-weight: 600;
+ display: inline-block;
+ text-align: center;
}
.loading-spinner {
diff --git a/public/images/favicon.ico b/public/images/favicon.ico
new file mode 100644
index 0000000..582df3a
Binary files /dev/null and b/public/images/favicon.ico differ
diff --git a/public/images/favicon.svg b/public/images/favicon.svg
new file mode 100644
index 0000000..feb5a41
--- /dev/null
+++ b/public/images/favicon.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/js/common/auth.js b/public/js/common/auth.js
new file mode 100644
index 0000000..825bc31
--- /dev/null
+++ b/public/js/common/auth.js
@@ -0,0 +1,29 @@
+window.getAuthToken = function () {
+ return localStorage.getItem("knoptpq");
+};
+
+window.fetchWithAuth = async function (url, options = {}) {
+ const token = getAuthToken();
+
+ if (!token) {
+ window.location.href = "/client/login";
+ return;
+ }
+
+ const res = await fetch(url, {
+ ...options,
+ headers: {
+ token,
+ "Content-Type": "application/json",
+ ...options.headers,
+ },
+ });
+
+ if (res.status === 401) {
+ localStorage.clear();
+ window.location.href = "/client/login";
+ return;
+ }
+
+ return res;
+};
diff --git a/public/js/common/header.js b/public/js/common/header.js
new file mode 100644
index 0000000..7939feb
--- /dev/null
+++ b/public/js/common/header.js
@@ -0,0 +1,29 @@
+document.addEventListener('DOMContentLoaded', () => {
+ const profileTrigger = document.querySelector('.profile-trigger');
+ const profileMenu = document.querySelector('.profile-menu');
+
+ if (!profileTrigger || !profileMenu) return;
+
+ // Toggle on click
+ profileTrigger.addEventListener('click', (e) => {
+ e.stopPropagation();
+ profileMenu.classList.toggle('active');
+ });
+
+ // Close on outside click
+ document.addEventListener('click', () => {
+ profileMenu.classList.remove('active');
+ });
+
+ // Prevent closing when clicking inside menu
+ profileMenu.addEventListener('click', (e) => {
+ e.stopPropagation();
+ });
+
+ // Close on ESC
+ document.addEventListener('keydown', (e) => {
+ if (e.key === 'Escape') {
+ profileMenu.classList.remove('active');
+ }
+ });
+});
\ No newline at end of file
diff --git a/public/js/common/logout.js b/public/js/common/logout.js
new file mode 100644
index 0000000..76289c7
--- /dev/null
+++ b/public/js/common/logout.js
@@ -0,0 +1,23 @@
+function openLogoutModal() {
+ document.getElementById("logoutModal").style.display = "flex";
+ document.body.style.overflow = "hidden";
+}
+
+function closeLogoutModal() {
+ document.getElementById("logoutModal").style.display = "none";
+ document.body.style.overflow = "auto";
+}
+
+async function handleLogout() {
+ try {
+ await fetchWithAuth("/api/v1/client/logout", { method: "POST" });
+ } finally {
+ localStorage.clear();
+ window.location.href = "/client/login";
+ }
+}
+
+document.addEventListener("DOMContentLoaded", () => {
+ document.getElementById("logoutButton")?.addEventListener("click", openLogoutModal);
+ document.getElementById("confirmLogoutBtn")?.addEventListener("click", handleLogout);
+});
diff --git a/public/js/common/sidebar.js b/public/js/common/sidebar.js
new file mode 100644
index 0000000..c6922e5
--- /dev/null
+++ b/public/js/common/sidebar.js
@@ -0,0 +1,90 @@
+const currentPath = window.location.pathname;
+document.querySelectorAll('.nav-button').forEach(link => {
+ if (link.getAttribute('href') === currentPath) {
+ link.classList.add('active');
+ } else {
+ link.classList.remove('active');
+ }
+ });
+
+
+ document.querySelectorAll('.sidebar-link').forEach(link => {
+ if (link.getAttribute('href') === currentPath) {
+ link.classList.add('active');
+ } else {
+ link.classList.remove('active');
+ }
+ });
+
+// Auth helpers
+function getAuthToken() {
+ return localStorage.getItem("knoptpq");
+}
+
+async function fetchWithAuth(url, options = {}) {
+ const token = getAuthToken();
+ if (!token) {
+ window.location.href = "/client/login";
+ return;
+ }
+
+ const res = await fetch(url, {
+ ...options,
+ headers: {
+ "token": token,
+ "Content-Type": "application/json",
+ ...options.headers,
+ }
+ });
+
+ if (res.status === 401) {
+ localStorage.clear();
+ window.location.href = "/client/login";
+ return;
+ }
+
+ return res;
+}
+
+// Logout modal
+function openLogoutModal() {
+ document.getElementById("logoutModal").style.display = "flex";
+ document.body.style.overflow = "hidden";
+}
+
+function closeLogoutModal() {
+ document.getElementById("logoutModal").style.display = "none";
+ document.body.style.overflow = "auto";
+}
+
+// Final logout
+async function handleLogout() {
+ try {
+ await fetchWithAuth("/api/v1/client/logout", { method: "POST" });
+ } catch (e) {
+ console.error("Logout error", e);
+ } finally {
+ localStorage.clear();
+ window.location.href = "/client/login";
+ }
+}
+
+// DOM bindings
+document.addEventListener("DOMContentLoaded", () => {
+
+ // Logout button
+ const logoutBtn = document.getElementById("logoutButton");
+ if (logoutBtn) logoutBtn.addEventListener("click", openLogoutModal);
+
+ // Confirm logout
+ const confirmBtn = document.getElementById("confirmLogoutBtn");
+ if (confirmBtn) confirmBtn.addEventListener("click", handleLogout);
+
+ // Click outside modal
+ const modal = document.getElementById("logoutModal");
+ if (modal) {
+ modal.addEventListener("click", (e) => {
+ if (e.target === modal) closeLogoutModal();
+ });
+ }
+});
diff --git a/public/js/common/theme.js b/public/js/common/theme.js
new file mode 100644
index 0000000..554ff85
--- /dev/null
+++ b/public/js/common/theme.js
@@ -0,0 +1,24 @@
+(function () {
+ const root = document.documentElement;
+
+ function applyTheme(theme) {
+ root.setAttribute("data-theme", theme);
+ localStorage.setItem("theme", theme);
+ }
+
+ function getSystemTheme() {
+ return window.matchMedia("(prefers-color-scheme: dark)").matches
+ ? "dark"
+ : "light";
+ }
+
+ function initTheme() {
+ const savedTheme = localStorage.getItem("theme");
+ applyTheme(savedTheme || getSystemTheme());
+ }
+
+ // expose globally
+ window.setTheme = applyTheme;
+
+ document.addEventListener("DOMContentLoaded", initTheme);
+})();
diff --git a/public/js/pages/dashboard-virtual-assistants.js b/public/js/pages/dashboard-virtual-assistants.js
new file mode 100644
index 0000000..c6ac760
--- /dev/null
+++ b/public/js/pages/dashboard-virtual-assistants.js
@@ -0,0 +1,263 @@
+const API_BASE = "/api/v1/client";
+
+// Format time function
+function formatTime(seconds) {
+ if (!seconds) return 'No Record Found';
+
+ const hours = Math.floor(seconds / 3600);
+ const minutes = Math.floor((seconds % 3600) / 60);
+
+ if (hours > 0) {
+ return `${hours}h ${minutes}m`;
+ } else if (minutes > 0) {
+ return `${minutes}m`;
+ } else {
+ return 'Just started';
+ }
+}
+
+const vaModal = document.getElementById('vaModal');
+
+function openVAModal(va) {
+ // Set basic VA info
+ document.getElementById('modalVAName').textContent = va.name;
+
+ // Set projects
+ const projects = va.projects || [];
+ document.getElementById('projectsCountBadge').textContent = projects.length;
+
+ const projectsTableBody = document.getElementById('modalProjectsTableBody');
+ const noProjectsMessage = document.getElementById('modalNoProjects');
+
+ if (projects.length > 0) {
+ projectsTableBody.innerHTML = projects.map(project => createProjectTableRow(project)).join('');
+ projectsTableBody.style.display = '';
+ noProjectsMessage.style.display = 'none';
+ } else {
+ projectsTableBody.innerHTML = '';
+ projectsTableBody.style.display = 'none';
+ noProjectsMessage.style.display = 'block';
+ }
+
+ // Set action button
+ const modalActions = document.getElementById('modalActions');
+ const viewSessionBtn = document.getElementById('viewSessionBtn');
+
+ if (va.isCurrentlyActive && va.lastProjectId) {
+ viewSessionBtn.href = `/live/${va.lastProjectId}/${va.id}`;
+ modalActions.style.display = 'block';
+ } else {
+ modalActions.style.display = 'none';
+ }
+
+ // Show modal
+ vaModal.style.display = 'flex';
+ document.body.style.overflow = 'hidden';
+}
+
+function closeVAModal() {
+ vaModal.style.display = 'none';
+ document.body.style.overflow = 'auto';
+}
+
+// Create project table row for modal
+function createProjectTableRow(project) {
+ const statusClass = project.isCurrentlyWorking ? 'active' : 'inactive';
+ const statusText = project.isCurrentlyWorking ? 'Working' : 'Not Active';
+
+ return `
+
+ |
+
+ |
+
+ $${project.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 = `
+
+ `;
+ 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}
+
+ |
+
+
+ |
+
+ `;
+}
+
+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 = `
+
+ `;
+ projectsEmptyState.style.display = 'block';
+
+ vaEmptyState.innerHTML = `
+
+ `;
+ 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 `
+
+
+
+ ${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 `
+
+
+
+
+ | Title |
+ App Name |
+ From |
+ To |
+ Total Time |
+
+
+
+ ${allIndividualLogs.map((log, index) => createIndividualLogRow(log, index)).join('')}
+
+
+
+ `;
+}
+
+// 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 @@