diff --git a/public/css/dashboard.css b/public/css/dashboard.css index 7ffb796..cbb0fa6 100644 --- a/public/css/dashboard.css +++ b/public/css/dashboard.css @@ -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; +} \ No newline at end of file diff --git a/public/js/va-client-register.js b/public/js/va-client-register.js index 3fa242e..650beed 100644 --- a/public/js/va-client-register.js +++ b/public/js/va-client-register.js @@ -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 diff --git a/public/js/va-register.js b/public/js/va-register.js index 32928e0..b7ff84a 100644 --- a/public/js/va-register.js +++ b/public/js/va-register.js @@ -71,7 +71,7 @@ document.addEventListener('DOMContentLoaded', function() { // Populate country dropdown const countrySelect = document.getElementById('country'); - countrySelect.innerHTML = ''; + countrySelect.innerHTML = ''; countries.forEach(country => { const option = document.createElement('option'); @@ -89,7 +89,7 @@ document.getElementById('state-loading').style.display = 'block'; // Clear current states - stateSelect.innerHTML = ''; + stateSelect.innerHTML = ''; // 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 = ''; @@ -901,4 +904,15 @@ 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 = ""; + } } \ No newline at end of file diff --git a/src/controllers/client/dashboard/clientDashboardProjects.controller.js b/src/controllers/client/dashboard/clientDashboardProjects.controller.js index d7b77cc..421e572 100644 --- a/src/controllers/client/dashboard/clientDashboardProjects.controller.js +++ b/src/controllers/client/dashboard/clientDashboardProjects.controller.js @@ -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, diff --git a/src/controllers/client/dashboard/clientLogout.controller.js b/src/controllers/client/dashboard/clientLogout.controller.js new file mode 100644 index 0000000..2b7cf27 --- /dev/null +++ b/src/controllers/client/dashboard/clientLogout.controller.js @@ -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 }; \ No newline at end of file diff --git a/src/routes/v1/client.routes.js b/src/routes/v1/client.routes.js index 382d722..066ccc3 100644 --- a/src/routes/v1/client.routes.js +++ b/src/routes/v1/client.routes.js @@ -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) diff --git a/views/client/dashboard-dashboard.ejs b/views/client/dashboard-dashboard.ejs index 0dede01..a86925c 100644 --- a/views/client/dashboard-dashboard.ejs +++ b/views/client/dashboard-dashboard.ejs @@ -46,8 +46,6 @@ --> -
TT
- @@ -91,6 +89,12 @@ Referrals --> + @@ -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); + }); diff --git a/views/client/dashboard-projects.ejs b/views/client/dashboard-projects.ejs index a443e12..68e300c 100644 --- a/views/client/dashboard-projects.ejs +++ b/views/client/dashboard-projects.ejs @@ -46,7 +46,6 @@ --> -
TT
@@ -91,6 +89,12 @@ Referrals --> + @@ -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); + }); diff --git a/views/va_register.ejs b/views/va_register.ejs index e45d784..4e5450e 100644 --- a/views/va_register.ejs +++ b/views/va_register.ejs @@ -8,6 +8,9 @@