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;
gap: 1rem;
width: 100%;
/* max-width: 1024px; */
flex-direction: row;
align-items: center;
justify-content: space-between;
@ -188,7 +187,10 @@ body {
width: 16rem;
background-color: var(--bg-card);
border-right: 1px solid var(--border-color);
display: none;
/* display: none; */
}
.dashboard-sidebar.active {
display: block;
}
@media (min-width: 768px) {
@ -196,6 +198,20 @@ body {
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 {
padding: 1.5rem;

View File

@ -476,3 +476,42 @@ body {
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
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() {
if (this.value) {
this.nextElementSibling.style.top = '4px';
@ -139,6 +154,13 @@
// Handle select field label animation
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() {
if (this.value) {
this.nextElementSibling.style.top = '4px';
@ -228,10 +250,14 @@
return maxStep;
}
async function handleNextStep(step) {
await nextStep(step);
}
// Function to navigate to next step
function nextStep(step) {
async function nextStep(step) {
// Validate current step before proceeding
if (!validateStep(currentStep)) {
if (!(await validateStep(currentStep))) {
return;
}
@ -306,10 +332,14 @@
}
// Function to validate current step
function validateStep(step) {
async function validateStep(step) {
let isValid = true;
const form = document.getElementById(getFormId(step));
// Hide any previous errors for this step
hideError(step);
clearFieldErrors();
if (form) {
// For account setup step
if (step === 1) {
@ -319,21 +349,31 @@
// Email validation
if (!validateEmail(email)) {
alert('Please enter a valid email address');
showError('Please enter a valid email address', 'email', step);
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
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;
}
if (password !== confirmPassword) {
document.getElementById('password-match-error').style.display = 'block';
document.getElementById('confirmPassword').classList.add('field-error');
showError('Passwords do not match', 'confirmPassword', step);
isValid = false;
} else {
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;
if (!firstName) {
document.getElementById('profile-incomplete').style.display = 'block';
document.getElementById('firstName').classList.add('field-error');
showError('First name is required', 'firstName', step);
isValid = false;
} else {
document.getElementById('profile-incomplete').style.display = 'none';
document.getElementById('firstName').classList.remove('field-error');
}
// Age validation
const age = document.getElementById('age').value;
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;
}
@ -359,12 +402,12 @@
const state = document.getElementById('state').value;
if (!country) {
alert('Please select your country');
showError('Please select your country', 'country', step);
isValid = false;
}
if (!state) {
alert('Please select your state/region');
showError('Please select your state/region', 'state', step);
isValid = false;
}
@ -373,9 +416,40 @@
requiredFields.forEach(field => {
if (!field.value.trim()) {
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 {
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) {
const selectedPayment = document.querySelector('.payment-option.selected');
if (!selectedPayment) {
alert('Please select a payment method');
showError('Please select a payment method', null, step);
isValid = false;
} else {
const paymentMethod = selectedPayment.getAttribute('onclick').match(/'([^']+)'/)[1];
if (paymentMethod === 'payoneer') {
const payoneerAccount = document.getElementById('payoneerAccount').value;
if (!payoneerAccount) {
alert('Please enter your Payoneer account number');
showError('Please enter your Payoneer account number', 'payoneerAccount', step);
isValid = false;
}
} else if (paymentMethod === 'veem') {
const veemAccount = document.getElementById('veemAccount').value;
if (!veemAccount) {
alert('Please enter your Veem account number');
showError('Please enter your Veem account number', 'veemAccount', step);
isValid = false;
}
}
@ -410,9 +484,9 @@
requiredFields.forEach(field => {
if (!field.value.trim()) {
isValid = false;
field.style.border = '1px solid #ef4444';
field.classList.add('field-error');
} else {
field.style.border = '';
field.classList.remove('field-error');
}
});
}
@ -598,12 +672,12 @@
}, 1000);
} else {
// 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) {
// Handle network errors
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 {
// Restore button state
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,11 +42,54 @@ async function closePreviousSession(vaId, lastLog) {
});
// 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 {
closedWorkTracks: openWorkTracks.length,
logoutLogId: logoutLog._id
};
closedWorkTracks: openWorkTracks.length,
logoutLogId: logoutLog._id,
};
} catch (error) {
console.error("Error closing previous session:", 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
});
return {
shouldLogout: true,
totalTodayHours: totalTodayHours,
dedicatedHours: project.dedicatedHours,
logoutLogId: 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": 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 {
shouldLogout: true,
totalTodayHours: totalTodayHours,
dedicatedHours: project.dedicatedHours,
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 {

View File

@ -66,11 +66,49 @@ async function closePreviousSession(vaId, lastLog) {
});
// 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,
}
return {
closedWorkTracks: openWorkTracks.length,
logoutLogId: logoutLog._id
};
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;
}
} catch (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 })
// console.log(createdLog)
try {
const salesforceUrl = `${process.env.SF_DOCTRACKER_API_URL}services/apexrest/docva/api/va-project-log`;
const sfData = {
"_id": createdLog._id,
"loggedType": type,
"vaId": vaId,
"vaEmail": vaEmail,
"projectId": projectId,
"time": createdLog.time,
}
// try {
// const salesforceUrl = `${process.env.SF_DOCTRACKER_API_URL}services/apexrest/docva/api/va-project-log`;
// const sfData = {
// "_id": createdLog._id,
// "loggedType": type,
// "vaId": vaId,
// "vaEmail": vaEmail,
// "projectId": projectId,
// "time": createdLog.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(sfData)
});
// 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(sfData)
// });
if (!response.ok) {
const errorText = await response.text();
throw new Error(`Salesforce API responded with status ${response.status}: ${errorText}`);
}
// 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);
// const result = await response.json();
// // console.log("✅ Successfully synced VA data to Salesforce:", result);
res.status(201).json({ success: true, message: `${type} Successfully`, logId: createdLog._id, SyncSalesforce: result });
} catch (error) {
console.error("Failed to sync with Salesforce:", error.message);
console.log(error)
// res.status(201).json({ success: true, message: `${type} Successfully`, logId: createdLog._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;
}
// // 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).send({ success: true, message: `${type} Successfully`, logId: createdLog._id });
return res.status(201).send({ success: true, message: `${type} Successfully`, logId: createdLog._id });
} catch (error) {
return res.status(500).send("Internal Server Error");

View File

@ -140,19 +140,80 @@ async function va_timesheet_adjustment_function(req, res) {
return `${h}h ${m}m ${s}s`;
};
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"
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: "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) {
console.error("Timesheet adjustment error:", error);

View File

@ -46,17 +46,39 @@ async function updateEntityRelationships(data) {
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 = {};
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,
@ -215,14 +237,37 @@ async function removeEntityRelationships(data) {
// Remove projectId from VAs
if (vaId && Array.isArray(vaId)) {
await VAModel.updateMany(
{ _id: { $in: vaId } },
{ $pull: {
projectIds: projectId,
// 🐞🐞🐞 Here before removing we need to check if VA is still assigned to other projects of the same client
clientIds: clientId
}}
);
// await VAModel.updateMany(
// { _id: { $in: vaId } },
// { $pull: {
// projectIds: projectId,
// // 🐞🐞🐞 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
};
}
await VAModel.findByIdAndUpdate(va, updateOperation);
}
}
results.projectRemoval = 'Project relationships removed successfully';

View File

@ -424,6 +424,51 @@ const adjustedHoursSchema = new Schema({
}]
}, { 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 ---------------------- */
const VAModel = model("VA", vaSchema);
const ClientModel = model("Client", clientSchema);
@ -432,5 +477,6 @@ const LoggedModel = model("TrackLog", loggedSchema);
const WorkTrackModel = model("TrackWork", vaWorkTrackSchema);
const PasswordResetModel = model("PasswordReset", passwordResetSchema);
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 { clientAuthValidator } from "../../middlewares/auth.middleware.js";
import { client_dashboard } from "./../../controllers/client/dashboard/clientDashboard.controller.js";
import { client_dashboard_va_log } from "../../controllers/client/dashboard/clientDashboardVaLog.controller.js"
import { client_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
clientRoutes.post("/register", va_register_client); // Register a new VA
clientRoutes.post("/login", va_login_client); // Login Client
clientRoutes.get("/refreshToken", client_refresh_token_callback); // Client Refresh Token
clientRoutes.post("/forgot-password", client_request_password_reset);
clientRoutes.post("/verify-client-email", client_verify_email);
// clientRoutes.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)
@ -24,7 +27,8 @@ clientRoutes.get("/:clientId/projects/:projectId", va_clients_projects_callback)
// dashboard API's
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 { 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_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
@ -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("/register", va_register); // Register a new VA
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.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
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.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);
// 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
// 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) => {
const { projectId, vaId } = req.params;

View File

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

View File

@ -29,12 +29,12 @@
<i class="fas fa-users"></i>Virtual Assistants
</a>
</li>
<!-- <li class="nav-item">
<a href="/client/invoices" class="nav-button">
<i class="fas fa-file-invoice"></i>Invoices
<li class="nav-item">
<a href="/client/projects" class="nav-button">
<i class="fas fa-file-invoice"></i>Projects
</a>
</li>
<li class="nav-item">
<!-- <li class="nav-item">
<a href="/client/timesheets" class="nav-button">
<i class="fas fa-clock"></i>Timesheets
</a>
@ -73,13 +73,13 @@
<span>Virtual Assistants</span>
</a>
</li>
<!-- <li class="sidebar-item">
<a href="/client/timesheets" class="sidebar-link">
<i class="fas fa-clock sidebar-icon"></i>
<span>Timesheets</span>
<li class="sidebar-item">
<a href="/client/projects" class="sidebar-link">
<i class="fas fa-file-invoice sidebar-icon"></i>
<span>Projects</span>
</a>
</li>
<li class="sidebar-item">
<!-- <li class="sidebar-item">
<a href="/client/invoices" class="sidebar-link">
<i class="fas fa-file-invoice sidebar-icon"></i>
<span>Invoices</span>
@ -148,6 +148,7 @@
<th>Status</th>
<th>Virtual Assistant</th>
<th>Projects Assigned</th>
<th>Active Projects</th>
<th>Worked Time</th>
<th>Action</th>
</tr>
@ -239,6 +240,7 @@
function createVATableRow(va) {
const statusClass = va.isCurrentlyActive ? 'active' : 'inactive';
const statusText = va.isCurrentlyActive ? 'Active' : 'Inactive';
const activeProject = va?.lastProject?.name ? va.lastProject.name : '-';
const workedTime = formatTime(va.currentSessionDuration);
const projectsAssigned = va.projectsAssigned || 0;
const viewLink = va.isCurrentlyActive ? `<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>
</td>
<td>
<span class="projects-count">${activeProject}</span>
</td>
<td>
<span class="projects-count">${projectsAssigned} Projects</span>
</td>
@ -352,6 +357,15 @@
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

@ -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
</a>
</li>
<!-- <li class="nav-item">
<a href="/client/invoices" class="nav-button">
<i class="fas fa-file-invoice"></i>Invoices
<li class="nav-item">
<a href="/client/projects" class="nav-button">
<i class="fas fa-file-invoice"></i>Projects
</a>
</li>
<li class="nav-item">
<!-- <li class="nav-item">
<a href="/client/timesheets" class="nav-button">
<i class="fas fa-clock"></i>Timesheets
</a>
@ -73,13 +73,13 @@
<span>Virtual Assistants</span>
</a>
</li>
<!-- <li class="sidebar-item">
<a href="/client/timesheets" class="sidebar-link">
<i class="fas fa-clock sidebar-icon"></i>
<span>Timesheets</span>
<li class="sidebar-item">
<a href="/client/projects" class="sidebar-link">
<i class="fas fa-file-invoice sidebar-icon"></i>
<span>Projects</span>
</a>
</li>
<li class="sidebar-item">
<!-- <li class="sidebar-item">
<a href="/client/invoices" class="sidebar-link">
<i class="fas fa-file-invoice sidebar-icon"></i>
<span>Invoices</span>
@ -148,6 +148,7 @@
<th>Status</th>
<th>Virtual Assistant</th>
<th>Projects Assigned</th>
<th>Active Projects</th>
<th>Worked Time</th>
<th>Action</th>
</tr>
@ -239,6 +240,7 @@
function createVATableRow(va) {
const statusClass = va.isCurrentlyActive ? 'active' : 'inactive';
const statusText = va.isCurrentlyActive ? 'Active' : 'Inactive';
const activeProject = va?.lastProject?.name ? va.lastProject.name : '-';
const workedTime = formatTime(va.currentSessionDuration);
const projectsAssigned = va.projectsAssigned || 0;
const viewLink = va.isCurrentlyActive ? `<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>
</td>
<td>
<span class="projects-count">${activeProject}</span>
</td>
<td>
<span class="projects-count">${projectsAssigned} Projects</span>
</td>
@ -352,6 +357,15 @@
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

@ -127,6 +127,11 @@
</div>
<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 () {
const prefersDark = window.matchMedia("(prefers-color-scheme: dark)");
const logo = document.getElementById("logo");
@ -154,8 +159,8 @@
e.preventDefault();
const email = emailInput.value.trim();
if (!email) {
alert("Please enter your email address.");
if (!email || !validateEmail(email)) {
alert("Please enter valid email address.");
return;
}
@ -174,8 +179,13 @@
const data = await response.json();
if (response.ok) {
if(confirm('Please Check your mail inbox for password reset link')) {
window.location.href = "/client/login"; // Redirect to reset password page
if(response.success) {
if(confirm('Please Check your mail inbox for password reset link')) {
window.location.href = "/client/login"; // Redirect to reset password page
// console.log(response)
}
} else {
alert(data.message || "Failed to send reset link.");
}
} else {
alert("❌ " + (data.message || "Failed to send reset link."));

View File

@ -92,12 +92,18 @@
<!-- Step 1: Create Account -->
<div class="form-step active" id="step-1">
<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">
<div class="form-group">
<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>
</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 class="form-group">
<div class="input-container">
@ -107,7 +113,7 @@
</div>
<div class="form-actions">
<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>
</form>
</div>
@ -115,6 +121,9 @@
<!-- Step 2: Business Information -->
<div class="form-step" id="step-2">
<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">
<div class="form-group">
<div class="input-container">
@ -124,9 +133,12 @@
</div>
<div class="form-group">
<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>
</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 class="form-group">
<div class="input-container">
@ -141,13 +153,16 @@
</div>
<div class="form-group">
<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>
</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 class="form-actions">
<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>
</form>
</div>
@ -155,6 +170,9 @@
<!-- Step 3: Billing Address -->
<div class="form-step" id="step-3">
<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">
<div class="checkbox-group">
<input type="checkbox" id="use-business-address">
@ -174,9 +192,12 @@
</div>
<div class="form-group">
<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>
</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 class="form-group">
<div class="input-container">
@ -190,9 +211,12 @@
</div>
<div class="form-group">
<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>
</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 class="checkbox-group">
<input type="checkbox" id="use-account-email">
@ -206,7 +230,7 @@
</div>
<div class="form-actions">
<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>
</form>
</div>
@ -214,21 +238,30 @@
<!-- Step 4: Authorized Signatory -->
<div class="form-step" id="step-4">
<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;">
We will use the details below in the agreement that we will send to your prior to entering the project.
</p>
<form id="signatory-form">
<div class="form-group">
<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>
</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 class="form-group">
<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>
</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 class="form-group">
<div class="input-container">
@ -238,7 +271,7 @@
</div>
<div class="form-actions">
<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>
</form>
</div>
@ -246,6 +279,9 @@
<!-- Step 5: Payment Details -->
<div class="form-step" id="step-5">
<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">
<div class="payment-option" onclick="selectPaymentOption('card')">
<h4>Credit or Debit Card</h4>
@ -267,16 +303,22 @@
</div>
<div class="form-group">
<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>
</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 class="form-group">
<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>
</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 class="payment-option" onclick="selectPaymentOption('bank')">
@ -286,15 +328,21 @@
<div id="bank-details" class="payment-details">
<div class="form-group">
<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>
</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 class="form-group">
<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>
</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 class="form-group">
<div class="input-container">
@ -314,9 +362,12 @@
</div>
<div class="form-group">
<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>
</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 class="form-actions">

View File

@ -165,6 +165,9 @@
<!-- Step 1: Account Setup -->
<div class="form-step active" id="step-1">
<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">
<div class="form-group">
<div class="input-container">
@ -175,7 +178,7 @@
<div class="form-group">
<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>
</div>
<div class="password-actions">
@ -186,7 +189,7 @@
<div class="form-group">
<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>
</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></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>
</form>
</div>
@ -202,7 +205,9 @@
<!-- Step 2: Personal Information -->
<div class="form-step" id="step-2">
<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;">
Profile is not yet complete, first name is missing
</div>
@ -232,7 +237,7 @@
</div>
<div class="form-group">
<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>
</div>
</div>
@ -274,7 +279,7 @@
<div class="form-actions">
<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>
</form>
</div>
@ -282,13 +287,19 @@
<!-- Step 3: Contact Information -->
<div class="form-step" id="step-3">
<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">
<div class="form-group">
<label for="phone" class="input-label">Phone Number *</label>
<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>
</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 class="form-group">
@ -300,7 +311,7 @@
<div class="form-actions">
<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>
</form>
</div>
@ -308,6 +319,9 @@
<!-- Step 4: Payment Details -->
<div class="form-step" id="step-4">
<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">
<div class="form-group">
<div class="payment-option" onclick="selectPaymentMethod('payoneer')">
@ -317,9 +331,12 @@
<div id="payoneer-details" class="payment-details">
<div class="form-group">
<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>
</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>
@ -330,9 +347,12 @@
<div id="veem-details" class="payment-details">
<div class="form-group">
<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>
</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>