Merge branch 'merge2'

This commit is contained in:
=bhagwanSingh 2025-11-24 13:47:32 +05:30
commit dcda1629ff
27 changed files with 3012 additions and 216 deletions

View File

@ -80,7 +80,6 @@ body {
padding: 0 1.5rem; padding: 0 1.5rem;
gap: 1rem; gap: 1rem;
width: 100%; width: 100%;
/* max-width: 1024px; */
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
@ -188,7 +187,10 @@ body {
width: 16rem; width: 16rem;
background-color: var(--bg-card); background-color: var(--bg-card);
border-right: 1px solid var(--border-color); border-right: 1px solid var(--border-color);
display: none; /* display: none; */
}
.dashboard-sidebar.active {
display: block;
} }
@media (min-width: 768px) { @media (min-width: 768px) {
@ -196,6 +198,20 @@ body {
display: block; display: block;
} }
} }
@media (max-width: 768px) {
.dashboard-sidebar {
position: absolute;
left: 0;
bottom: 0;
z-index: 99;
top: 0;
transform: translateX(-100%);
transition: .3s ease;
}
.dashboard-sidebar.active {
transform: translateX(0);
}
}
.sidebar-header { .sidebar-header {
padding: 1.5rem; padding: 1.5rem;

View File

@ -476,3 +476,42 @@ body {
margin-bottom: 40px; margin-bottom: 40px;
} }
} }
.step-error-container {
background-color: #fef2f2;
border: 1px solid #fecaca;
border-radius: 8px;
padding: 12px 16px;
margin-bottom: 20px;
animation: slideDown 0.3s ease-out;
}
.error-message {
color: #dc2626;
font-size: 14px;
font-weight: 500;
display: flex;
align-items: center;
gap: 8px;
}
.error-message::before {
content: "⚠";
font-size: 16px;
}
.field-error {
border: 1px solid #dc2626 !important;
background-color: #fef2f2;
}
@keyframes slideDown {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}

File diff suppressed because it is too large Load Diff

View File

@ -109,7 +109,22 @@
// Add input event listeners to handle label animation // Add input event listeners to handle label animation
document.querySelectorAll('.input-field').forEach(input => { document.querySelectorAll('.input-field:not(.dob)').forEach(input => {
setTimeout(() => {
if (input.value) {
input.nextElementSibling.style.top = '4px';
input.nextElementSibling.style.fontSize = '12px';
input.nextElementSibling.style.transform = 'none';
}
}, 100);
input.addEventListener('animationstart', function() {
if (this.value) {
this.nextElementSibling.style.top = '4px';
this.nextElementSibling.style.fontSize = '12px';
this.nextElementSibling.style.transform = 'none';
}
});
input.addEventListener('input', function() { input.addEventListener('input', function() {
if (this.value) { if (this.value) {
this.nextElementSibling.style.top = '4px'; this.nextElementSibling.style.top = '4px';
@ -139,6 +154,13 @@
// Handle select field label animation // Handle select field label animation
document.querySelectorAll('.select-field').forEach(select => { document.querySelectorAll('.select-field').forEach(select => {
setTimeout(() => {
if (select.value) {
select.nextElementSibling.style.top = '4px';
select.nextElementSibling.style.fontSize = '12px';
select.nextElementSibling.style.transform = 'none';
}
}, 100);
select.addEventListener('change', function() { select.addEventListener('change', function() {
if (this.value) { if (this.value) {
this.nextElementSibling.style.top = '4px'; this.nextElementSibling.style.top = '4px';
@ -228,10 +250,14 @@
return maxStep; return maxStep;
} }
async function handleNextStep(step) {
await nextStep(step);
}
// Function to navigate to next step // Function to navigate to next step
function nextStep(step) { async function nextStep(step) {
// Validate current step before proceeding // Validate current step before proceeding
if (!validateStep(currentStep)) { if (!(await validateStep(currentStep))) {
return; return;
} }
@ -306,10 +332,14 @@
} }
// Function to validate current step // Function to validate current step
function validateStep(step) { async function validateStep(step) {
let isValid = true; let isValid = true;
const form = document.getElementById(getFormId(step)); const form = document.getElementById(getFormId(step));
// Hide any previous errors for this step
hideError(step);
clearFieldErrors();
if (form) { if (form) {
// For account setup step // For account setup step
if (step === 1) { if (step === 1) {
@ -319,21 +349,31 @@
// Email validation // Email validation
if (!validateEmail(email)) { if (!validateEmail(email)) {
alert('Please enter a valid email address'); showError('Please enter a valid email address', 'email', step);
isValid = false; isValid = false;
} else {
// Email verification - wait for the result
const isEmailVerified = await verifyEmail(email);
if (!isEmailVerified) {
showError('Email already registered. Please use a different email address.', 'email', step);
isValid = false;
}
} }
// Password validation // Password validation
if (password.length < 8) { if (password.length < 8) {
alert('Password must be at least 8 characters long'); showError('Password must be at least 8 characters long', 'password', step);
isValid = false; isValid = false;
} }
if (password !== confirmPassword) { if (password !== confirmPassword) {
document.getElementById('password-match-error').style.display = 'block'; document.getElementById('password-match-error').style.display = 'block';
document.getElementById('confirmPassword').classList.add('field-error');
showError('Passwords do not match', 'confirmPassword', step);
isValid = false; isValid = false;
} else { } else {
document.getElementById('password-match-error').style.display = 'none'; document.getElementById('password-match-error').style.display = 'none';
document.getElementById('confirmPassword').classList.remove('field-error');
} }
} }
@ -342,15 +382,18 @@
const firstName = document.getElementById('firstName').value; const firstName = document.getElementById('firstName').value;
if (!firstName) { if (!firstName) {
document.getElementById('profile-incomplete').style.display = 'block'; document.getElementById('profile-incomplete').style.display = 'block';
document.getElementById('firstName').classList.add('field-error');
showError('First name is required', 'firstName', step);
isValid = false; isValid = false;
} else { } else {
document.getElementById('profile-incomplete').style.display = 'none'; document.getElementById('profile-incomplete').style.display = 'none';
document.getElementById('firstName').classList.remove('field-error');
} }
// Age validation // Age validation
const age = document.getElementById('age').value; const age = document.getElementById('age').value;
if (age && (age < 18 || age > 100)) { if (age && (age < 18 || age > 100)) {
alert('You must be 18 years or older to register'); showError('You age must be 18 years or older to register', 'dob', step);
isValid = false; isValid = false;
} }
@ -359,12 +402,12 @@
const state = document.getElementById('state').value; const state = document.getElementById('state').value;
if (!country) { if (!country) {
alert('Please select your country'); showError('Please select your country', 'country', step);
isValid = false; isValid = false;
} }
if (!state) { if (!state) {
alert('Please select your state/region'); showError('Please select your state/region', 'state', step);
isValid = false; isValid = false;
} }
@ -373,9 +416,40 @@
requiredFields.forEach(field => { requiredFields.forEach(field => {
if (!field.value.trim()) { if (!field.value.trim()) {
isValid = false; isValid = false;
field.style.border = '1px solid #ef4444'; field.classList.add('field-error');
if (isValid === false) {
showError(`Please fill in ${field.labels[0]?.textContent || 'this field'}`, field.id, step);
}
} else { } else {
field.style.border = ''; field.classList.remove('field-error');
}
});
}
if (step === 3) {
// Phone number validation
const phone = document.getElementById('phone').value;
if (!phone) {
showError('Please enter your phone number', 'phone', step);
isValid = false;
} else if (!validatePhoneNumberStrict(phone)) {
document.getElementById('phone-error').style.display = 'block';
document.getElementById('phone').classList.add('field-error');
showError('Please enter a valid phone number (10-15 digits)', 'phone', step);
isValid = false;
} else {
document.getElementById('phone-error').style.display = 'none';
document.getElementById('phone').classList.remove('field-error');
}
// Validate other required fields in step 3
const requiredFields = form.querySelectorAll('[required]');
requiredFields.forEach(field => {
if (!field.value.trim()) {
isValid = false;
field.classList.add('field-error');
} else {
field.classList.remove('field-error');
} }
}); });
} }
@ -384,20 +458,20 @@
if (step === 4) { if (step === 4) {
const selectedPayment = document.querySelector('.payment-option.selected'); const selectedPayment = document.querySelector('.payment-option.selected');
if (!selectedPayment) { if (!selectedPayment) {
alert('Please select a payment method'); showError('Please select a payment method', null, step);
isValid = false; isValid = false;
} else { } else {
const paymentMethod = selectedPayment.getAttribute('onclick').match(/'([^']+)'/)[1]; const paymentMethod = selectedPayment.getAttribute('onclick').match(/'([^']+)'/)[1];
if (paymentMethod === 'payoneer') { if (paymentMethod === 'payoneer') {
const payoneerAccount = document.getElementById('payoneerAccount').value; const payoneerAccount = document.getElementById('payoneerAccount').value;
if (!payoneerAccount) { if (!payoneerAccount) {
alert('Please enter your Payoneer account number'); showError('Please enter your Payoneer account number', 'payoneerAccount', step);
isValid = false; isValid = false;
} }
} else if (paymentMethod === 'veem') { } else if (paymentMethod === 'veem') {
const veemAccount = document.getElementById('veemAccount').value; const veemAccount = document.getElementById('veemAccount').value;
if (!veemAccount) { if (!veemAccount) {
alert('Please enter your Veem account number'); showError('Please enter your Veem account number', 'veemAccount', step);
isValid = false; isValid = false;
} }
} }
@ -410,9 +484,9 @@
requiredFields.forEach(field => { requiredFields.forEach(field => {
if (!field.value.trim()) { if (!field.value.trim()) {
isValid = false; isValid = false;
field.style.border = '1px solid #ef4444'; field.classList.add('field-error');
} else { } else {
field.style.border = ''; field.classList.remove('field-error');
} }
}); });
} }
@ -598,12 +672,12 @@
}, 1000); }, 1000);
} else { } else {
// Handle server errors // Handle server errors
alert('There was an error submitting your registration. Please try again.'); showError('There was an error submitting your registration. Please try again.', null, 4);
} }
} catch (error) { } catch (error) {
// Handle network errors // Handle network errors
console.error('Error:', error); console.error('Error:', error);
alert('There was a network error. Please check your connection and try again.'); showError('There was a network error. Please check your connection and try again.', null, 4);
} finally { } finally {
// Restore button state // Restore button state
submitBtn.textContent = originalText; submitBtn.textContent = originalText;
@ -611,3 +685,220 @@
} }
} }
async function verifyEmail(email) {
try {
const response = await fetch('/api/v1/va/verify-va-email', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({"email": email})
});
if (response.ok) {
return false;
} else {
return true;
}
} catch (error) {
// Handle network errors
console.error('Error:', error);
showError('There was a network error. Please check your connection and try again.', null, 1);
return false;
}
}
// Function to calculate age from date of birth
function calculateAge(dob) {
if (!dob) return '';
const birthDate = new Date(dob);
const today = new Date();
let age = today.getFullYear() - birthDate.getFullYear();
const monthDiff = today.getMonth() - birthDate.getMonth();
// Adjust age if birthday hasn't occurred this year yet
if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < birthDate.getDate())) {
age--;
}
return age;
}
// Function to handle date of birth change and update age
function handleDOBChange() {
const dob = document.getElementById('dob').value;
const ageInput = document.getElementById('age');
if (dob) {
const age = calculateAge(dob);
ageInput.value = age;
// Trigger input event to update the label position
ageInput.dispatchEvent(new Event('input'));
// 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',);
ageInput.style.border = '1px solid #ef4444';
} else if (age > 100) {
// alert('Please enter a valid age');
showError('Please enter a valid age', 'dob');
ageInput.style.border = '1px solid #ef4444';
} else {
ageInput.style.border = '';
}
} else {
// Clear age if DOB is cleared
ageInput.value = '';
ageInput.dispatchEvent(new Event('input'));
}
}
// Function to handle age manual input and update DOB (optional)
function handleAgeChange() {
const age = document.getElementById('age').value;
const dobInput = document.getElementById('dob');
if (age && age >= 18 && age <= 100) {
// Calculate approximate DOB based on age
const today = new Date();
const birthYear = today.getFullYear() - age;
const approximateDOB = `${birthYear}-${String(today.getMonth() + 1).padStart(2, '0')}-${String(today.getDate()).padStart(2, '0')}`;
// You can set this as the DOB or just leave it for reference
// dobInput.value = approximateDOB;
// dobInput.dispatchEvent(new Event('change'));
}
}
// Function to validate phone number in real-time
function validatePhoneNumber(input) {
const phoneError = document.getElementById('phone-error');
const value = input.value;
// Remove any non-digit characters
const digitsOnly = value.replace(/\D/g, '');
// Update the input value with digits only
if (value !== digitsOnly) {
input.value = digitsOnly;
}
// Validate length
if (digitsOnly.length > 0 && (digitsOnly.length < 10 || digitsOnly.length > 15)) {
phoneError.style.display = 'block';
input.style.border = '1px solid #ef4444';
return false;
} else {
phoneError.style.display = 'none';
input.style.border = '';
return true;
}
}
// Function to validate phone number on form submission
function validatePhoneNumberStrict(phone) {
const phoneRegex = /^[0-9]{10,15}$/;
return phoneRegex.test(phone);
}
// Function to validate Payoneer account number
function validatePayoneerAccount(input) {
const payoneerError = document.getElementById('payoneer-error');
const value = input.value.trim();
// Payoneer typically uses email format or alphanumeric IDs
// Adjust validation rules based on Payoneer's actual format requirements
if (value.length > 0 && (value.length < 5 || value.length > 20)) {
payoneerError.textContent = 'Payoneer account must be 5-20 characters';
payoneerError.style.display = 'block';
input.style.border = '1px solid #ef4445';
return false;
} else {
payoneerError.style.display = 'none';
input.style.border = '';
return true;
}
}
// Function to validate Veem account number
function validateVeemAccount(input) {
const veemError = document.getElementById('veem-error');
const value = input.value.trim();
// Veem account numbers are typically longer alphanumeric strings
if (value.length > 0 && (value.length < 8 || value.length > 25)) {
veemError.textContent = 'Veem account must be 8-25 characters';
veemError.style.display = 'block';
input.style.border = '1px solid #ef4445';
return false;
} else {
veemError.style.display = 'none';
input.style.border = '';
return true;
}
}
function showError(message, fieldId = null, step = null) {
// Determine the current step if not provided
const currentStep = step || getCurrentStep();
const errorContainer = document.getElementById(`step-${currentStep}-error`);
const errorMessage = document.getElementById(`step-${currentStep}-error-message`);
if (errorContainer && errorMessage) {
errorMessage.textContent = message;
errorContainer.style.display = 'block';
// Scroll to error container
errorContainer.scrollIntoView({ behavior: 'smooth', block: 'center' });
// 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);
}
}
// Auto-hide error after 8 seconds
setTimeout(() => {
hideError(currentStep);
}, 8000);
}
}
function hideError(step = null) {
if (step) {
// Hide specific step error
const errorContainer = document.getElementById(`step-${step}-error`);
if (errorContainer) {
errorContainer.style.display = 'none';
}
} else {
// Hide all step errors
for (let i = 1; i <= 5; i++) {
const errorContainer = document.getElementById(`step-${i}-error`);
if (errorContainer) {
errorContainer.style.display = 'none';
}
}
}
}
function clearFieldErrors() {
document.querySelectorAll('.field-error').forEach(field => {
field.classList.remove('field-error');
});
}
function getCurrentStep() {
return currentStep;
}

View File

@ -0,0 +1,100 @@
import { ClientModel, ProjectModel, VAModel } from "../../../models/vaModel.js";
import jwt from "jsonwebtoken";
function formatHoursToReadable(hours) {
const wholeHours = Math.floor(hours);
const decimalMinutes = (hours - wholeHours) * 60;
const wholeMinutes = Math.round(decimalMinutes);
if (wholeHours > 0 && wholeMinutes > 0) {
return `${wholeHours}h ${wholeMinutes}m`;
} else if (wholeHours > 0) {
return `${wholeHours}h`;
} else {
return `${wholeMinutes}m`;
}
}
// Getting client dashboard data - all VAs and projects
async function client_dashboard_projects(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" });
}
// Get all VA IDs and Project IDs assigned to this client
const clientVaIds = clientUser.vaIds || [];
const clientProjectIds = clientUser.projectIds || [];
// Get all VAs assigned to this client (with required fields only)
const allAssignedVAs = await VAModel.find({
_id: { $in: clientVaIds }
})
.select('userDetails.firstName userDetails.lastName userDetails.email status')
.lean()
.exec();
// Get All Projects of client with required fields
const clientProjects = await ProjectModel.find({
_id: { $in: clientProjectIds }
})
.select('name hourlyRate vaAssigned dedicatedHours status')
.lean()
.exec();
// Format VAs with only required fields
const vasFormatted = allAssignedVAs.map((va) => ({
id: va._id,
firstName: va.userDetails?.firstName || '',
lastName: va.userDetails?.lastName || '',
email: va.userDetails?.email || '',
status: va.status || ''
}));
// Format projects with only required fields
const projectsFormatted = clientProjects.map((project) => ({
id: project._id,
name: project.name,
hourlyRate: project.hourlyRate,
vaAssigned: project.vaAssigned.length || 0,
dedicatedHours: formatHoursToReadable(project.dedicatedHours) || 0,
status: project.status || ''
}));
return res.status(200).json({
success: true,
data: {
vas: vasFormatted,
projects: projectsFormatted
},
counts: {
totalVAs: vasFormatted.length,
totalProjects: projectsFormatted.length
}
});
} catch (error) {
console.error("Client dashboard error:", error);
return res.status(500).json({
success: false,
message: "Internal Server Error",
error: error.message
});
}
}
export { client_dashboard_projects };

View File

@ -0,0 +1,32 @@
import { ClientModel } from "../../models/vaModel.js";
// Verify VA Email
async function client_verify_email(req, res) {
try {
// const payload = req.body ? JSON.parse(req.body) : {};
const payload = req.body || {};
const { email } = payload;
if (!email) {
return res.status(400).json({ success: false, message: "Please provide email" });
}
// Find user by email
const user = await ClientModel.findOne({ "userDetails.email": email });
if (!user) {
return res.status(404).json({ success: false, message: "Email not found" });
}
// Return accessToken and user info
return res.status(200).json({
success: true,
email: user.userDetails?.email,
message: "Email ID is already registerd"
});
} catch (error) {
console.error("va_register error:", error);
return res.status(500).json({ success: false, message: "Internal Server Error" });
}
}
export { client_verify_email };

View File

@ -42,10 +42,53 @@ async function closePreviousSession(vaId, lastLog) {
}); });
// console.log(`Created logout log: ${logoutLog._id}`); // console.log(`Created logout log: ${logoutLog._id}`);
// try {
// const salesforceUrl = `${process.env.SF_DOCTRACKER_API_URL}services/apexrest/docva/api/va-project-log`;
// const sfDataSync = {
// "_id": logoutLog._id,
// "loggedType": "LOGOUT",
// "vaId": vaId,
// "vaEmail": lastLog.vaEmail,
// "projectId": lastLog.projectId,
// "time": logoutLog.time,
// }
// 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(sfDataSync)
// });
// 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);
// return {
// closedWorkTracks: openWorkTracks.length,
// logoutLogId: logoutLog._id,
// SyncSalesforce: result
// };
// } catch (error) {
// console.error("Failed to sync with Salesforce:", error.message);
// console.log(error)
// // Create a custom error to distinguish Salesforce errors
// const salesforceError = new Error(`Salesforce sync failed: ${error.message}`);
// salesforceError.isSalesforceError = true;
// throw salesforceError;
// }
return { return {
closedWorkTracks: openWorkTracks.length, closedWorkTracks: openWorkTracks.length,
logoutLogId: logoutLog._id logoutLogId: logoutLog._id,
}; };
} catch (error) { } catch (error) {

View File

@ -0,0 +1,32 @@
import { VAModel } from "../../../models/vaModel.js";
// Verify VA Email
async function va_verify_email(req, res) {
try {
// const payload = req.body ? JSON.parse(req.body) : {};
const payload = req.body || {};
const { email } = payload;
if (!email) {
return res.status(400).json({ success: false, message: "Please provide email" });
}
// Find user by email
const user = await VAModel.findOne({ "userDetails.email": email });
if (!user) {
return res.status(404).json({ success: false, message: "Invalid Email ID" });
}
// Return accessToken and user info
return res.status(200).json({
success: true,
email: user.userDetails?.email,
message: "Email verified successfully"
});
} catch (error) {
console.error("va_register error:", error);
return res.status(500).json({ success: false, message: "Internal Server Error" });
}
}
export { va_verify_email };

View File

@ -0,0 +1,101 @@
import jwt from "jsonwebtoken";
import { VAModel, LeavesModel } from "../../../models/vaModel.js";
// Get all leave lists applied by VA
async function va_leave_list_function(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 vaEmail = decoded.email;
const vaUser = await VAModel.findOne({ "userDetails.email": vaEmail });
if (!vaUser) {
return res.status(404).json({ success: false, message: "VA not found" });
}
const vaId = vaUser._id;
// Get parameters from route params (they are optional)
const { status, type } = req.params;
// Get sorting from query params (optional)
const {
sortBy = 'createdAt',
sortOrder = 'desc'
} = req.query;
// Build filter object
const filter = { vaId };
// Handle status filter from params
if (status && status !== 'ALL') {
filter.status = status.toUpperCase();
}
// Handle type filter from params (using 'type' instead of 'leaveType')
if (type && type !== 'ALL') {
filter.leaveType = type.toUpperCase();
}
// Validate sort parameters
const validSortFields = ['createdAt', 'dateFrom', 'dateTo', 'status', 'leaveType'];
const sortField = validSortFields.includes(sortBy) ? sortBy : 'createdAt';
const sortDirection = sortOrder.toLowerCase() === 'asc' ? 1 : -1;
// Get all leaves without pagination
const leaves = await LeavesModel.find(filter)
.sort({ [sortField]: sortDirection })
.select('-__v')
.lean();
// Format the response data
const formattedLeaves = leaves.map(leave => ({
leaveId: leave._id,
dateFrom: leave.dateFrom.toISOString().split('T')[0],
dateTo: leave.dateTo.toISOString().split('T')[0],
totalDays: parseInt(leave.days),
leaveType: leave.leaveType,
comment: leave.comment,
status: leave.status,
appliedOn: leave.createdAt,
reviewedAt: leave.reviewedAt || null,
rejectionReason: leave.rejectionReason || null
}));
return res.status(200).json({
success: true,
message: "Leave list retrieved successfully",
data: {
leaves: formattedLeaves,
totalCount: formattedLeaves.length
}
});
} catch (error) {
console.error("Leave list error:", error);
if (error.name === 'JsonWebTokenError') {
return res.status(401).json({ success: false, message: "Invalid token" });
}
if (error.name === 'TokenExpiredError') {
return res.status(401).json({ success: false, message: "Token expired" });
}
return res.status(500).json({
success: false,
message: "Internal Server Error",
error: error.message
});
}
}
export { va_leave_list_function };

View File

@ -0,0 +1,114 @@
import jwt from "jsonwebtoken";
import { VAModel, LeavesModel } from "../../../models/vaModel.js";
// Approve or reject leave
async function va_leave_manage_function(req, res) {
try {
const { leaveId } = req.params;
const { action, rejectionReason } = req.body;
if (!leaveId) {
return res.status(400).json({
success: false,
message: "Leave ID is required"
});
}
if (!action || !['APPROVE', 'REJECT'].includes(action.toUpperCase())) {
return res.status(400).json({
success: false,
message: "Action is required and must be either 'APPROVE' or 'REJECT'"
});
}
// If rejecting, rejectionReason is required
if (action.toUpperCase() === 'REJECT' && !rejectionReason) {
return res.status(400).json({
success: false,
message: "Rejection reason is required when rejecting a leave"
});
}
// Find the leave request
const leaveRequest = await LeavesModel.findById(leaveId);
if (!leaveRequest) {
return res.status(404).json({
success: false,
message: "Leave request not found"
});
}
// Check if leave is already processed
if (leaveRequest.status !== 'PENDING') {
return res.status(400).json({
success: false,
message: `Leave request is already ${leaveRequest.status.toLowerCase()}`
});
}
// Prepare update data
const updateData = {
status: action.toUpperCase() === 'APPROVE' ? 'APPROVED' : 'REJECTED',
reviewedAt: new Date()
};
// Add rejection reason if rejecting
if (action.toUpperCase() === 'REJECT') {
updateData.rejectionReason = rejectionReason;
}
// Update the leave request
const updatedLeave = await LeavesModel.findByIdAndUpdate(
leaveId,
updateData,
{ new: true } // Return updated document
).select('-__v').lean();
// Format the response
const formattedLeave = {
leaveId: updatedLeave._id,
dateFrom: updatedLeave.dateFrom.toISOString().split('T')[0],
dateTo: updatedLeave.dateTo.toISOString().split('T')[0],
totalDays: parseInt(updatedLeave.days),
leaveType: updatedLeave.leaveType,
comment: updatedLeave.comment,
status: updatedLeave.status,
appliedOn: updatedLeave.createdAt,
reviewedAt: updatedLeave.reviewedAt,
rejectionReason: updatedLeave.rejectionReason || null
};
return res.status(200).json({
success: true,
message: `Leave request ${action.toLowerCase()}d successfully`,
data: formattedLeave
});
} catch (error) {
console.error("Leave approve/reject error:", error);
if (error.name === 'JsonWebTokenError') {
return res.status(401).json({ success: false, message: "Invalid token" });
}
if (error.name === 'TokenExpiredError') {
return res.status(401).json({ success: false, message: "Token expired" });
}
if (error.name === 'CastError') {
return res.status(400).json({
success: false,
message: "Invalid leave ID format"
});
}
return res.status(500).json({
success: false,
message: "Internal Server Error",
error: error.message
});
}
}
export { va_leave_manage_function };

View File

@ -0,0 +1,241 @@
import jwt from "jsonwebtoken";
import { VAModel, LeavesModel } from "../../../models/vaModel.js";
// Apply for leave
async function va_leave_apply_function(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 vaEmail = decoded.email;
const vaUser = await VAModel.findOne({ "userDetails.email": vaEmail });
if (!vaUser) {
return res.status(404).json({ success: false, message: "VA not found" });
}
const vaId = vaUser._id;
const { dateFrom, dateTo, leaveType, comment } = req.body;
if (!dateFrom || !dateTo || !leaveType || !comment) {
return res.status(400).json({
success: false,
message: "dateFrom, dateTo, leaveType, and comment are required"
});
}
// Validate date formats
let startDate, endDate;
try {
startDate = new Date(dateFrom);
endDate = new Date(dateTo);
// Set to start of day for accurate calculation
startDate.setHours(0, 0, 0, 0);
endDate.setHours(0, 0, 0, 0);
if (isNaN(startDate.getTime()) || isNaN(endDate.getTime())) {
return res.status(400).json({
success: false,
message: "Invalid date format. Use valid date format (YYYY-MM-DD)"
});
}
} catch (error) {
return res.status(400).json({
success: false,
message: "Invalid date format"
});
}
// Ensure start date is before or equal to end date
if (startDate > endDate) {
return res.status(400).json({
success: false,
message: "Start date must be before or equal to end date"
});
}
// Ensure dates are not in the past
const today = new Date();
today.setHours(0, 0, 0, 0);
if (startDate < today) {
return res.status(400).json({
success: false,
message: "Cannot apply for leave for past dates"
});
}
// Validate leave type
const validLeaveTypes = ["SICK", "PTO"];
if (!validLeaveTypes.includes(leaveType)) {
return res.status(400).json({
success: false,
message: "Invalid leave type. Must be SICK or PTO"
});
}
// Calculate total days (inclusive of both start and end dates)
const timeDifference = endDate.getTime() - startDate.getTime();
const totalDays = Math.floor(timeDifference / (1000 * 60 * 60 * 24)) + 1; // +1 to include both start and end dates
if (totalDays <= 0) {
return res.status(400).json({
success: false,
message: "Invalid date range"
});
}
// Check for overlapping leave requests
const existingLeave = await LeavesModel.findOne({
vaId,
status: "PENDING",
$or: [
{
dateFrom: { $lte: endDate },
dateTo: { $gte: startDate }
}
]
});
if (existingLeave) {
return res.status(400).json({
success: false,
message: "You already have a pending leave request that overlaps with these dates"
});
}
// Check for overlapping approved leaves
const existingApprovedLeave = await LeavesModel.findOne({
vaId,
status: "APPROVED",
$or: [
{
dateFrom: { $lte: endDate },
dateTo: { $gte: startDate }
}
]
});
if (existingApprovedLeave) {
return res.status(400).json({
success: false,
message: "You already have an approved leave that overlaps with these dates"
});
}
// Create leave request
const leaveRequest = new LeavesModel({
vaId,
dateFrom: startDate,
dateTo: endDate,
days: totalDays.toString(),
leaveType,
comment,
status: "PENDING"
});
await leaveRequest.save();
// try {
// const salesforceUrl = `${process.env.SF_DOCTRACKER_API_URL}services/apexrest/docva/api/timesheet-adjustment`;
// const sfData = {
// "id": adjustmentRequest._id,
// "va": {
// "id": vaId
// },
// "project": {
// "id": projectId
// },
// "date": adjustmentDate.toISOString().split('T')[0],
// "loginTime": loginDateTime.toISOString(),
// "logoutTime": logoutDateTime.toISOString(),
// "comment": comment,
// "status": "PENDING",
// "rejectionReason": "",
// "adjustedHours": formatTime(adjustedSeconds)
// }
// 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({"data": sfData})
// });
// 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);
// return res.status(201).json({
// success: true,
// message: "Adjustment request submitted successfully",
// data: {
// adjustmentId: adjustmentRequest._id,
// date: adjustmentDate.toISOString().split('T')[0],
// loginTime: loginDateTime.toISOString(),
// logoutTime: logoutDateTime.toISOString(),
// adjustedHours: formatTime(adjustedSeconds),
// comment: comment,
// status: "PENDING"
// },
// SyncSalesforce: result
// });
// } catch (error) {
// console.error("Failed to sync with Salesforce:", error.message);
// console.log(error)
// // Create a custom error to distinguish Salesforce errors
// const salesforceError = new Error(`Salesforce sync failed: ${error.message}`);
// salesforceError.isSalesforceError = true;
// throw salesforceError;
// }
return res.status(201).json({
success: true,
message: "Leave request submitted successfully",
data: {
leaveId: leaveRequest._id,
dateFrom: startDate.toISOString().split('T')[0],
dateTo: endDate.toISOString().split('T')[0],
totalDays: totalDays,
leaveType,
comment,
status: "PENDING"
}
});
} catch (error) {
console.error("Leave apply error:", error);
if (error.name === 'JsonWebTokenError') {
return res.status(401).json({ success: false, message: "Invalid token" });
}
if (error.name === 'TokenExpiredError') {
return res.status(401).json({ success: false, message: "Token expired" });
}
return res.status(500).json({
success: false,
message: "Internal Server Error",
error: error.message
});
}
}
export { va_leave_apply_function };

View File

@ -66,12 +66,51 @@ async function checkDailyHoursAndLogout(vaId, vaEmail, projectId, project, curre
projectId projectId
}); });
try {
const salesforceUrl = `${process.env.SF_DOCTRACKER_API_URL}services/apexrest/docva/api/va-project-log`;
const sfDataSync = {
"_id": logoutLog._id,
"loggedType": "LOGOUT",
"vaId": vaId,
"vaEmail": vaEmail,
"projectId": projectId,
"time": logoutLog.time,
}
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(sfDataSync)
});
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);
return { return {
shouldLogout: true, shouldLogout: true,
totalTodayHours: totalTodayHours, totalTodayHours: totalTodayHours,
dedicatedHours: project.dedicatedHours, dedicatedHours: project.dedicatedHours,
logoutLogId: logoutLog._id logoutLogId: logoutLog._id,
SyncSalesforce: result
}; };
} catch (error) {
console.error("Failed to sync with Salesforce:", error.message);
console.log(error)
// Create a custom error to distinguish Salesforce errors
const salesforceError = new Error(`Salesforce sync failed: ${error.message}`);
salesforceError.isSalesforceError = true;
throw salesforceError;
}
} }
return { return {

View File

@ -66,11 +66,49 @@ async function closePreviousSession(vaId, lastLog) {
}); });
// console.log(`Created logout log: ${logoutLog._id}`); // console.log(`Created logout log: ${logoutLog._id}`);
try {
const salesforceUrl = `${process.env.SF_DOCTRACKER_API_URL}services/apexrest/docva/api/va-project-log`;
const sfDataSync = {
"_id": logoutLog._id,
"loggedType": "LOGOUT",
"vaId": vaId,
"vaEmail": lastLog.vaEmail,
"projectId": lastLog.projectId,
"time": logoutLog.time,
}
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(sfDataSync)
});
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);
return { return {
closedWorkTracks: openWorkTracks.length, closedWorkTracks: openWorkTracks.length,
logoutLogId: logoutLog._id logoutLogId: logoutLog._id,
SyncSalesforce: result
}; };
} catch (error) {
console.error("Failed to sync with Salesforce:", error.message);
console.log(error)
// Create a custom error to distinguish Salesforce errors
const salesforceError = new Error(`Salesforce sync failed: ${error.message}`);
salesforceError.isSalesforceError = true;
throw salesforceError;
}
} catch (error) { } catch (error) {
console.error("Error closing previous session:", error); console.error("Error closing previous session:", error);
@ -151,47 +189,47 @@ async function va_project_log_callback(req, res) {
const createdLog = await LoggedModel.create({ loggedType: type, vaId, vaEmail, projectId }) const createdLog = await LoggedModel.create({ loggedType: type, vaId, vaEmail, projectId })
// console.log(createdLog) // console.log(createdLog)
try { // try {
const salesforceUrl = `${process.env.SF_DOCTRACKER_API_URL}services/apexrest/docva/api/va-project-log`; // const salesforceUrl = `${process.env.SF_DOCTRACKER_API_URL}services/apexrest/docva/api/va-project-log`;
const sfData = { // const sfData = {
"_id": createdLog._id, // "_id": createdLog._id,
"loggedType": type, // "loggedType": type,
"vaId": vaId, // "vaId": vaId,
"vaEmail": vaEmail, // "vaEmail": vaEmail,
"projectId": projectId, // "projectId": projectId,
"time": createdLog.time, // "time": createdLog.time,
} // }
const response = await fetch(salesforceUrl, { // const response = await fetch(salesforceUrl, {
method: 'POST', // method: 'POST',
headers: { // headers: {
'Content-Type': 'application/json', // 'Content-Type': 'application/json',
// Add any required headers for Salesforce authentication // // Add any required headers for Salesforce authentication
// 'Authorization': 'Bearer ' + process.env.SF_ACCESS_TOKEN // // 'Authorization': 'Bearer ' + process.env.SF_ACCESS_TOKEN
}, // },
body: JSON.stringify(sfData) // body: JSON.stringify(sfData)
}); // });
if (!response.ok) { // if (!response.ok) {
const errorText = await response.text(); // const errorText = await response.text();
throw new Error(`Salesforce API responded with status ${response.status}: ${errorText}`); // throw new Error(`Salesforce API responded with status ${response.status}: ${errorText}`);
} // }
const result = await response.json(); // const result = await response.json();
// console.log("✅ Successfully synced VA data to Salesforce:", result); // // console.log("✅ Successfully synced VA data to Salesforce:", result);
res.status(201).json({ success: true, message: `${type} Successfully`, logId: createdLog._id, SyncSalesforce: result }); // res.status(201).json({ success: true, message: `${type} Successfully`, logId: createdLog._id, SyncSalesforce: result });
} catch (error) { // } catch (error) {
console.error("Failed to sync with Salesforce:", error.message); // console.error("Failed to sync with Salesforce:", error.message);
console.log(error) // console.log(error)
// Create a custom error to distinguish Salesforce errors // // Create a custom error to distinguish Salesforce errors
const salesforceError = new Error(`Salesforce sync failed: ${error.message}`); // const salesforceError = new Error(`Salesforce sync failed: ${error.message}`);
salesforceError.isSalesforceError = true; // salesforceError.isSalesforceError = true;
throw salesforceError; // throw salesforceError;
} // }
// return res.status(201).send({ success: true, message: `${type} Successfully`, logId: createdLog._id }); return res.status(201).send({ success: true, message: `${type} Successfully`, logId: createdLog._id });
} catch (error) { } catch (error) {
return res.status(500).send("Internal Server Error"); return res.status(500).send("Internal Server Error");

View File

@ -140,6 +140,43 @@ async function va_timesheet_adjustment_function(req, res) {
return `${h}h ${m}m ${s}s`; return `${h}h ${m}m ${s}s`;
}; };
try {
const salesforceUrl = `${process.env.SF_DOCTRACKER_API_URL}services/apexrest/docva/api/timesheet-adjustment`;
const sfData = {
"id": adjustmentRequest._id,
"va": {
"id": vaId
},
"project": {
"id": projectId
},
"date": adjustmentDate.toISOString().split('T')[0],
"loginTime": loginDateTime.toISOString(),
"logoutTime": logoutDateTime.toISOString(),
"comment": comment,
"status": "PENDING",
"rejectionReason": "",
"adjustedHours": formatTime(adjustedSeconds)
}
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({"data": sfData})
});
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);
return res.status(201).json({ return res.status(201).json({
success: true, success: true,
message: "Adjustment request submitted successfully", message: "Adjustment request submitted successfully",
@ -151,8 +188,32 @@ async function va_timesheet_adjustment_function(req, res) {
adjustedHours: formatTime(adjustedSeconds), adjustedHours: formatTime(adjustedSeconds),
comment: comment, comment: comment,
status: "PENDING" status: "PENDING"
} },
SyncSalesforce: result
}); });
} catch (error) {
console.error("Failed to sync with Salesforce:", error.message);
console.log(error)
// Create a custom error to distinguish Salesforce errors
const salesforceError = new Error(`Salesforce sync failed: ${error.message}`);
salesforceError.isSalesforceError = true;
throw salesforceError;
}
// return res.status(201).json({
// success: true,
// message: "Adjustment request submitted successfully",
// data: {
// adjustmentId: adjustmentRequest._id,
// date: adjustmentDate.toISOString().split('T')[0],
// loginTime: loginDateTime.toISOString(),
// logoutTime: logoutDateTime.toISOString(),
// adjustedHours: formatTime(adjustedSeconds),
// comment: comment,
// status: "PENDING"
// }
// });
} catch (error) { } catch (error) {
console.error("Timesheet adjustment error:", error); console.error("Timesheet adjustment error:", error);

View File

@ -46,17 +46,39 @@ async function updateEntityRelationships(data) {
results.vaUpdate = `Updated ${vaUpdateResult.modifiedCount} VAs`; results.vaUpdate = `Updated ${vaUpdateResult.modifiedCount} VAs`;
} }
// Update Project with clientId and vaId // // Update Project with clientId and vaId
// const projectUpdateData = {};
// if (clientId) {
// projectUpdateData.clientId = clientId;
// }
// // 🐞🐞🐞 Here Duplicate Entry of Va is Created if SF pass same va multiple times in same req
// if (vaId && Array.isArray(vaId)) {
// projectUpdateData.vaAssigned = vaId;
// }
// if (Object.keys(projectUpdateData).length > 0) {
// const projectUpdate = await ProjectModel.findByIdAndUpdate(
// projectId,
// projectUpdateData,
// { new: true }
// );
// results.projectUpdate = projectUpdate ? 'Project updated successfully' : 'Project not found';
// }
if (vaId && Array.isArray(vaId)) {
await ProjectModel.findByIdAndUpdate(
projectId,
{ $addToSet: { vaAssigned: { $each: vaId } } }
);
}
// Update other fields normally
const projectUpdateData = {}; const projectUpdateData = {};
if (clientId) { if (clientId) {
projectUpdateData.clientId = clientId; projectUpdateData.clientId = clientId;
} }
// 🐞🐞🐞 Here Duplicate Entry of Va is Created if SF pass same va multiple times in same req
if (vaId && Array.isArray(vaId)) {
projectUpdateData.vaAssigned = vaId;
}
if (Object.keys(projectUpdateData).length > 0) { if (Object.keys(projectUpdateData).length > 0) {
const projectUpdate = await ProjectModel.findByIdAndUpdate( const projectUpdate = await ProjectModel.findByIdAndUpdate(
projectId, projectId,
@ -215,14 +237,37 @@ async function removeEntityRelationships(data) {
// Remove projectId from VAs // Remove projectId from VAs
if (vaId && Array.isArray(vaId)) { if (vaId && Array.isArray(vaId)) {
await VAModel.updateMany( // await VAModel.updateMany(
{ _id: { $in: vaId } }, // { _id: { $in: vaId } },
{ $pull: { // { $pull: {
projectIds: projectId, // projectIds: projectId,
// 🐞🐞🐞 Here before removing we need to check if VA is still assigned to other projects of the same client // // 🐞🐞🐞 Here before removing we need to check if VA is still assigned to other projects of the same client
// clientIds: clientId
// }}
// );
for (const va of vaId) {
// Check if VA is still assigned to other projects of the same client
const otherProjectsWithSameClient = await ProjectModel.countDocuments({
_id: { $ne: projectId },
clientId: clientId,
vaAssigned: va
});
// Prepare update operation
const updateOperation = {
$pull: { projectIds: projectId }
};
// Only remove clientId if VA is not assigned to any other projects from this client
if (!otherProjectsWithSameClient) {
updateOperation.$pull = {
...updateOperation.$pull,
clientIds: clientId clientIds: clientId
}} };
); }
await VAModel.findByIdAndUpdate(va, updateOperation);
}
} }
results.projectRemoval = 'Project relationships removed successfully'; results.projectRemoval = 'Project relationships removed successfully';

View File

@ -424,6 +424,51 @@ const adjustedHoursSchema = new Schema({
}] }]
}, { timestamps: true }); }, { timestamps: true });
const leavesSchema = new Schema({
vaId: {
type: Types.ObjectId,
ref: "VA",
required: true
},
clientId: {
type: Types.ObjectId,
ref: "Client"
},
dateFrom: {
type: Date,
required: true
},
dateTo: {
type: Date,
required: true
},
days: {
type: String,
required: true
},
comment: {
type: String,
required: true
},
leaveType: {
type: String,
enum: ["SICK", "PTO"],
default: "SICK",
required: true
},
status: {
type: String,
enum: ["PENDING", "APPROVED", "REJECTED"],
default: "PENDING"
},
reviewedAt: {
type: Date
},
rejectionReason: {
type: String // If rejected, store the reason
}
}, { timestamps: true });
/* ---------------------- Model Exports ---------------------- */ /* ---------------------- Model Exports ---------------------- */
const VAModel = model("VA", vaSchema); const VAModel = model("VA", vaSchema);
const ClientModel = model("Client", clientSchema); const ClientModel = model("Client", clientSchema);
@ -432,5 +477,6 @@ const LoggedModel = model("TrackLog", loggedSchema);
const WorkTrackModel = model("TrackWork", vaWorkTrackSchema); const WorkTrackModel = model("TrackWork", vaWorkTrackSchema);
const PasswordResetModel = model("PasswordReset", passwordResetSchema); const PasswordResetModel = model("PasswordReset", passwordResetSchema);
const AdjustedHoursModel = model("AdjustedHours", adjustedHoursSchema); const AdjustedHoursModel = model("AdjustedHours", adjustedHoursSchema);
const LeavesModel = model("Leaves", leavesSchema);
export { VAModel, ClientModel, ProjectModel, LoggedModel, WorkTrackModel, PasswordResetModel, AdjustedHoursModel }; export { VAModel, ClientModel, ProjectModel, LoggedModel, WorkTrackModel, PasswordResetModel, AdjustedHoursModel, LeavesModel };

View File

@ -9,13 +9,16 @@ import { client_refresh_token_callback } from "../../controllers/client/clientRe
import { client_request_password_reset } from "../../controllers/passwordReset.controllers.js"; import { client_request_password_reset } from "../../controllers/passwordReset.controllers.js";
import { clientAuthValidator } from "../../middlewares/auth.middleware.js"; import { clientAuthValidator } from "../../middlewares/auth.middleware.js";
import { client_dashboard } from "./../../controllers/client/dashboard/clientDashboard.controller.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_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"
// clients routes // clients routes
clientRoutes.post("/register", va_register_client); // Register a new VA clientRoutes.post("/register", va_register_client); // Register a new VA
clientRoutes.post("/login", va_login_client); // Login Client clientRoutes.post("/login", va_login_client); // Login Client
clientRoutes.get("/refreshToken", client_refresh_token_callback); // Client Refresh Token clientRoutes.get("/refreshToken", client_refresh_token_callback); // Client Refresh Token
clientRoutes.post("/forgot-password", client_request_password_reset); clientRoutes.post("/forgot-password", client_request_password_reset);
clientRoutes.post("/verify-client-email", client_verify_email);
// clientRoutes.get("/:clientID?", va_clients_callback); // VA Clients Callback for getting all VA's or specific client by ID (NC) // 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) clientRoutes.get("/:clientId/projects/:projectId", va_clients_projects_callback); // VA Clients Projects Callback (NC)
@ -24,7 +27,8 @@ clientRoutes.get("/:clientId/projects/:projectId", va_clients_projects_callback)
// dashboard API's // dashboard API's
clientRoutes.get("/dashboard", clientAuthValidator, client_dashboard); // VA Clients Projects Callback clientRoutes.get("/dashboard", clientAuthValidator, client_dashboard); // VA Clients Projects Callback
clientRoutes.post("/dashboard/va-report", clientAuthValidator, client_dashboard_va_log); // VA Clients Projects Callback clientRoutes.post("/dashboard/va-report", clientAuthValidator, client_dashboard_va_log); // VA details
clientRoutes.get("/dashboard-projects", clientAuthValidator, client_dashboard_projects); // VA Clients Projects Callback

View File

@ -22,6 +22,10 @@ import { va_timesheet_summary_function } from "../../controllers/va/vaTimesheetA
import { va_timesheet_adjustment_function } from "../../controllers/va/vaTimesheetAdjustment/vaTimesheetAdjustment.controller.js"; import { va_timesheet_adjustment_function } from "../../controllers/va/vaTimesheetAdjustment/vaTimesheetAdjustment.controller.js";
import { get_adjustment_requests_list } from "../../controllers/va/vaTimesheetAdjustment/vaAllTimesheetAdjustmentReuqest.controller.js" import { get_adjustment_requests_list } from "../../controllers/va/vaTimesheetAdjustment/vaAllTimesheetAdjustmentReuqest.controller.js"
import { va_timesheet_manage_adjustment_request } from "../../controllers/va/vaTimesheetAdjustment/vaManageTimesheetAdjustment.controller.js" import { va_timesheet_manage_adjustment_request } from "../../controllers/va/vaTimesheetAdjustment/vaManageTimesheetAdjustment.controller.js"
import { va_verify_email } from "../../controllers/va/vaAuth/vaVerifyEmail.controller.js";
import { va_leave_apply_function } from "../../controllers/va/vaLeaves/vaLeavesApply.controller.js";
import { va_leave_list_function } from "../../controllers/va/vaLeaves/vaLeaveList.controller.js"
import { va_leave_manage_function } from "../../controllers/va/vaLeaves/vaLeaveManage.controller.js";
vaRoutes.get("/", va_callback); // VA Callback for getting active VA vaRoutes.get("/", va_callback); // VA Callback for getting active VA
@ -30,6 +34,7 @@ vaRoutes.get("/logout", authValidator, va_logout_callback); // VA Callback for l
vaRoutes.post("/refreshToken", va_refresh_token_callback); // VA Callback for refresh token vaRoutes.post("/refreshToken", va_refresh_token_callback); // VA Callback for refresh token
vaRoutes.post("/register", va_register); // Register a new VA vaRoutes.post("/register", va_register); // Register a new VA
vaRoutes.post("/forgot-password", va_request_password_reset); vaRoutes.post("/forgot-password", va_request_password_reset);
vaRoutes.post("/verify-va-email", va_verify_email);
vaRoutes.get("/profile", authValidator, va_profile_callback); // Get VA Profile vaRoutes.get("/profile", authValidator, va_profile_callback); // Get VA Profile
vaRoutes.patch("/profile", authValidator, va_profile_update_callback); // Update VA Profile vaRoutes.patch("/profile", authValidator, va_profile_update_callback); // Update VA Profile
@ -41,9 +46,17 @@ vaRoutes.get("/:userName", va_username_callback); // VA Username Callback
// timesheet adjustment routes // timesheet adjustment routes
vaRoutes.get("/timesheet/summary/:projectId/:date", authValidator, va_timesheet_summary_function); // Get Timesheet Adjustment for VA on specific project and date vaRoutes.get("/timesheet/summary/:projectId/:date", authValidator, va_timesheet_summary_function); // Get Timesheet Adjustment for VA on specific project and date
vaRoutes.post("/timesheet/adjustment/request", authValidator, va_timesheet_adjustment_function); // Timesheet Adjustment for VA vaRoutes.post("/timesheet/adjustment/request", authValidator, va_timesheet_adjustment_function); // Timesheet Adjustment for VA
vaRoutes.get("/timesheet/adjustments/list/:status?/:projectId?/:vaId?", get_adjustment_requests_list); // show all adjustment list to approve and reject // vaRoutes.get("/timesheet/adjustments/list/:status?/:projectId?/:vaId?", get_adjustment_requests_list); // show all adjustment list to approve and reject
vaRoutes.patch("/timesheet/adjustments/:adjustmentId", va_timesheet_manage_adjustment_request); vaRoutes.patch("/timesheet/adjustments/:adjustmentId", va_timesheet_manage_adjustment_request);
// Leaves routes
vaRoutes.get("/leaves/all", authValidator, va_leave_list_function); // Get all leaves
vaRoutes.get("/leaves/status/:status", authValidator, va_leave_list_function); // Leaves Filter by status
vaRoutes.get("/leaves/type/:leaveType", authValidator, va_leave_list_function); // Filter by leave type
vaRoutes.get("/leaves/status/:status/type/:leaveType", authValidator, va_leave_list_function); // leaves Filter
vaRoutes.post("/leave/request", authValidator, va_leave_apply_function); // Timesheet Adjustment for VA
vaRoutes.patch("/leave/:leaveId", va_leave_manage_function);
// timesheet adjustment routes // timesheet adjustment routes
// Example: GET /va/search?name=john&email=@example.com&from=2025-01-01&to=2025-01-31 (NC) // Example: GET /va/search?name=john&email=@example.com&from=2025-01-01&to=2025-01-31 (NC)

View File

@ -148,6 +148,16 @@ 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');
} catch (error) {
console.error('Projects error:', error);
res.status(500).render('client/dashboard-projects', { error: 'Failed to load virtual assistants' });
}
});
app.get("/live/:projectId/:vaId", async (req, res) => { app.get("/live/:projectId/:vaId", async (req, res) => {
const { projectId, vaId } = req.params; const { projectId, vaId } = req.params;

View File

@ -19,8 +19,8 @@ class PasswordResetService {
if (!user) { if (!user) {
logger.info(`Password reset requested for non-existent ${userType}: ${email}`); logger.info(`Password reset requested for non-existent ${userType}: ${email}`);
return { return {
success: true, success: false,
message: "If the email exists, a reset link has been sent" message: "Email not recognized. Please sign up to continue."
}; };
} }

View File

@ -29,12 +29,12 @@
<i class="fas fa-users"></i>Virtual Assistants <i class="fas fa-users"></i>Virtual Assistants
</a> </a>
</li> </li>
<!-- <li class="nav-item"> <li class="nav-item">
<a href="/client/invoices" class="nav-button"> <a href="/client/projects" class="nav-button">
<i class="fas fa-file-invoice"></i>Invoices <i class="fas fa-file-invoice"></i>Projects
</a> </a>
</li> </li>
<li class="nav-item"> <!-- <li class="nav-item">
<a href="/client/timesheets" class="nav-button"> <a href="/client/timesheets" class="nav-button">
<i class="fas fa-clock"></i>Timesheets <i class="fas fa-clock"></i>Timesheets
</a> </a>
@ -73,13 +73,13 @@
<span>Virtual Assistants</span> <span>Virtual Assistants</span>
</a> </a>
</li> </li>
<!-- <li class="sidebar-item"> <li class="sidebar-item">
<a href="/client/timesheets" class="sidebar-link"> <a href="/client/projects" class="sidebar-link">
<i class="fas fa-clock sidebar-icon"></i> <i class="fas fa-file-invoice sidebar-icon"></i>
<span>Timesheets</span> <span>Projects</span>
</a> </a>
</li> </li>
<li class="sidebar-item"> <!-- <li class="sidebar-item">
<a href="/client/invoices" class="sidebar-link"> <a href="/client/invoices" class="sidebar-link">
<i class="fas fa-file-invoice sidebar-icon"></i> <i class="fas fa-file-invoice sidebar-icon"></i>
<span>Invoices</span> <span>Invoices</span>
@ -148,6 +148,7 @@
<th>Status</th> <th>Status</th>
<th>Virtual Assistant</th> <th>Virtual Assistant</th>
<th>Projects Assigned</th> <th>Projects Assigned</th>
<th>Active Projects</th>
<th>Worked Time</th> <th>Worked Time</th>
<th>Action</th> <th>Action</th>
</tr> </tr>
@ -239,6 +240,7 @@
function createVATableRow(va) { function createVATableRow(va) {
const statusClass = va.isCurrentlyActive ? 'active' : 'inactive'; const statusClass = va.isCurrentlyActive ? 'active' : 'inactive';
const statusText = va.isCurrentlyActive ? 'Active' : 'Inactive'; const statusText = va.isCurrentlyActive ? 'Active' : 'Inactive';
const activeProject = va?.lastProject?.name ? va.lastProject.name : '-';
const workedTime = formatTime(va.currentSessionDuration); const workedTime = formatTime(va.currentSessionDuration);
const projectsAssigned = va.projectsAssigned || 0; const projectsAssigned = va.projectsAssigned || 0;
const viewLink = va.isCurrentlyActive ? `<a href="/live/${va.lastProjectId}/${va.id}" class="view-link" data-va-id="${va.id}" target="_blank"> const viewLink = va.isCurrentlyActive ? `<a href="/live/${va.lastProjectId}/${va.id}" class="view-link" data-va-id="${va.id}" target="_blank">
@ -260,6 +262,9 @@
<div class="va-email">${va.email || 'N/A'}</div> <div class="va-email">${va.email || 'N/A'}</div>
</div> </div>
</td> </td>
<td>
<span class="projects-count">${activeProject}</span>
</td>
<td> <td>
<span class="projects-count">${projectsAssigned} Projects</span> <span class="projects-count">${projectsAssigned} Projects</span>
</td> </td>
@ -352,6 +357,15 @@
setInterval(loadDashboardData, 30000); setInterval(loadDashboardData, 30000);
document.addEventListener('DOMContentLoaded', () => {
var menuToggle = document.querySelector('.mobile-menu-button');
var sidebar = document.querySelector('.dashboard-sidebar');
menuToggle.addEventListener('click', () => {
sidebar.classList.toggle('active')
})
})
</script> </script>
</body> </body>
</html> </html>

View File

@ -0,0 +1,378 @@
<!DOCTYPE html>
<html lang="en" data-theme="light">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>DocVA Client | Dashboard</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
<link rel="stylesheet" href="/public/css/dashboard.css">
</head>
<body class="dashboard-body">
<div id="root">
<nav class="dashboard-header">
<div class="header-content">
<div class="header-logo">
<div class="logo-container">
<h2 class="sidebar-title">DocVA Client Portal</h2>
</div>
</div>
<div class="header-nav">
<ul class="nav-list">
<li class="nav-item">
<a href="/client/dashboard" class="nav-button">
<i class="fas fa-chart-bar"></i>Dashboard
</a>
</li>
<li class="nav-item">
<a href="/client/virtual-assistants" class="nav-button">
<i class="fas fa-users"></i>Virtual Assistants
</a>
</li>
<li class="nav-item">
<a href="/client/projects" class="nav-button active">
<i class="fas fa-file-invoice"></i>Projects
</a>
</li>
<!-- <li class="nav-item">
<a href="/client/timesheets" class="nav-button">
<i class="fas fa-clock"></i>Timesheets
</a>
</li>
<li class="nav-item">
<a href="/client/referrals" class="nav-button">
<i class="fas fa-share-alt"></i>Referrals
</a>
</li> -->
</ul>
<div class="user-avatar">TT</div>
<button class="mobile-menu-button">
<i class="fas fa-bars"></i>
</button>
</div>
</div>
</nav>
<div class="dashboard-container">
<aside class="dashboard-sidebar">
<!-- <div class="sidebar-header">
<h2 class="sidebar-title">DocVA Client Portal</h2>
</div> -->
<nav class="sidebar-nav">
<ul class="sidebar-list">
<li class="sidebar-item">
<a href="/client/dashboard" class="sidebar-link">
<i class="fas fa-chart-bar sidebar-icon"></i>
<span>Dashboard</span>
</a>
</li>
<li class="sidebar-item">
<a href="/client/virtual-assistants" class="sidebar-link">
<i class="fas fa-users sidebar-icon"></i>
<span>Virtual Assistants</span>
</a>
</li>
<li class="sidebar-item">
<a href="/client/projects" class="sidebar-link active">
<i class="fas fa-file-invoice sidebar-icon"></i>
<span>Projects</span>
</a>
</li>
<!-- <li class="sidebar-item">
<a href="/client/invoices" class="sidebar-link">
<i class="fas fa-file-invoice sidebar-icon"></i>
<span>Invoices</span>
</a>
</li>
<li class="sidebar-item">
<a href="/client/referrals" class="sidebar-link">
<i class="fas fa-share-alt sidebar-icon"></i>
<span>Referrals</span>
</a>
</li> -->
</ul>
</nav>
</aside>
<main class="dashboard-main">
<div class="dashboard-content">
<!-- Projects List -->
<div class="content-section">
<div class="section-header">
<h2 class="section-title">Projects <span class="total-count" id="projectsCount">0 Projects</span></h2>
</div>
<div class="table-container">
<div class="table-wrapper">
<table class="data-table">
<thead>
<tr>
<th>Name</th>
<th>Hourly Rate</th>
<th>Total VA Assigned</th>
<th>Dedicated Hours</th>
<th>Status</th>
</tr>
</thead>
<tbody id="projectsTableBody">
<!-- Data will be loaded here -->
</tbody>
</table>
</div>
<div id="projectsEmptyState" style="display: none;">
<div class="empty-state">
<i class="fas fa-briefcase"></i>
<h3>No Projects</h3>
<p>You don't have any projects yet.</p>
</div>
</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>
</div>
<script>
// Theme management
function setTheme(theme) {
document.documentElement.setAttribute('data-theme', theme);
localStorage.setItem('theme', theme);
}
function getSystemTheme() {
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
}
// Initialize theme
const savedTheme = localStorage.getItem('theme') || getSystemTheme();
setTheme(savedTheme);
// API base URL
const API_BASE = '/api/v1/client';
// Auth token management
function getAuthToken() {
return localStorage.getItem('knoptpq');
}
async function fetchWithAuth(url, options = {}) {
const token = getAuthToken();
if (!token) {
window.location.href = '/client/login';
return;
}
const response = await fetch(url, {
...options,
headers: {
'token': `${token}`,
'Content-Type': 'application/json',
...options.headers,
}
});
if (response.status === 401) {
localStorage.removeItem('knoptpq');
localStorage.removeItem('refreshToken');
window.location.href = '/client/login';
return;
}
return response;
}
// Create Project table row HTML
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 `
<tr>
<td>
<div class="project-info">
<div class="project-name">${project.name || 'Unnamed Project'}</div>
</div>
</td>
<td>
<span class="hourly-rate">${hourlyRate}</span>
</td>
<td>
<span class="va-count">${project.vaAssigned || 0}</span>
</td>
<td>
<span class="dedicated-hours">${dedicatedHours}</span>
</td>
<td>
<div class="status-indicator">
<div class="status-dot ${statusClass}"></div>
<span class="status-text ${statusClass}">${statusText}</span>
</div>
</td>
</tr>
`;
}
// 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';
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() {
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`;
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 || [];
if (projects.length > 0) {
projectsTableBody.innerHTML = projects.map(project => createProjectTableRow(project)).join('');
projectsTableBody.style.display = '';
projectsEmptyState.style.display = 'none';
} 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');
}
} 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 = `
<div class="empty-state">
<i class="fas fa-exclamation-triangle"></i>
<h3>Error</h3>
<p>${message}</p>
</div>
`;
projectsEmptyState.style.display = 'block';
vaEmptyState.innerHTML = `
<div class="empty-state">
<i class="fas fa-exclamation-triangle"></i>
<h3>Error</h3>
<p>${message}</p>
</div>
`;
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');
menuToggle.addEventListener('click', () => {
sidebar.classList.toggle('active')
})
})
</script>
</body>
</html>

View File

@ -29,12 +29,12 @@
<i class="fas fa-users"></i>Virtual Assistants <i class="fas fa-users"></i>Virtual Assistants
</a> </a>
</li> </li>
<!-- <li class="nav-item"> <li class="nav-item">
<a href="/client/invoices" class="nav-button"> <a href="/client/projects" class="nav-button">
<i class="fas fa-file-invoice"></i>Invoices <i class="fas fa-file-invoice"></i>Projects
</a> </a>
</li> </li>
<li class="nav-item"> <!-- <li class="nav-item">
<a href="/client/timesheets" class="nav-button"> <a href="/client/timesheets" class="nav-button">
<i class="fas fa-clock"></i>Timesheets <i class="fas fa-clock"></i>Timesheets
</a> </a>
@ -73,13 +73,13 @@
<span>Virtual Assistants</span> <span>Virtual Assistants</span>
</a> </a>
</li> </li>
<!-- <li class="sidebar-item"> <li class="sidebar-item">
<a href="/client/timesheets" class="sidebar-link"> <a href="/client/projects" class="sidebar-link">
<i class="fas fa-clock sidebar-icon"></i> <i class="fas fa-file-invoice sidebar-icon"></i>
<span>Timesheets</span> <span>Projects</span>
</a> </a>
</li> </li>
<li class="sidebar-item"> <!-- <li class="sidebar-item">
<a href="/client/invoices" class="sidebar-link"> <a href="/client/invoices" class="sidebar-link">
<i class="fas fa-file-invoice sidebar-icon"></i> <i class="fas fa-file-invoice sidebar-icon"></i>
<span>Invoices</span> <span>Invoices</span>
@ -148,6 +148,7 @@
<th>Status</th> <th>Status</th>
<th>Virtual Assistant</th> <th>Virtual Assistant</th>
<th>Projects Assigned</th> <th>Projects Assigned</th>
<th>Active Projects</th>
<th>Worked Time</th> <th>Worked Time</th>
<th>Action</th> <th>Action</th>
</tr> </tr>
@ -239,6 +240,7 @@
function createVATableRow(va) { function createVATableRow(va) {
const statusClass = va.isCurrentlyActive ? 'active' : 'inactive'; const statusClass = va.isCurrentlyActive ? 'active' : 'inactive';
const statusText = va.isCurrentlyActive ? 'Active' : 'Inactive'; const statusText = va.isCurrentlyActive ? 'Active' : 'Inactive';
const activeProject = va?.lastProject?.name ? va.lastProject.name : '-';
const workedTime = formatTime(va.currentSessionDuration); const workedTime = formatTime(va.currentSessionDuration);
const projectsAssigned = va.projectsAssigned || 0; const projectsAssigned = va.projectsAssigned || 0;
const viewLink = va.isCurrentlyActive ? `<a href="/live/${va.lastProjectId}/${va.id}" class="view-link" data-va-id="${va.id}" target="_blank"> const viewLink = va.isCurrentlyActive ? `<a href="/live/${va.lastProjectId}/${va.id}" class="view-link" data-va-id="${va.id}" target="_blank">
@ -260,6 +262,9 @@
<div class="va-email">${va.email || 'N/A'}</div> <div class="va-email">${va.email || 'N/A'}</div>
</div> </div>
</td> </td>
<td>
<span class="projects-count">${activeProject}</span>
</td>
<td> <td>
<span class="projects-count">${projectsAssigned} Projects</span> <span class="projects-count">${projectsAssigned} Projects</span>
</td> </td>
@ -352,6 +357,15 @@
setInterval(loadDashboardData, 30000); setInterval(loadDashboardData, 30000);
document.addEventListener('DOMContentLoaded', () => {
var menuToggle = document.querySelector('.mobile-menu-button');
var sidebar = document.querySelector('.dashboard-sidebar');
menuToggle.addEventListener('click', () => {
sidebar.classList.toggle('active')
})
})
</script> </script>
</body> </body>
</html> </html>

View File

@ -127,6 +127,11 @@
</div> </div>
<script> <script>
function validateEmail(email) {
const emailRegex = /^[a-zA-Z0-9](?:[a-zA-Z0-9]*[._-]?[a-zA-Z0-9]+)*@[a-zA-Z0-9](?:[a-zA-Z0-9]*[-]?[a-zA-Z0-9]+)*\.[a-zA-Z]{2,}$/;
return emailRegex.test(email);
}
(function () { (function () {
const prefersDark = window.matchMedia("(prefers-color-scheme: dark)"); const prefersDark = window.matchMedia("(prefers-color-scheme: dark)");
const logo = document.getElementById("logo"); const logo = document.getElementById("logo");
@ -154,8 +159,8 @@
e.preventDefault(); e.preventDefault();
const email = emailInput.value.trim(); const email = emailInput.value.trim();
if (!email) { if (!email || !validateEmail(email)) {
alert("Please enter your email address."); alert("Please enter valid email address.");
return; return;
} }
@ -174,8 +179,13 @@
const data = await response.json(); const data = await response.json();
if (response.ok) { if (response.ok) {
if(response.success) {
if(confirm('Please Check your mail inbox for password reset link')) { if(confirm('Please Check your mail inbox for password reset link')) {
window.location.href = "/client/login"; // Redirect to reset password page window.location.href = "/client/login"; // Redirect to reset password page
// console.log(response)
}
} else {
alert(data.message || "Failed to send reset link.");
} }
} else { } else {
alert("❌ " + (data.message || "Failed to send reset link.")); alert("❌ " + (data.message || "Failed to send reset link."));

View File

@ -92,12 +92,18 @@
<!-- Step 1: Create Account --> <!-- Step 1: Create Account -->
<div class="form-step active" id="step-1"> <div class="form-step active" id="step-1">
<h2 class="form-title">Create Your Account</h2> <h2 class="form-title">Create Your Account</h2>
<div class="step-error-container" id="step-1-error" style="display: none;">
<div class="error-message" id="step-1-error-message"></div>
</div>
<form id="account-form"> <form id="account-form">
<div class="form-group"> <div class="form-group">
<div class="input-container"> <div class="input-container">
<input type="text" id="company-name" class="input-field" placeholder=" " required> <input type="text" id="company-name" class="input-field" placeholder=" " oninput="validateCompanyName(this)" required>
<label for="company-name" class="input-label">Company / Client Name *</label> <label for="company-name" class="input-label">Company / Client Name *</label>
</div> </div>
<div class="invalid-feedback" id="company-name-error" style="display: none; color: #dc3545; font-size: 12px; margin-top: 5px;">
Company name should only contain letters, numbers, spaces, hyphens, apostrophes, ampersands, and periods
</div>
</div> </div>
<div class="form-group"> <div class="form-group">
<div class="input-container"> <div class="input-container">
@ -107,7 +113,7 @@
</div> </div>
<div class="form-actions"> <div class="form-actions">
<div></div> <!-- Empty div for spacing --> <div></div> <!-- Empty div for spacing -->
<button type="button" class="btn btn-primary" onclick="nextStep(2)">Continue</button> <button type="button" class="btn btn-primary" onclick="handleNextStep(2)">Continue</button>
</div> </div>
</form> </form>
</div> </div>
@ -115,6 +121,9 @@
<!-- Step 2: Business Information --> <!-- Step 2: Business Information -->
<div class="form-step" id="step-2"> <div class="form-step" id="step-2">
<h2 class="form-title">Business Information</h2> <h2 class="form-title">Business Information</h2>
<div class="step-error-container" id="step-2-error" style="display: none;">
<div class="error-message" id="step-2-error-message"></div>
</div>
<form id="business-form"> <form id="business-form">
<div class="form-group"> <div class="form-group">
<div class="input-container"> <div class="input-container">
@ -124,9 +133,12 @@
</div> </div>
<div class="form-group"> <div class="form-group">
<div class="input-container"> <div class="input-container">
<input type="text" id="city" class="input-field" placeholder=" " required> <input type="text" id="city" class="input-field" placeholder=" " oninput="validateCity(this)" onkeypress="return preventInvalidCityChars(event)" required>
<label for="city" class="input-label">City *</label> <label for="city" class="input-label">City *</label>
</div> </div>
<div class="invalid-feedback" id="city-error" style="display: none; color: #dc3545; font-size: 12px; margin-top: 5px;">
City name should contain letters and cannot be only numbers or special characters
</div>
</div> </div>
<div class="form-group"> <div class="form-group">
<div class="input-container"> <div class="input-container">
@ -141,13 +153,16 @@
</div> </div>
<div class="form-group"> <div class="form-group">
<div class="input-container"> <div class="input-container">
<input type="text" id="zip" class="input-field" placeholder=" " required> <input type="text" id="zip" class="input-field" placeholder=" " oninput="validateZipCode(this)" onkeypress="return preventNonNumericZip(event)" maxlength="10" required>
<label for="zip" class="input-label">ZIP *</label> <label for="zip" class="input-label">ZIP *</label>
</div> </div>
<div class="invalid-feedback" id="zip-error" style="display: none; color: #dc3545; font-size: 12px; margin-top: 5px;">
ZIP code should contain only numbers
</div>
</div> </div>
<div class="form-actions"> <div class="form-actions">
<button type="button" class="btn btn-secondary" onclick="prevStep(1)">Back</button> <button type="button" class="btn btn-secondary" onclick="prevStep(1)">Back</button>
<button type="button" class="btn btn-primary" onclick="nextStep(3)">Continue</button> <button type="button" class="btn btn-primary" onclick="handleNextStep(3)">Continue</button>
</div> </div>
</form> </form>
</div> </div>
@ -155,6 +170,9 @@
<!-- Step 3: Billing Address --> <!-- Step 3: Billing Address -->
<div class="form-step" id="step-3"> <div class="form-step" id="step-3">
<h2 class="form-title">Billing Address</h2> <h2 class="form-title">Billing Address</h2>
<div class="step-error-container" id="step-3-error" style="display: none;">
<div class="error-message" id="step-3-error-message"></div>
</div>
<form id="billing-form"> <form id="billing-form">
<div class="checkbox-group"> <div class="checkbox-group">
<input type="checkbox" id="use-business-address"> <input type="checkbox" id="use-business-address">
@ -174,9 +192,12 @@
</div> </div>
<div class="form-group"> <div class="form-group">
<div class="input-container"> <div class="input-container">
<input type="text" id="billing-city" class="input-field" placeholder=" " required> <input type="text" id="billing-city" class="input-field" placeholder=" " oninput="validateCity(this)" onkeypress="return preventInvalidCityChars(event)" required>
<label for="billing-city" class="input-label">City *</label> <label for="billing-city" class="input-label">City *</label>
</div> </div>
<div class="invalid-feedback" id="billing-city-error" style="display: none; color: #dc3545; font-size: 12px; margin-top: 5px;">
City name should contain letters and cannot be only numbers or special characters
</div>
</div> </div>
<div class="form-group"> <div class="form-group">
<div class="input-container"> <div class="input-container">
@ -190,9 +211,12 @@
</div> </div>
<div class="form-group"> <div class="form-group">
<div class="input-container"> <div class="input-container">
<input type="text" id="billing-zip" class="input-field" placeholder=" " required> <input type="text" id="billing-zip" class="input-field" placeholder=" " oninput="validateZipCode(this)" onkeypress="return preventNonNumericZip(event)" maxlength="10" required>
<label for="billing-zip" class="input-label">ZIP *</label> <label for="billing-zip" class="input-label">ZIP *</label>
</div> </div>
<div class="invalid-feedback" id="billing-zip-error" style="display: none; color: #dc3545; font-size: 12px; margin-top: 5px;">
ZIP code should contain only numbers
</div>
</div> </div>
<div class="checkbox-group"> <div class="checkbox-group">
<input type="checkbox" id="use-account-email"> <input type="checkbox" id="use-account-email">
@ -206,7 +230,7 @@
</div> </div>
<div class="form-actions"> <div class="form-actions">
<button type="button" class="btn btn-secondary" onclick="prevStep(2)">Back</button> <button type="button" class="btn btn-secondary" onclick="prevStep(2)">Back</button>
<button type="button" class="btn btn-primary" onclick="nextStep(4)">Continue</button> <button type="button" class="btn btn-primary" onclick="handleNextStep(4)">Continue</button>
</div> </div>
</form> </form>
</div> </div>
@ -214,21 +238,30 @@
<!-- Step 4: Authorized Signatory --> <!-- Step 4: Authorized Signatory -->
<div class="form-step" id="step-4"> <div class="form-step" id="step-4">
<h2 class="form-title">Authorized Signatory</h2> <h2 class="form-title">Authorized Signatory</h2>
<div class="step-error-container" id="step-4-error" style="display: none;">
<div class="error-message" id="step-4-error-message"></div>
</div>
<p style="margin-bottom: 20px; color: #6b7280; font-size: 14px;"> <p style="margin-bottom: 20px; color: #6b7280; font-size: 14px;">
We will use the details below in the agreement that we will send to your prior to entering the project. We will use the details below in the agreement that we will send to your prior to entering the project.
</p> </p>
<form id="signatory-form"> <form id="signatory-form">
<div class="form-group"> <div class="form-group">
<div class="input-container"> <div class="input-container">
<input type="text" id="position" class="input-field" placeholder=" " required> <input type="text" id="position" class="input-field" placeholder=" " oninput="validatePosition(this)" onkeypress="return preventInvalidPositionChars(event)" required>
<label for="position" class="input-label">Position (Owner / CEO / Manager / etc.) *</label> <label for="position" class="input-label">Position (Owner / CEO / Manager / etc.) *</label>
</div> </div>
<div class="invalid-feedback" id="position-error" style="display: none; color: #dc3545; font-size: 12px; margin-top: 5px;">
Position should only contain letters, spaces, and common punctuation
</div>
</div> </div>
<div class="form-group"> <div class="form-group">
<div class="input-container"> <div class="input-container">
<input type="text" id="signatory-name" class="input-field" placeholder=" " required> <input type="text" id="signatory-name" class="input-field" placeholder=" " oninput="validateSignatoryName(this)" onkeypress="return preventNumbers(event)" required>
<label for="signatory-name" class="input-label">Name *</label> <label for="signatory-name" class="input-label">Name *</label>
</div> </div>
<div class="invalid-feedback" id="signatory-name-error" style="display: none; color: #dc3545; font-size: 12px; margin-top: 5px;">
Name should not contain numbers
</div>
</div> </div>
<div class="form-group"> <div class="form-group">
<div class="input-container"> <div class="input-container">
@ -238,7 +271,7 @@
</div> </div>
<div class="form-actions"> <div class="form-actions">
<button type="button" class="btn btn-secondary" onclick="prevStep(3)">Back</button> <button type="button" class="btn btn-secondary" onclick="prevStep(3)">Back</button>
<button type="button" class="btn btn-primary" onclick="nextStep(5)">Continue</button> <button type="button" class="btn btn-primary" onclick="handleNextStep(5)">Continue</button>
</div> </div>
</form> </form>
</div> </div>
@ -246,6 +279,9 @@
<!-- Step 5: Payment Details --> <!-- Step 5: Payment Details -->
<div class="form-step" id="step-5"> <div class="form-step" id="step-5">
<h2 class="form-title">Payment Details</h2> <h2 class="form-title">Payment Details</h2>
<div class="step-error-container" id="step-5-error" style="display: none;">
<div class="error-message" id="step-5-error-message"></div>
</div>
<form id="payment-form"> <form id="payment-form">
<div class="payment-option" onclick="selectPaymentOption('card')"> <div class="payment-option" onclick="selectPaymentOption('card')">
<h4>Credit or Debit Card</h4> <h4>Credit or Debit Card</h4>
@ -267,16 +303,22 @@
</div> </div>
<div class="form-group"> <div class="form-group">
<div class="input-container"> <div class="input-container">
<input type="text" id="security-code" class="input-field" placeholder=" " required> <input type="text" id="security-code" class="input-field" placeholder=" " oninput="validateCVVVV(this)" nkeypress="return preventInvalidCVVChars(event)" maxlength="4" required>
<label for="security-code" class="input-label">Security Code *</label> <label for="security-code" class="input-label">Security Code *</label>
</div> </div>
<div class="invalid-feedback" id="security-code-error" style="display: none; color: #dc3545; font-size: 12px; margin-top: 5px;">
Please enter a valid security code
</div>
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group">
<div class="input-container"> <div class="input-container">
<input type="text" id="card-zip" class="input-field" placeholder=" " required> <input type="text" id="card-zip" class="input-field" placeholder=" " oninput="validateZipCode(this)" onkeypress="return preventNonNumericZip(event)" maxlength="10" required>
<label for="card-zip" class="input-label">ZIP Code *</label> <label for="card-zip" class="input-label">ZIP Code *</label>
</div> </div>
<div class="invalid-feedback" id="card-zip-error" style="display: none; color: #dc3545; font-size: 12px; margin-top: 5px;">
ZIP code should contain only numbers
</div>
</div> </div>
</div> </div>
<div class="payment-option" onclick="selectPaymentOption('bank')"> <div class="payment-option" onclick="selectPaymentOption('bank')">
@ -286,15 +328,21 @@
<div id="bank-details" class="payment-details"> <div id="bank-details" class="payment-details">
<div class="form-group"> <div class="form-group">
<div class="input-container"> <div class="input-container">
<input type="text" id="bank-name" class="input-field" placeholder=" " required> <input type="text" id="bank-name" class="input-field" placeholder=" " oninput="validateBankName(this)" onkeypress="return preventInvalidBankNameChars(event)" required>
<label for="bank-name" class="input-label">Bank Name *</label> <label for="bank-name" class="input-label">Bank Name *</label>
</div> </div>
<div class="invalid-feedback" id="bank-name-error" style="display: none; color: #dc3545; font-size: 12px; margin-top: 5px;">
Bank name should contain letters and cannot be only numbers or special characters
</div>
</div> </div>
<div class="form-group"> <div class="form-group">
<div class="input-container"> <div class="input-container">
<input type="text" id="account-number" class="input-field" placeholder=" " required> <input type="text" id="account-number" class="input-field" placeholder=" " oninput="validateAccountNumber(this)" onkeypress="return preventInvalidAccountNumberChars(event)" required>
<label for="account-number" class="input-label">Account Number *</label> <label for="account-number" class="input-label">Account Number *</label>
</div> </div>
<div class="invalid-feedback" id="account-number-error" style="display: none; color: #dc3545; font-size: 12px; margin-top: 5px;">
Please enter a valid account number
</div>
</div> </div>
<div class="form-group"> <div class="form-group">
<div class="input-container"> <div class="input-container">
@ -314,9 +362,12 @@
</div> </div>
<div class="form-group"> <div class="form-group">
<div class="input-container"> <div class="input-container">
<input type="text" id="account-holder-name" class="input-field" placeholder=" " required> <input type="text" id="account-holder-name" class="input-field" placeholder=" " oninput="validateAccountHolderName(this)" onkeypress="return preventInvalidAccountHolderChars(event)" required>
<label for="account-holder-name" class="input-label">Account Holder Name *</label> <label for="account-holder-name" class="input-label">Account Holder Name *</label>
</div> </div>
<div class="invalid-feedback" id="account-holder-name-error" style="display: none; color: #dc3545; font-size: 12px; margin-top: 5px;">
Account holder name should only contain letters and spaces
</div>
</div> </div>
</div> </div>
<div class="form-actions"> <div class="form-actions">

View File

@ -165,6 +165,9 @@
<!-- Step 1: Account Setup --> <!-- Step 1: Account Setup -->
<div class="form-step active" id="step-1"> <div class="form-step active" id="step-1">
<h2 class="form-title">Account Setup</h2> <h2 class="form-title">Account Setup</h2>
<div class="step-error-container" id="step-1-error" style="display: none;">
<div class="error-message" id="step-1-error-message"></div>
</div>
<form id="account-form"> <form id="account-form">
<div class="form-group"> <div class="form-group">
<div class="input-container"> <div class="input-container">
@ -175,7 +178,7 @@
<div class="form-group"> <div class="form-group">
<div class="input-container"> <div class="input-container">
<input type="password" id="password" class="input-field" placeholder=" " required> <input type="password" id="password" class="input-field" placeholder=" " required autocomplete="off">
<label for="password" class="input-label">Create Password *</label> <label for="password" class="input-label">Create Password *</label>
</div> </div>
<div class="password-actions"> <div class="password-actions">
@ -186,7 +189,7 @@
<div class="form-group"> <div class="form-group">
<div class="input-container"> <div class="input-container">
<input type="password" id="confirmPassword" class="input-field" placeholder=" " required> <input type="password" id="confirmPassword" class="input-field" placeholder=" " required autocomplete="off">
<label for="confirmPassword" class="input-label">Confirm Password *</label> <label for="confirmPassword" class="input-label">Confirm Password *</label>
</div> </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> <div class="invalid-feedback" id="password-match-error" style="display: none; color: #dc3545; font-size: 12px; margin-top: 5px;">Passwords do not match</div>
@ -194,7 +197,7 @@
<div class="form-actions"> <div class="form-actions">
<div></div> <div></div>
<button type="button" class="btn btn-primary" onclick="nextStep(2)">Continue</button> <button type="button" class="btn btn-primary" onclick="handleNextStep(2)">Continue</button>
</div> </div>
</form> </form>
</div> </div>
@ -202,7 +205,9 @@
<!-- Step 2: Personal Information --> <!-- Step 2: Personal Information -->
<div class="form-step" id="step-2"> <div class="form-step" id="step-2">
<h2 class="form-title">Personal Information</h2> <h2 class="form-title">Personal Information</h2>
<div class="step-error-container" id="step-2-error" style="display: none;">
<div class="error-message" id="step-2-error-message"></div>
</div>
<div class="profile-incomplete" id="profile-incomplete" style="display: none;"> <div class="profile-incomplete" id="profile-incomplete" style="display: none;">
Profile is not yet complete, first name is missing Profile is not yet complete, first name is missing
</div> </div>
@ -232,7 +237,7 @@
</div> </div>
<div class="form-group"> <div class="form-group">
<div class="input-container"> <div class="input-container">
<input type="date" id="dob" class="input-field" placeholder=" "> <input type="date" id="dob" class="input-field dob" placeholder=" " onchange="handleDOBChange()">
<label for="dob" class="input-label">Date of Birth</label> <label for="dob" class="input-label">Date of Birth</label>
</div> </div>
</div> </div>
@ -274,7 +279,7 @@
<div class="form-actions"> <div class="form-actions">
<button type="button" class="btn btn-secondary" onclick="prevStep(1)">Back</button> <button type="button" class="btn btn-secondary" onclick="prevStep(1)">Back</button>
<button type="button" class="btn btn-primary" onclick="nextStep(3)">Continue</button> <button type="button" class="btn btn-primary" onclick="handleNextStep(3)">Continue</button>
</div> </div>
</form> </form>
</div> </div>
@ -282,13 +287,19 @@
<!-- Step 3: Contact Information --> <!-- Step 3: Contact Information -->
<div class="form-step" id="step-3"> <div class="form-step" id="step-3">
<h2 class="form-title">Contact Information</h2> <h2 class="form-title">Contact Information</h2>
<div class="step-error-container" id="step-3-error" style="display: none;">
<div class="error-message" id="step-3-error-message"></div>
</div>
<form id="contact-form"> <form id="contact-form">
<div class="form-group"> <div class="form-group">
<label for="phone" class="input-label">Phone Number *</label> <label for="phone" class="input-label">Phone Number *</label>
<div class="input-container"> <div class="input-container">
<input type="tel" id="phone" class="input-field" placeholder=" " required> <input type="tel" id="phone" class="input-field" placeholder=" " pattern="[0-9]*" minlength="10" maxlength="15" oninput="validatePhoneNumber(this)" required>
<label for="phone" class="input-label">+ Phone Number</label> <label for="phone" class="input-label">+ Phone Number</label>
</div> </div>
<div class="invalid-feedback" id="phone-error" style="display: none; color: #dc3545; font-size: 12px; margin-top: 5px;">
Please enter a valid phone number (10-15 digits)
</div>
</div> </div>
<div class="form-group"> <div class="form-group">
@ -300,7 +311,7 @@
<div class="form-actions"> <div class="form-actions">
<button type="button" class="btn btn-secondary" onclick="prevStep(2)">Back</button> <button type="button" class="btn btn-secondary" onclick="prevStep(2)">Back</button>
<button type="button" class="btn btn-primary" onclick="nextStep(4)">Continue</button> <button type="button" class="btn btn-primary" onclick="handleNextStep(4)">Continue</button>
</div> </div>
</form> </form>
</div> </div>
@ -308,6 +319,9 @@
<!-- Step 4: Payment Details --> <!-- Step 4: Payment Details -->
<div class="form-step" id="step-4"> <div class="form-step" id="step-4">
<h2 class="form-title">Payment Details</h2> <h2 class="form-title">Payment Details</h2>
<div class="step-error-container" id="step-4-error" style="display: none;">
<div class="error-message" id="step-4-error-message"></div>
</div>
<form id="payment-form"> <form id="payment-form">
<div class="form-group"> <div class="form-group">
<div class="payment-option" onclick="selectPaymentMethod('payoneer')"> <div class="payment-option" onclick="selectPaymentMethod('payoneer')">
@ -317,9 +331,12 @@
<div id="payoneer-details" class="payment-details"> <div id="payoneer-details" class="payment-details">
<div class="form-group"> <div class="form-group">
<div class="input-container"> <div class="input-container">
<input type="text" id="payoneerAccount" class="input-field" placeholder=" " required> <input type="text" id="payoneerAccount" class="input-field" placeholder=" " minlength="5" maxlength="20" oninput="validatePayoneerAccount(this)" required>
<label for="payoneerAccount" class="input-label">Payoneer Account Number *</label> <label for="payoneerAccount" class="input-label">Payoneer Account Number *</label>
</div> </div>
<div class="invalid-feedback" id="payoneer-error" style="display: none; color: #dc3545; font-size: 12px; margin-top: 5px;">
Account number must be 5-20 characters
</div>
</div> </div>
</div> </div>
@ -330,9 +347,12 @@
<div id="veem-details" class="payment-details"> <div id="veem-details" class="payment-details">
<div class="form-group"> <div class="form-group">
<div class="input-container"> <div class="input-container">
<input type="text" id="veemAccount" class="input-field" placeholder=" " required> <input type="text" id="veemAccount" class="input-field" placeholder=" " minlength="8" maxlength="25" oninput="validateVeemAccount(this)" required>
<label for="veemAccount" class="input-label">Veem Account Number *</label> <label for="veemAccount" class="input-label">Veem Account Number *</label>
</div> </div>
<div class="invalid-feedback" id="veem-error" style="display: none; color: #dc3545; font-size: 12px; margin-top: 5px;">
Account number must be 8-25 characters
</div>
</div> </div>
</div> </div>
</div> </div>