diff --git a/public/css/dashboard.css b/public/css/dashboard.css new file mode 100644 index 0000000..ce02b96 --- /dev/null +++ b/public/css/dashboard.css @@ -0,0 +1,635 @@ +:root { + --color-primary: #0d0b57; + --color-primary-light: #2b8efc; + --color-secondary: #6366f1; + --color-success: #10b981; + --color-warning: #f59e0b; + --color-danger: #ef4444; + --color-gray-50: #f9fafb; + --color-gray-100: #f3f4f6; + --color-gray-200: #e5e7eb; + --color-gray-300: #d1d5db; + --color-gray-400: #9ca3af; + --color-gray-500: #6b7280; + --color-gray-600: #4b5563; + --color-gray-700: #374151; + --color-gray-800: #1f2937; + --color-gray-900: #111827; + + --bg-primary: #ffffff; + --bg-secondary: #f8fafc; + --bg-card: #ffffff; + --text-primary: #1f2937; + --text-secondary: #6b7280; + --text-muted: #9ca3af; + --border-color: #e5e7eb; + --shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06); + --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); +} + +[data-theme="dark"] { + --bg-primary: #121212; + --bg-secondary: #1e1e1e; + --bg-card: #1e1e1e; + --text-primary: #f5f5f5; + --text-secondary: #d1d5db; + --text-muted: #9ca3af; + --border-color: #374151; + --shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.3), 0 1px 2px 0 rgba(0, 0, 0, 0.2); + --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.3), 0 4px 6px -2px rgba(0, 0, 0, 0.2); +} + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif; + background-color: var(--bg-primary); + color: var(--text-primary); + transition: background-color 0.3s ease, color 0.3s ease; +} + +.dashboard-body { + overflow: hidden; +} + +/* Header Styles */ +.dashboard-header { + position: sticky; + top: 0; + z-index: 40; + width: 100%; + height: 4rem; + display: flex; + align-items: center; + justify-content: center; + backdrop-filter: blur(16px); + background-color: rgba(255, 255, 255, 0.7); + border-bottom: 1px solid var(--border-color); +} + +[data-theme="dark"] .dashboard-header { + background-color: rgba(30, 30, 30, 0.7); +} + +.header-content { + display: flex; + padding: 0 1.5rem; + gap: 1rem; + width: 100%; + max-width: 1024px; + flex-direction: row; + align-items: center; + justify-content: space-between; + position: relative; + flex-wrap: nowrap; +} + +.header-logo { + display: flex; + flex-basis: 0; + flex-grow: 1; + flex-direction: row; + flex-wrap: nowrap; + justify-content: flex-start; + align-items: center; + background: transparent; + text-decoration: none; + color: inherit; + white-space: nowrap; + box-sizing: border-box; +} + +.header-nav { + display: flex; + height: 100%; + flex-direction: row; + flex-wrap: nowrap; + gap: 0.5rem; + align-items: center; + justify-content: flex-end; +} + +.nav-list { + display: flex; + list-style: none; + gap: 0.5rem; +} + +.nav-button { + padding: 0.5rem 1rem; + border-radius: 0.5rem; + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; + transition: all 0.2s; + font-size: 0.875rem; + font-weight: 500; + display: flex; + align-items: center; + gap: 0.5rem; + color: var(--text-secondary); + background: none; + border: none; + cursor: pointer; + text-decoration: none; +} + +.nav-button:hover { + background-color: var(--color-gray-100); + color: var(--color-primary-light); +} + +[data-theme="dark"] .nav-button:hover { + background-color: var(--color-gray-800); +} + +.nav-button.active { + color: var(--color-primary-light); + border-bottom: 2px solid var(--color-primary-light); +} + +.user-avatar { + width: 2rem; + height: 2rem; + background-color: var(--color-primary); + color: white; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-size: 0.75rem; + font-weight: 600; + cursor: pointer; +} + +.mobile-menu-button { + display: none; + width: 2rem; + height: 2rem; + align-items: center; + justify-content: center; + color: var(--text-secondary); + background: none; + border: none; + cursor: pointer; +} + +/* Container Layout */ +.dashboard-container { + display: flex; + height: calc(100vh - 4rem); +} + +/* Sidebar Styles */ +.dashboard-sidebar { + width: 16rem; + background-color: var(--bg-card); + border-right: 1px solid var(--border-color); + display: none; +} + +@media (min-width: 768px) { + .dashboard-sidebar { + display: block; + } +} + +.sidebar-header { + padding: 1.5rem; +} + +.sidebar-title { + font-size: 1.125rem; + font-weight: 600; + color: var(--text-primary); +} + +.sidebar-nav { + margin-top: 1.5rem; +} + +.sidebar-list { + list-style: none; + padding: 0 1rem; + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.sidebar-item { + margin-bottom: 0.5rem; +} + +.sidebar-link { + display: flex; + align-items: center; + gap: 0.75rem; + padding: 0.75rem 1rem; + color: var(--text-secondary); + text-decoration: none; + border-radius: 0.5rem; + transition: all 0.2s; +} + +.sidebar-link:hover { + background-color: var(--color-gray-100); + color: var(--text-primary); +} + +[data-theme="dark"] .sidebar-link:hover { + background-color: var(--color-gray-800); +} + +.sidebar-link.active { + background-color: rgba(59, 130, 246, 0.1); + color: var(--color-primary-light); +} + +.sidebar-icon { + width: 1.25rem; + text-align: center; +} + +/* Main Content */ +.dashboard-main { + flex: 1; + overflow: auto; + padding: 1.5rem; + background-color: var(--bg-secondary); +} + +.dashboard-content { + max-width: 80rem; + margin: 0 auto; +} + +/* Stats Grid */ +.stats-grid { + display: grid; + grid-template-columns: 1fr; + gap: 1.5rem; + margin-bottom: 2rem; +} + +@media (min-width: 768px) { + .stats-grid { + grid-template-columns: repeat(2, 1fr); + } +} + +@media (min-width: 1024px) { + .stats-grid { + grid-template-columns: repeat(4, 1fr); + } +} + +.stat-card { + background-color: var(--bg-card); + border-radius: 0.75rem; + padding: 1.5rem; + box-shadow: var(--shadow); + border: 1px solid var(--border-color); + transition: box-shadow 0.2s; +} + +.stat-card:hover { + box-shadow: var(--shadow-lg); +} + +.stat-content { + display: flex; + align-items: center; + justify-content: space-between; +} + +.stat-info h3 { + font-size: 0.875rem; + color: var(--text-secondary); + margin-bottom: 0.25rem; + font-weight: 500; +} + +.stat-value { + font-size: 1.5rem; + font-weight: 700; + color: var(--text-primary); +} + +.stat-icon { + width: 3rem; + height: 3rem; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; +} + +.stat-icon.blue { + background-color: rgba(59, 130, 246, 0.1); + color: var(--color-primary-light); +} + +.stat-icon.green { + background-color: rgba(16, 185, 129, 0.1); + color: var(--color-success); +} + +.stat-icon.red { + background-color: rgba(239, 68, 68, 0.1); + color: var(--color-danger); +} + +.stat-icon.yellow { + background-color: rgba(245, 158, 11, 0.1); + color: var(--color-warning); +} + +/* Content Grid */ +.content-grid { + display: grid; + grid-template-columns: 1fr; + gap: 1.5rem; +} + +@media (min-width: 1024px) { + .content-grid { + grid-template-columns: repeat(2, 1fr); + } +} + +.content-card { + background-color: var(--bg-card); + border-radius: 0.75rem; + padding: 1.5rem; + box-shadow: var(--shadow); + border: 1px solid var(--border-color); +} + +.card-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 1rem; +} + +.card-title { + font-size: 1.125rem; + font-weight: 600; + color: var(--text-primary); +} + +.card-link { + font-size: 0.875rem; + color: var(--color-primary-light); + text-decoration: none; +} + +.card-link:hover { + text-decoration: underline; +} + +/* Activity Items */ +.activity-list { + display: flex; + flex-direction: column; + gap: 0.75rem; +} + +.activity-item { + display: flex; + justify-content: space-between; + align-items: center; + padding: 0.75rem; + background-color: var(--color-gray-50); + border-radius: 0.5rem; +} + +[data-theme="dark"] .activity-item { + background-color: var(--color-gray-800); +} + +.activity-info h4 { + font-weight: 500; + color: var(--text-primary); + margin-bottom: 0.25rem; +} + +.activity-info p { + font-size: 0.875rem; + color: var(--text-secondary); +} + +.activity-meta { + text-align: right; +} + +.activity-meta .value { + font-weight: 600; + color: var(--text-primary); + margin-bottom: 0.25rem; +} + +.activity-meta .date { + font-size: 0.875rem; + color: var(--text-secondary); +} + +.status-badge { + display: inline-block; + padding: 0.25rem 0.5rem; + font-size: 0.75rem; + border-radius: 9999px; + font-weight: 600; +} + +.status-paid { + background-color: rgba(16, 185, 129, 0.1); + color: var(--color-success); +} + +.status-unpaid { + background-color: rgba(239, 68, 68, 0.1); + color: var(--color-danger); +} + +/* Page Header */ +.page-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 1.5rem; +} + +.page-title { + font-size: 1.5rem; + font-weight: 300; + color: var(--text-primary); +} + +.action-button { + padding: 0.5rem 1rem; + background-color: rgba(59, 130, 246, 0.1); + color: var(--color-primary-light); + border-radius: 0.5rem; + font-weight: 500; + display: flex; + align-items: center; + gap: 0.5rem; + text-decoration: none; + border: none; + cursor: pointer; + transition: background-color 0.2s; +} + +.action-button:hover { + background-color: rgba(59, 130, 246, 0.2); +} + +/* Table Styles */ +.table-container { + background-color: var(--bg-card); + border-radius: 0.5rem; + box-shadow: var(--shadow); + border: 1px solid var(--border-color); + overflow: hidden; +} + +.table-wrapper { + overflow-x: auto; +} + +.data-table { + width: 100%; + border-collapse: collapse; +} + +.data-table thead { + background-color: var(--color-gray-50); +} + +[data-theme="dark"] .data-table thead { + background-color: var(--color-gray-800); +} + +.data-table th { + padding: 0.75rem 1.5rem; + text-align: left; + font-size: 0.75rem; + font-weight: 500; + color: var(--text-secondary); + text-transform: uppercase; + letter-spacing: 0.05em; + border-bottom: 1px solid var(--border-color); +} + +.data-table tbody tr { + border-bottom: 1px solid var(--border-color); + transition: background-color 0.2s; + cursor: pointer; +} + +.data-table tbody tr:hover { + background-color: var(--color-gray-50); +} + +[data-theme="dark"] .data-table tbody tr:hover { + background-color: var(--color-gray-800); +} + +.data-table td { + padding: 1rem 1.5rem; + font-size: 0.875rem; + color: var(--text-primary); +} + +.data-table .status-active { + background-color: rgba(16, 185, 129, 0.1); + color: var(--color-success); +} + +.data-table .status-badge { + padding: 0.25rem 0.75rem; + border-radius: 9999px; + font-size: 0.75rem; + font-weight: 600; +} + +.hours-badge { + display: inline-flex; + align-items: center; + gap: 0.25rem; + padding: 0.25rem 0.75rem; + border-radius: 9999px; + font-size: 0.75rem; + font-weight: 600; +} + +.hours-default { + background-color: rgba(107, 114, 128, 0.1); + color: var(--color-gray-600); +} + +.hours-primary { + background-color: rgba(59, 130, 246, 0.1); + color: var(--color-primary-light); +} + +.hours-secondary { + background-color: rgba(99, 102, 241, 0.1); + color: var(--color-secondary); +} + +.hours-warning { + background-color: rgba(245, 158, 11, 0.1); + color: var(--color-warning); +} + +.amount-positive { + color: var(--color-success); +} + +.amount-tips { + color: var(--color-warning); +} + +/* Empty State */ +.empty-state { + padding: 3rem 1.5rem; + text-align: center; + color: var(--text-secondary); +} + +/* Responsive */ +@media (max-width: 768px) { + .dashboard-main { + padding: 1rem; + } + + .mobile-menu-button { + display: flex; + } + + .nav-list { + display: none; + } + + .stats-grid { + grid-template-columns: 1fr; + } + + .content-grid { + grid-template-columns: 1fr; + } + + .data-table { + font-size: 0.75rem; + } + + .data-table th, + .data-table td { + padding: 0.5rem 0.75rem; + } +} \ No newline at end of file diff --git a/public/css/va-register.css b/public/css/va-register.css index 97cc23a..659500e 100644 --- a/public/css/va-register.css +++ b/public/css/va-register.css @@ -20,8 +20,8 @@ body { .sidebar { display: none; flex-direction: column; - width: 280px; - background: linear-gradient(to bottom, #0b57d0bd, #0b57d0d6, #0b57d0); + width: 388px; + background: linear-gradient(to bottom, #f4f4f5, #d0f9f9, #cac5f7); padding: 24px; border-radius: 14px; margin: 20px; diff --git a/public/js/va-client-register.js b/public/js/va-client-register.js index 783bcc2..c44cba4 100644 --- a/public/js/va-client-register.js +++ b/public/js/va-client-register.js @@ -603,6 +603,10 @@ async function submitForm() { stepNumber.style.color = 'white'; }); nextStep(6); + + setTimeout(() => { + window.location.href = '/client/login'; + }, 1000); } else { // Handle server errors if (response.status === 409) { diff --git a/public/js/va-register.js b/public/js/va-register.js index d343149..5f73dbf 100644 --- a/public/js/va-register.js +++ b/public/js/va-register.js @@ -593,6 +593,9 @@ }); nextStep(5); + setTimeout(() => { + window.location.href = '/login'; + }, 1000); } else { // Handle server errors alert('There was an error submitting your registration. Please try again.'); diff --git a/src/controllers/client/vaClient.controller.js b/src/controllers/client/vaClient.controller.js index f34de5d..700b238 100644 --- a/src/controllers/client/vaClient.controller.js +++ b/src/controllers/client/vaClient.controller.js @@ -11,7 +11,6 @@ async function va_register_client(req, res) { try { const payload = req.body || {}; const { userDetails } = payload; - if (!userDetails || !userDetails.email || !userDetails.name) { return res.status(400).json({ success: false, @@ -37,6 +36,13 @@ async function va_register_client(req, res) { }); } + if (error.message.includes("Salesforce API is unreachable")) { + return res.status(503).json({ + success: false, + message: "Salesforce service is currently unavailable. Please try again later." + }); + } + return res.status(500).json({ success: false, message: "Internal Server Error" diff --git a/src/controllers/passwordReset.controllers.js b/src/controllers/passwordReset.controllers.js index e23a485..45da602 100644 --- a/src/controllers/passwordReset.controllers.js +++ b/src/controllers/passwordReset.controllers.js @@ -56,14 +56,14 @@ async function render_reset_form(req, res) { const { token, userType } = req.query; if (!token || !userType) { - return res.render('auth/reset-password-error', { error: 'Reset token or user type is missing' }); + return res.render('auth/reset-password-error', { error: 'Reset token or user type is missing', userType: userType || 'va' }); } // Validate token const validation = await passwordResetService.validateResetToken(token, userType); if (!validation.valid) { - return res.render('auth/reset-password-error', { error: validation.message }); + return res.render('auth/reset-password-error', { error: validation.message, userType: userType || 'va' }); } // Render reset form with token @@ -102,7 +102,7 @@ async function process_reset(req, res) { await passwordResetService.resetPassword(token, password, userType); // Render success page - res.render('auth/reset-password-success'); + res.render('auth/reset-password-success', {userType}); } catch (error) { logger.error(`PasswordResetController - Process reset error: ${error.message}`); diff --git a/src/controllers/va/vaAuth/vaLogin.controller.js b/src/controllers/va/vaAuth/vaLogin.controller.js index d603004..580552d 100644 --- a/src/controllers/va/vaAuth/vaLogin.controller.js +++ b/src/controllers/va/vaAuth/vaLogin.controller.js @@ -1,6 +1,6 @@ import bcrypt from "bcrypt"; import jwt from "jsonwebtoken"; -import { VAModel } from "../../../models/vaModel.js"; +import { VAModel, LoggedModel } from "../../../models/vaModel.js"; // Login VA async function va_login_callback(req, res) { @@ -32,6 +32,36 @@ async function va_login_callback(req, res) { return res.status(401).json({ success: false, message: "Invalid password" }); } + // Check if VA has any active sessions (LOGIN without LOGOUT) + const activeSessions = await LoggedModel.find({ + vaId: user._id, + loggedType: "LOGIN" + }).sort({ time: -1 }); + + if (activeSessions.length > 0) { + // Get the latest login to check if there's no corresponding logout + const latestLogin = activeSessions[0]; + + // Check if there's a logout after this login + const correspondingLogout = await LoggedModel.findOne({ + vaId: user._id, + loggedType: "LOGOUT", + time: { $gt: latestLogin.time } + }); + + if (!correspondingLogout) { + // VA is still logged in to a project + const projectInfo = await LoggedModel.findById(latestLogin._id) + .populate('projectId', 'name') + .lean(); + + return res.status(400).json({ + success: false, + message: "You are still logged in to a project. Please logout first before logging in again." + }); + } + } + // Generate JWT accessToken with 24h expiry const accessToken = jwt.sign( { @@ -76,4 +106,17 @@ async function va_login_callback(req, res) { } } +// Helper function to format session duration +function formatSessionDuration(loginTime) { + const now = new Date(); + const login = new Date(loginTime); + const durationMs = now - login; + + const hours = Math.floor(durationMs / (1000 * 60 * 60)); + const minutes = Math.floor((durationMs % (1000 * 60 * 60)) / (1000 * 60)); + const seconds = Math.floor((durationMs % (1000 * 60)) / 1000); + + return `${hours}h ${minutes}m ${seconds}s`; +} + export { va_login_callback }; \ No newline at end of file diff --git a/src/controllers/va/vaProfile/vaRegister.controller.js b/src/controllers/va/vaProfile/vaRegister.controller.js index 61352fc..da2f8fb 100644 --- a/src/controllers/va/vaProfile/vaRegister.controller.js +++ b/src/controllers/va/vaProfile/vaRegister.controller.js @@ -47,7 +47,37 @@ async function va_register(req, res) { const created = await VAModel.create(toCreate); - return res.status(201).json({ success: true, data: created }); + // Send data to Salesforce API after successful registration + try { + const salesforceUrl = `${process.env.SF_DOCTRACKER_API_URL}services/apexrest/docva/api/virtualassistant`; + + const response = await fetch(salesforceUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + // Add any required headers for Salesforce authentication + // 'Authorization': 'Bearer ' + process.env.SF_ACCESS_TOKEN + }, + body: JSON.stringify(created) + }); + + if (!response.ok) { + const errorText = await response.text(); + throw new Error(`Salesforce API responded with status ${response.status}: ${errorText}`); + } + + const result = await response.json(); + console.log("✅ Successfully synced VA data to Salesforce:", result); + + res.status(201).json({ success: true, data: created, SyncSalesforce: result }); + } catch (error) { + console.error("❌ Failed to sync with Salesforce:", error.message); + + // Create a custom error to distinguish Salesforce errors + const salesforceError = new Error(`Salesforce sync failed: ${error.message}`); + salesforceError.isSalesforceError = true; + throw salesforceError; + } } catch (error) { console.error("va_register error:", error); return res.status(500).json({ success: false, message: "Internal Server Error" }); diff --git a/src/middlewares/auth.middleware.js b/src/middlewares/auth.middleware.js index cb8a078..36b2ff5 100644 --- a/src/middlewares/auth.middleware.js +++ b/src/middlewares/auth.middleware.js @@ -1,12 +1,12 @@ import jwt from "jsonwebtoken"; -import { VAModel } from "../models/vaModel.js"; +import { VAModel, ClientModel } from "../models/vaModel.js"; import logger from "../config/logger.js"; async function authValidator(req, res, next) { try { - const sf_token = req.header.sf_token; + const sf_token = req.headers.sf_token; if (sf_token) { - + next(); } else { const accessToken = req.headers.token; if (!accessToken) return res.status(401).json({ success: false, message: "Not Authorized" }); @@ -15,12 +15,12 @@ async function authValidator(req, res, next) { // success: true, // message: "Log Out Successfull", // }); - + const decoded = jwt.verify(accessToken, process.env.JWT_SECRET_ACCESS_TOKEN); - + if (decoded && ("id" in decoded) && decoded.id !== null && ("email" in decoded)) { const vaEmail = decoded.email; - + const vaUser = await VAModel.findOne({ "userDetails.email": vaEmail }) if (!vaUser) { return res.status(401).json({ success: false, message: "Not Authorized" }); @@ -28,7 +28,7 @@ async function authValidator(req, res, next) { if (!("accessToken" in vaUser) || !vaUser.accessToken) { return res.status(401).json({ success: false, message: "Not Authorized" }); } - + if (vaUser.accessToken === accessToken) { req.body.vaId = vaUser._id req.body.vaEmail = vaEmail @@ -36,7 +36,7 @@ async function authValidator(req, res, next) { } else { return res.status(401).json({ success: false, message: "Not Authorized" }); } - + } } diff --git a/src/server.js b/src/server.js index 53d0c95..89dbe86 100644 --- a/src/server.js +++ b/src/server.js @@ -60,6 +60,57 @@ app.get("/client/forgot-password", (req, res) => { res.render("forgot_password") }) +// Client Dashboard Routes with new file names +app.get('/client/dashboard', async (req, res) => { + try { + const dashboardData = await dashboardService.getClientDashboard(req.user.id); + res.render('client/dashboard-dashboard', dashboardData); + } catch (error) { + console.error('Dashboard error:', error); + res.status(500).render('client/dashboard-dashboard', { error: 'Failed to load dashboard' }); + } +}); + +app.get('/client/virtual-assistants', async (req, res) => { + try { + const vaData = await virtualAssistantService.getClientVAs(req.user.id); + res.render('client/dashboard-virtual-assistants', vaData); + } catch (error) { + console.error('Virtual assistants error:', error); + res.status(500).render('client/dashboard-virtual-assistants', { error: 'Failed to load virtual assistants' }); + } +}); + +// app.get('/client/timesheets', async (req, res) => { +// try { +// const timesheetData = await timesheetService.getClientTimesheets(req.user.id); +// res.render('client/dashboard-timesheets', timesheetData); +// } catch (error) { +// console.error('Timesheets error:', error); +// res.status(500).render('client/dashboard-timesheets', { error: 'Failed to load timesheets' }); +// } +// }); + +// app.get('/client/invoices', async (req, res) => { +// try { +// const invoiceData = await invoiceService.getClientInvoices(req.user.id); +// res.render('client/dashboard-invoices', invoiceData); +// } catch (error) { +// console.error('Invoices error:', error); +// res.status(500).render('client/dashboard-invoices', { error: 'Failed to load invoices' }); +// } +// }); + +// app.get('/client/referrals', async (req, res) => { +// try { +// const referralData = await referralService.getClientReferrals(req.user.id); +// res.render('client/dashboard-referrals', referralData); +// } catch (error) { +// console.error('Referrals error:', error); +// res.status(500).render('client/dashboard-referrals', { error: 'Failed to load referrals' }); +// } +// }); + app.get("/live/:projectId", (req, res) => { const { projectId } = req.params; res.render("va_live_tracker", { title: "VA Live Tracker", projectId }) diff --git a/src/services/salesforce.service.js b/src/services/salesforce.service.js new file mode 100644 index 0000000..3f310ad --- /dev/null +++ b/src/services/salesforce.service.js @@ -0,0 +1,78 @@ +import axios from "axios"; +import logger from "../config/logger.js"; + +class SalesforceService { + + async registerClientInSalesforce(clientData, _id) { + try { + const url = `${process.env.SF_DOCTRACKER_API_URL}/services/apexrest/docva/api/client`; + + // Extract only the required data for Salesforce + const salesforcePayload = this.prepareSalesforcePayload(clientData, _id); + + logger.info(`SalesforceService - Sending client data to Salesforce: ${clientData.userDetails.email}`); + + const response = await axios.post(url, salesforcePayload, { + headers: { + 'Content-Type': 'application/json', + }, + timeout: 30000, // 30 seconds timeout + }); + + logger.info(`SalesforceService - Client registered successfully in Salesforce: ${clientData.userDetails.email}`); + return response.data; + } catch (error) { + logger.error(`SalesforceService - Error registering client in Salesforce: ${error.message}`); + + if (error.response) { + // Salesforce returned an error response + logger.error(`SalesforceService - Salesforce API error: ${error.response.status} - ${JSON.stringify(error.response.data)}`); + throw new Error(`Salesforce API error: ${error.response.status} - ${error.response.data.message || 'Unknown error'}`); + } else if (error.request) { + // Request was made but no response received + logger.error('SalesforceService - No response received from Salesforce API'); + throw new Error('Salesforce API is unreachable'); + } else { + // Something else happened + throw error; + } + } + } + + prepareSalesforcePayload(clientData, _id) { + const { userDetails } = clientData; + + return { + _id: _id, + userDetails: { + name: userDetails.name, + email: userDetails.email.toLowerCase(), + address: userDetails.address || null, + city: userDetails.city || null, + state: userDetails.state || null, + zip: userDetails.zip || null, + billingName: userDetails.billingName || null, + billingAddress: userDetails.billingAddress || null, + billingCity: userDetails.billingCity || null, + billingState: userDetails.billingState || null, + billingZip: userDetails.billingZip || null, + billingEmail: userDetails.billingEmail || null, + position: userDetails.position || null, + signatoryName: userDetails.signatoryName || null, + signatoryEmail: userDetails.signatoryEmail || null, + paymentMethod: userDetails.paymentMethod || null, + bankName: userDetails.bankName || null, + accountNumber: userDetails.accountNumber || null, + routingNumber: userDetails.routingNumber || null, + accountType: userDetails.accountType || null, + accountHolderName: userDetails.accountHolderName || null, + cardNumber: userDetails.cardNumber || null, + expiryDate: userDetails.expiryDate || null, + securityCode: userDetails.securityCode || null, + cardZip: userDetails.cardZip || null, + } + }; + } +} + +export default SalesforceService; \ No newline at end of file diff --git a/src/services/vaClient.service.js b/src/services/vaClient.service.js index e54f0ec..0feaa4c 100644 --- a/src/services/vaClient.service.js +++ b/src/services/vaClient.service.js @@ -1,8 +1,10 @@ import logger from "../config/logger.js"; +import SalesforceService from "./salesforce.service.js"; class VaClientService { constructor(vaClientRepository) { this.clientRepository = vaClientRepository; + this.salesforceService = new SalesforceService(); } async registerClient(data) { @@ -15,6 +17,9 @@ class VaClientService { // Sanitize and prepare data const clientData = this.sanitizeClientData(data); + + // Send data to Salesforce first + await this.salesforceService.registerClientInSalesforce(data, clientData._id); const response = await this.clientRepository.create(clientData); return response; @@ -87,19 +92,17 @@ class VaClientService { position: userDetails.position || null, signatoryName: userDetails.signatoryName || null, signatoryEmail: userDetails.signatoryEmail || null, - paymentMethod: userDetails.paymentMethod || null, - bankName: userDetails.bankName || null, - accountNumber: userDetails.accountNumber || null, - routingNumber: userDetails.routingNumber || null, - accountType: userDetails.accountType || null, - accountHolderName: userDetails.accountHolderName || null, - cardNumber: userDetails.cardNumber || null, - expiryDate: userDetails.expiryDate || null, - securityCode: userDetails.securityCode || null, - cardZip: userDetails.cardZip || null, - }, - vaIds: payload.vaIds || [], - projectIds: payload.projectIds || [], + paymentMethod: null, + bankName: null, + accountNumber: null, + routingNumber: null, + accountType: null, + accountHolderName: null, + cardNumber: null, + expiryDate: null, + securityCode: null, + cardZip: null, + } }; } } diff --git a/views/auth/reset-password-error.ejs b/views/auth/reset-password-error.ejs index 2af6b7d..2a61feb 100644 --- a/views/auth/reset-password-error.ejs +++ b/views/auth/reset-password-error.ejs @@ -12,8 +12,17 @@
- Request New Reset Link - Back to Login + <% + let forgotPasswordUrl = `${process.env.FRONTEND_URL}/forgot-password`; + let loginUrl = `${process.env.FRONTEND_URL}/login`; + + if (userType === 'client') { + forgotPasswordUrl = '/client/forgot-password'; + loginUrl = '/client/login'; + } + %> + Request New Reset Link + Back to Login
diff --git a/views/auth/reset-password-form.ejs b/views/auth/reset-password-form.ejs index 355262a..8ca134f 100644 --- a/views/auth/reset-password-form.ejs +++ b/views/auth/reset-password-form.ejs @@ -33,8 +33,13 @@ - - Back to Login + <% + let backToLoginUrl = `${process.env.FRONTEND_URL}/login`; + if (userType === 'client') { + backToLoginUrl = '/client/login'; + } + %> + Back to Login diff --git a/views/auth/reset-password-success.ejs b/views/auth/reset-password-success.ejs index 8c2c788..67676f3 100644 --- a/views/auth/reset-password-success.ejs +++ b/views/auth/reset-password-success.ejs @@ -12,7 +12,13 @@
- Go to Login + <% + let loginUrl = `${process.env.FRONTEND_URL}/login`; + if (userType === 'client') { + loginUrl = '/client/login'; + } + %> + Go to Login
diff --git a/views/client/dashboard-dashboard.ejs b/views/client/dashboard-dashboard.ejs new file mode 100644 index 0000000..c05be5a --- /dev/null +++ b/views/client/dashboard-dashboard.ejs @@ -0,0 +1,292 @@ + + + + + + DocVA Client | Dashboard + + + + +
+ +
+ +
+
+ +
+
+
+
+

Active Virtual Assistants

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

Total Billed Hours

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

Unpaid Invoices

+
$0.00
+
+
+ +
+
+
+ +
+
+
+

Outstanding Balance

+
$0.00
+
+
+ +
+
+
+
+ + +
+ +
+
+

Recent Timesheets

+ View All +
+
+ +
+
+ + +
+
+

Recent Invoices

+ View All +
+
+ +
+
+
+
+ +
+
+
+ + + + + \ No newline at end of file diff --git a/views/client/dashboard-invoices.ejs b/views/client/dashboard-invoices.ejs new file mode 100644 index 0000000..e69de29 diff --git a/views/client/dashboard-referrals.ejs b/views/client/dashboard-referrals.ejs new file mode 100644 index 0000000..e69de29 diff --git a/views/client/dashboard-timesheets.ejs b/views/client/dashboard-timesheets.ejs new file mode 100644 index 0000000..e69de29 diff --git a/views/client/dashboard-virtual-assistants.ejs b/views/client/dashboard-virtual-assistants.ejs new file mode 100644 index 0000000..ea6321c --- /dev/null +++ b/views/client/dashboard-virtual-assistants.ejs @@ -0,0 +1,286 @@ + + + + + + DocVA Client | Dashboard + + + + +
+ +
+ +
+
+ + + +
+
+
+
+

Active VAs

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

Unbilled Hours

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

Billed Hours

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

Billed Amount

+
$0.00
+
+
+ +
+
+
+
+ + +
+
+ + + + + + + + + + + + + + + +
StatusVirtual AssistantProjectUnbilled HoursBilled HoursBilled TipsBilled Amount
+
+
+
+
+
+
+ + + + + + \ No newline at end of file diff --git a/views/client_login.ejs b/views/client_login.ejs index 399c259..457ba08 100644 --- a/views/client_login.ejs +++ b/views/client_login.ejs @@ -88,7 +88,7 @@ } .login-card h2 { - margin-bottom: 25px; + margin-bottom: 15px; color: var(--color-muted); font-weight: 500; font-size: 0.95em; @@ -212,9 +212,7 @@
@@ -227,9 +225,9 @@ -
OR
+
@@ -247,6 +245,7 @@ } setTheme(getSystemTheme()); + checkAuthentication(); document.querySelector("form").addEventListener("submit", async (e) => { e.preventDefault(); // prevent page reload @@ -276,10 +275,11 @@ if (!res.ok) { throw new Error(data.message || "Login failed"); } - - if (data.token) { + + if (data.accessToken) { // 🟢 Save token - localStorage.setItem("authToken", data.token); + localStorage.setItem("knoptpq", data.accessToken); + localStorage.setItem("refreshToken", data.refreshToken); // 🟢 Redirect window.location.href = "/client/dashboard"; @@ -291,6 +291,43 @@ alert(err.message || "Something went wrong while logging in"); } }); + + // Check if user is already authenticated and redirect to dashboard + async function checkAuthentication() { + const authToken = localStorage.getItem("knoptpq"); + + if (!authToken) { + return false; // No token found + } + + try { + // Validate the token with the server + const response = await fetch("/api/v1/client/validate-token", { + method: "GET", + headers: { + "Authorization": `Bearer ${authToken}`, + "Content-Type": "application/json" + } + }); + + if (response.ok) { + // Token is valid, redirect to dashboard + window.location.href = "/client/dashboard"; + return true; + } else { + // Token is invalid, clear localStorage + localStorage.removeItem("knoptpq"); + localStorage.removeItem("refreshToken"); + return false; + } + } catch (error) { + console.error("Token validation error:", error); + // If validation fails, clear tokens to be safe + localStorage.removeItem("knoptpq"); + localStorage.removeItem("refreshToken"); + return false; + } + } diff --git a/views/forgot_password.ejs b/views/forgot_password.ejs index c9e1ff6..434bdcd 100644 --- a/views/forgot_password.ejs +++ b/views/forgot_password.ejs @@ -117,12 +117,12 @@
- +

DocTracker

Reset your password

- Go back to login + Go back to login
@@ -134,7 +134,7 @@ function applyTheme(isDark) { const theme = isDark ? "dark" : "light"; document.documentElement.setAttribute("data-theme", theme); - logo.src = isDark ? "/public/images/logo-white-vertical.svg" : "/public/images/logo-colored-vertical.svg"; + // logo.src = isDark ? "/public/images/logo-white-vertical.svg" : "/public/images/logo-colored-vertical.svg"; } // Initial set @@ -174,7 +174,9 @@ const data = await response.json(); if (response.ok) { - window.location.href = "/client/login"; // Redirect to reset password page + if(confirm('Please Check your mail inbox for password reset link')) { + window.location.href = "/client/login"; // Redirect to reset password page + } } else { alert("❌ " + (data.message || "Failed to send reset link.")); }