Fixed bugs

This commit is contained in:
Aakash Mittal 2025-11-24 13:22:25 +05:30
parent 904d9a1a93
commit 0867a1188f
10 changed files with 413 additions and 134 deletions

View File

@ -962,3 +962,136 @@ tr[data-vaid]:hover {
text-align: left;
}
}
/* Modal Styles */
.modal-overlay {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
z-index: 1000;
justify-content: center;
align-items: center;
}
.modal-content {
background: white;
border-radius: 8px;
padding: 20px;
max-width: 500px;
width: 90%;
max-height: 80vh;
overflow-y: auto;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.modal-header {
display: flex;
justify-content: between;
align-items: center;
margin-bottom: 15px;
border-bottom: 1px solid #e5e7eb;
padding-bottom: 10px;
}
.modal-title {
font-size: 1.25rem;
font-weight: 600;
margin: 0;
}
.close-modal {
background: none;
border: none;
font-size: 1.5rem;
cursor: pointer;
color: #6b7280;
}
.close-modal:hover {
color: #374151;
}
.va-list {
display: flex;
flex-direction: column;
gap: 10px;
}
.va-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px;
border: 1px solid #e5e7eb;
border-radius: 6px;
background: #f9fafb;
}
.va-info-modal {
display: flex;
flex-direction: column;
}
.va-name-modal {
font-weight: 600;
color: #374151;
}
.va-email-modal {
font-size: 0.875rem;
color: #6b7280;
}
.va-status {
padding: 4px 8px;
border-radius: 12px;
font-size: 0.75rem;
font-weight: 600;
}
.status-active {
background: #dcfce7;
color: #166534;
}
.status-inactive {
background: #f3f4f6;
color: #6b7280;
}
/* Project row hover effect */
.project-row {
cursor: pointer;
transition: background-color 0.2s ease;
}
.project-row:hover {
background-color: #f8fafc;
}
/* Logout button styles */
.logout-button {
background: none;
border: none;
color: #ef4444;
cursor: pointer;
padding: 10px;
border-radius: 6px;
transition: background-color 0.2s ease;
text-align: left;
width: 100%;
}
.logout-button:hover {
background-color: #fef2f2;
}
.logout-container {
margin-top: auto;
padding: 20px;
border-top: 1px solid #e5e7eb;
}

View File

@ -690,17 +690,50 @@ async function submitForm() {
saveStepData(currentStep);
// Save payment details based on selected method
if (formData.userDetails.paymentMethod === 'card') {
formData.userDetails.cardNumber = document.getElementById('card-number').value;
formData.userDetails.expiryDate = document.getElementById('expiry-date').value;
formData.userDetails.securityCode = document.getElementById('security-code').value;
formData.userDetails.cardZip = document.getElementById('card-zip').value;
} else if (formData.userDetails.paymentMethod === 'bank') {
formData.userDetails.bankName = document.getElementById('bank-name').value;
formData.userDetails.accountNumber = document.getElementById('account-number').value;
formData.userDetails.routingNumber = document.getElementById('routing-number').value;
formData.userDetails.accountType = document.getElementById('account-type').value;
formData.userDetails.accountHolderName = document.getElementById('account-holder-name').value;
let cardNumber = document.getElementById('card-number');
let expiryDate = document.getElementById('expiry-date');
let securityCode = document.getElementById('security-code');
let cardZip = document.getElementById('card-zip').value;
let bankName = document.getElementById('bank-name').value;
let accountNumber = document.getElementById('account-number').value;
let routingNumber = document.getElementById('routing-number').value;
let accountType = document.getElementById('account-type').value;
let accountHolderName = document.getElementById('account-holder-name').value;
if(cardNumber.value) {
formData.userDetails.cardNumber = cardNumber.value;
}
if(expiryDate.value) {
formData.userDetails.expiryDate = expiryDate.value;
}
if(securityCode.value) {
formData.userDetails.securityCode = securityCode.value;
}
if(cardZip.value) {
formData.userDetails.cardZip = cardZip.value;
}
if(bankName.value) {
formData.userDetails.bankName = bankName.value;
}
if(accountNumber.value) {
formData.userDetails.accountNumber = accountNumber.value;
}
if(routingNumber.value) {
formData.userDetails.routingNumber = routingNumber.value;
}
if(accountType.value) {
formData.userDetails.accountType = accountType.value;
}
if(accountHolderName.value) {
formData.userDetails.accountHolderName = accountHolderName.value;
}
// Show loading state

View File

@ -71,7 +71,7 @@
document.addEventListener('DOMContentLoaded', function() {
// Populate country dropdown
const countrySelect = document.getElementById('country');
countrySelect.innerHTML = '<option value=""></option>';
countrySelect.innerHTML = '<option value="" disabled selected></option>';
countries.forEach(country => {
const option = document.createElement('option');
@ -89,7 +89,7 @@
document.getElementById('state-loading').style.display = 'block';
// Clear current states
stateSelect.innerHTML = '<option value=""></option>';
stateSelect.innerHTML = '<option value="" disabled selected></option>';
// Simulate API delay
setTimeout(() => {
@ -393,7 +393,7 @@
// Age validation
const age = document.getElementById('age').value;
if (age && (age < 18 || age > 100)) {
showError('You age must be 18 years or older to register', 'dob', step);
showError('You age must be 18 years or older to register', 'age', step);
isValid = false;
}
@ -539,9 +539,10 @@
const paymentMethod = selectedPayment.getAttribute('onclick').match(/'([^']+)'/)[1];
formData.userDetails.paymentMethod = paymentMethod;
if (paymentMethod === 'payoneer') {
if (document.getElementById('payoneerAccount').value) {
formData.userDetails.payoneerAccount = document.getElementById('payoneerAccount').value;
} else if (paymentMethod === 'veem') {
}
if (document.getElementById('veemAccount').value) {
formData.userDetails.veemAccount = document.getElementById('veemAccount').value;
}
}
@ -742,11 +743,13 @@
// Validate age
if (age < 18) {
// alert('You must be 18 years or older to register');
showError('You Age must be 18 years or older to register', 'dob',);
// showError('You Age must be 18 years or older to register', 'dob');
showError('You Age must be 18 years or older to register', 'age');
ageInput.style.border = '1px solid #ef4444';
} else if (age > 100) {
// alert('Please enter a valid age');
showError('Please enter a valid age', 'dob');
// showError('Please enter a valid age', 'dob');
showError('Please enter a valid age', 'age');
ageInput.style.border = '1px solid #ef4444';
} else {
ageInput.style.border = '';
@ -902,3 +905,14 @@
function getCurrentStep() {
return currentStep;
}
function handleChangeSelect(selectElement) {
const selectedValue = selectElement.value;
let option = selectElement.querySelector('option:disabled');
if (selectedValue !== "") {
option.style.display = "none";
} else {
option.style.display = "";
}
}

View File

@ -1,4 +1,4 @@
import { ClientModel, ProjectModel, VAModel } from "../../../models/vaModel.js";
import { ClientModel, ProjectModel, VAModel, LoggedModel } from "../../../models/vaModel.js";
import jwt from "jsonwebtoken";
function formatHoursToReadable(hours) {
@ -44,7 +44,7 @@ async function client_dashboard_projects(req, res) {
const allAssignedVAs = await VAModel.find({
_id: { $in: clientVaIds }
})
.select('userDetails.firstName userDetails.lastName userDetails.email status')
.select('userDetails.firstName userDetails.lastName userDetails.email')
.lean()
.exec();
@ -56,6 +56,13 @@ async function client_dashboard_projects(req, res) {
.lean()
.exec();
allAssignedVAs.forEach(async (va) => {
const vaLogs = await LoggedModel.find({
vaId: va._id,
vaEmail: va.email
})
})
// Format VAs with only required fields
const vasFormatted = allAssignedVAs.map((va) => ({
id: va._id,

View File

@ -0,0 +1,44 @@
import { ClientModel } from "../../../models/vaModel.js";
import jwt from "jsonwebtoken";
async function client_logout_callback(req, res) {
try {
const accessToken = req.headers.token;
if (!accessToken) {
return res.status(401).json({ success: false, message: "Missing token" });
}
const decoded = jwt.verify(accessToken, process.env.JWT_SECRET_ACCESS_TOKEN);
if (!decoded || !decoded.email) {
return res.status(401).json({ success: false, message: "Invalid token" });
}
const clientEmail = decoded.email;
const clientUser = await ClientModel.findOne({ "userDetails.email": clientEmail });
if (!clientUser) {
return res.status(404).json({ success: false, message: "Client not found" });
}
// Find user by email
const user = await ClientModel.findOne({ "userDetails.email": clientEmail });
if (!user) {
return res.status(404).json({ success: false, message: "Invalid credentials" });
}
await ClientModel.updateOne({ "userDetails.email": clientEmail }, {
$set: { accessToken: null, refreshToken: null }
})
// Return accessToken and user info
return res.status(200).json({
success: true,
message: "Log Out Successfull",
});
} catch (error) {
console.error("client logout error:", error);
return res.status(500).json({ success: false, message: "Internal Server Error" });
}
}
export { client_logout_callback };

View File

@ -11,7 +11,8 @@ import { clientAuthValidator } from "../../middlewares/auth.middleware.js";
import { client_dashboard } from "./../../controllers/client/dashboard/clientDashboard.controller.js";
import { client_dashboard_va_log } from "../../controllers/client/dashboard/clientDashboardVaLog.controller.js";
import { client_verify_email } from "../../controllers/client/vaClientEmailVerify.controller.js";
import { client_dashboard_projects } from "../../controllers/client/dashboard/clientDashboardProjects.controller.js"
import { client_dashboard_projects } from "../../controllers/client/dashboard/clientDashboardProjects.controller.js";
import { client_logout_callback } from "../../controllers/client/dashboard/clientLogout.controller.js";
// clients routes
clientRoutes.post("/register", va_register_client); // Register a new VA
@ -19,6 +20,7 @@ clientRoutes.post("/login", va_login_client); // Login Client
clientRoutes.get("/refreshToken", client_refresh_token_callback); // Client Refresh Token
clientRoutes.post("/forgot-password", client_request_password_reset);
clientRoutes.post("/verify-client-email", client_verify_email);
clientRoutes.post("/logout", clientAuthValidator, client_logout_callback);
// clientRoutes.get("/:clientID?", va_clients_callback); // VA Clients Callback for getting all VA's or specific client by ID (NC)
clientRoutes.get("/:clientId/projects/:projectId", va_clients_projects_callback); // VA Clients Projects Callback (NC)

View File

@ -46,8 +46,6 @@
</li> -->
</ul>
<div class="user-avatar">TT</div>
<button class="mobile-menu-button">
<i class="fas fa-bars"></i>
</button>
@ -91,6 +89,12 @@
<span>Referrals</span>
</a>
</li> -->
<li class="sidebar-item">
<a href="javascript:void(0);" class="sidebar-link" id="logoutButton">
<i class="fas fa-sign-out-alt sidebar-icon"></i>
<span>Logout</span>
</a>
</li>
</ul>
</nav>
</aside>
@ -220,6 +224,22 @@
return response;
}
async function handleLogout() {
try {
// Call logout API
await fetchWithAuth(`${API_BASE}/logout`, {
method: 'POST'
});
} catch (error) {
console.error('Logout API error:', error);
} finally {
// Clear local storage and redirect
localStorage.removeItem('knoptpq');
localStorage.removeItem('refreshToken');
window.location.href = '/client/login';
}
}
// Format time function
function formatTime(seconds) {
if (!seconds) return 'Not working';
@ -350,20 +370,25 @@
vaEmptyState.style.display = 'block';
}
// Load data when page loads
document.addEventListener('DOMContentLoaded', loadDashboardData);
// Optional: Auto-refresh data every 30 seconds
setInterval(loadDashboardData, 30000);
document.addEventListener('DOMContentLoaded', () => {
var menuToggle = document.querySelector('.mobile-menu-button');
var sidebar = document.querySelector('.dashboard-sidebar');
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')
})
})
sidebar.classList.toggle('active');
});
// Logout button
const logoutButton = document.getElementById('logoutButton');
logoutButton.addEventListener('click', handleLogout);
});
</script>

View File

@ -46,7 +46,6 @@
</li> -->
</ul>
<div class="user-avatar">TT</div>
<button class="mobile-menu-button">
<i class="fas fa-bars"></i>
@ -91,6 +90,12 @@
<span>Referrals</span>
</a>
</li> -->
<li class="sidebar-item">
<a href="javascript:void(0);" class="sidebar-link" id="logoutButton">
<i class="fas fa-sign-out-alt sidebar-icon"></i>
<span>Logout</span>
</a>
</li>
</ul>
</nav>
</aside>
@ -127,36 +132,7 @@
</div>
</div>
</div>
<br><br>
<!-- Virtual Assistants List -->
<div class="content-section">
<div class="section-header">
<h2 class="section-title">Virtual Assistants <span class="total-count" id="vasCount">0 VAs</span></h2>
</div>
<div class="table-container">
<div class="table-wrapper">
<table class="data-table">
<thead>
<tr>
<th>Name</th>
<th>Email</th>
<th>Status</th>
</tr>
</thead>
<tbody id="vaTableBody">
<!-- Data will be loaded here -->
</tbody>
</table>
</div>
<div id="vaEmptyState" style="display: none;">
<div class="empty-state">
<i class="fas fa-users"></i>
<h3>No Virtual Assistants</h3>
<p>You don't have any virtual assistants assigned yet.</p>
</div>
</div>
</div>
</div>
</div>
</main>
</div>
@ -210,15 +186,32 @@
return response;
}
// Create Project table row HTML
function createProjectTableRow(project) {
const statusClass = project.status === 'active' ? 'active' : 'inactive';
const statusText = project.status === 'active' ? 'Active' : 'Inactive';
async function handleLogout() {
try {
// Call logout API
await fetchWithAuth(`${API_BASE}/logout`, {
method: 'POST'
});
} catch (error) {
console.error('Logout API error:', error);
} finally {
// Clear local storage and redirect
localStorage.removeItem('knoptpq');
localStorage.removeItem('refreshToken');
window.location.href = '/client/login';
}
}
function createProjectTableRow(project, allVAs) {
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}` : '-';
console.log(project)
console.log(allVAs)
return `
<tr>
<tr class="project-row" data-project-id="${project.id}" data-project-name="${project.name}">
<td>
<div class="project-info">
<div class="project-name">${project.name || 'Unnamed Project'}</div>
@ -234,7 +227,7 @@
<span class="dedicated-hours">${dedicatedHours}</span>
</td>
<td>
<div class="status-indicator">
<div class="">
<div class="status-dot ${statusClass}"></div>
<span class="status-text ${statusClass}">${statusText}</span>
</div>
@ -244,30 +237,30 @@
}
// Create VA table row HTML
function createVATableRow(va) {
const statusClass = va.status === 'active' ? 'active' : 'inactive';
const statusText = va.status === 'active' ? 'Active' : 'Inactive';
const fullName = `${va.firstName || ''} ${va.lastName || ''}`.trim() || 'Unknown';
// function createVATableRow(va) {
// const statusClass = va.status === 'active' ? 'active' : 'inactive';
// const statusText = va.status === 'active' ? 'Active' : 'Inactive';
// const fullName = `${va.firstName || ''} ${va.lastName || ''}`.trim() || 'Unknown';
return `
<tr>
<td>
<div class="va-info">
<div class="va-name">${fullName}</div>
</div>
</td>
<td>
<span class="va-email">${va.email || 'N/A'}</span>
</td>
<td>
<div class="status-indicator">
<div class="status-dot ${statusClass}"></div>
<span class="status-text ${statusClass}">${statusText}</span>
</div>
</td>
</tr>
`;
}
// return `
// <tr>
// <td>
// <div class="va-info">
// <div class="va-name">${fullName}</div>
// </div>
// </td>
// <td>
// <span class="va-email">${va.email || 'N/A'}</span>
// </td>
// <td>
// <div class="status-indicator">
// <div class="status-dot ${statusClass}"></div>
// <span class="status-text ${statusClass}">${statusText}</span>
// </div>
// </td>
// </tr>
// `;
// }
// Load dashboard data
async function loadDashboardData() {
@ -283,38 +276,33 @@
// Update counts
document.getElementById('projectsCount').textContent = `${counts.totalProjects || 0} Projects`;
document.getElementById('vasCount').textContent = `${counts.totalVAs || 0} VAs`;
// Update Projects table
const projectsTableBody = document.getElementById('projectsTableBody');
const projectsEmptyState = document.getElementById('projectsEmptyState');
const projects = data.projects || [];
const vas = data.vas || [];
if (projects.length > 0) {
projectsTableBody.innerHTML = projects.map(project => createProjectTableRow(project)).join('');
projectsTableBody.innerHTML = projects.map(project => createProjectTableRow(project, vas)).join('');
projectsTableBody.style.display = '';
projectsEmptyState.style.display = 'none';
// Add click event listeners to project rows
document.querySelectorAll('.project-row').forEach(row => {
row.addEventListener('click', function() {
const projectId = this.getAttribute('data-project-id');
const projectName = this.getAttribute('data-project-name');
console.log(projectName)
});
});
} else {
projectsTableBody.innerHTML = '';
projectsTableBody.style.display = 'none';
projectsEmptyState.style.display = 'block';
}
// Update VA table
const vaTableBody = document.getElementById('vaTableBody');
const vaEmptyState = document.getElementById('vaEmptyState');
const vas = data.vas || [];
if (vas.length > 0) {
vaTableBody.innerHTML = vas.map(va => createVATableRow(va)).join('');
vaTableBody.style.display = '';
vaEmptyState.style.display = 'none';
} 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');
@ -357,20 +345,26 @@
vaEmptyState.style.display = 'block';
}
// Load data when page loads
document.addEventListener('DOMContentLoaded', loadDashboardData);
// Optional: Auto-refresh data every 30 seconds
setInterval(loadDashboardData, 30000);
document.addEventListener('DOMContentLoaded', () => {
var menuToggle = document.querySelector('.mobile-menu-button');
var sidebar = document.querySelector('.dashboard-sidebar');
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')
})
})
sidebar.classList.toggle('active');
});
// Logout button
const logoutButton = document.getElementById('logoutButton');
logoutButton.addEventListener('click', handleLogout);
});
</script>

View File

@ -46,8 +46,6 @@
</li> -->
</ul>
<div class="user-avatar">TT</div>
<button class="mobile-menu-button">
<i class="fas fa-bars"></i>
</button>
@ -91,6 +89,12 @@
<span>Referrals</span>
</a>
</li> -->
<li class="sidebar-item">
<a href="javascript:void(0);" class="sidebar-link" id="logoutButton">
<i class="fas fa-sign-out-alt sidebar-icon"></i>
<span>Logout</span>
</a>
</li>
</ul>
</nav>
</aside>
@ -220,6 +224,22 @@
return response;
}
async function handleLogout() {
try {
// Call logout API
await fetchWithAuth(`${API_BASE}/logout`, {
method: 'POST'
});
} catch (error) {
console.error('Logout API error:', error);
} finally {
// Clear local storage and redirect
localStorage.removeItem('knoptpq');
localStorage.removeItem('refreshToken');
window.location.href = '/client/login';
}
}
// Format time function
function formatTime(seconds) {
if (!seconds) return 'Not working';
@ -350,20 +370,24 @@
vaEmptyState.style.display = 'block';
}
// Load data when page loads
document.addEventListener('DOMContentLoaded', loadDashboardData);
// Optional: Auto-refresh data every 30 seconds
setInterval(loadDashboardData, 30000);
document.addEventListener('DOMContentLoaded', function() {
// Load dashboard data
loadDashboardData();
document.addEventListener('DOMContentLoaded', () => {
var menuToggle = document.querySelector('.mobile-menu-button');
var sidebar = document.querySelector('.dashboard-sidebar');
// Mobile menu toggle
const menuToggle = document.querySelector('.mobile-menu-button');
const sidebar = document.querySelector('.dashboard-sidebar');
menuToggle.addEventListener('click', () => {
sidebar.classList.toggle('active')
})
})
sidebar.classList.toggle('active');
});
// Logout button
const logoutButton = document.getElementById('logoutButton');
logoutButton.addEventListener('click', handleLogout);
});
</script>

View File

@ -8,6 +8,9 @@
<link href="/public/css/va-register.css" rel="stylesheet">
<style>
select option:disabled {
display: none;
}
/* body {
background-color: #f8f9fa;
font-family: 'Segoe UI', sans-serif;
@ -171,14 +174,14 @@
<form id="account-form">
<div class="form-group">
<div class="input-container">
<input type="email" id="email" class="input-field" placeholder=" " required autocomplete="off">
<input type="email" id="email" class="input-field" placeholder=" " required autocomplete="username">
<label for="email" class="input-label">Email Address *</label>
</div>
</div>
<div class="form-group">
<div class="input-container">
<input type="password" id="password" class="input-field" placeholder=" " required autocomplete="off">
<input type="password" id="password" class="input-field" placeholder=" " required autocomplete="new-password">
<label for="password" class="input-label">Create Password *</label>
</div>
<div class="password-actions">
@ -189,7 +192,7 @@
<div class="form-group">
<div class="input-container">
<input type="password" id="confirmPassword" class="input-field" placeholder=" " required autocomplete="off">
<input type="password" id="confirmPassword" class="input-field" placeholder=" " required autocomplete="new-password">
<label for="confirmPassword" class="input-label">Confirm Password *</label>
</div>
<div class="invalid-feedback" id="password-match-error" style="display: none; color: #dc3545; font-size: 12px; margin-top: 5px;">Passwords do not match</div>
@ -259,7 +262,7 @@
</div>
<div class="form-group">
<div class="input-container">
<select id="country" class="select-field" required>
<select id="country" class="select-field" required onchange="handleChangeSelect(this)">
<option value=""></option>
</select>
<label for="country" class="input-label">Country *</label>
@ -269,7 +272,7 @@
<div class="form-group">
<div class="input-container">
<select id="state" class="select-field" required>
<select id="state" class="select-field" required onchange="handleChangeSelect(this)">
<option value=""></option>
</select>
<label for="state" class="input-label">State / Region *</label>