diff --git a/public/css/dashboard.css b/public/css/dashboard.css
index f1bf39d..7ffb796 100644
--- a/public/css/dashboard.css
+++ b/public/css/dashboard.css
@@ -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;
@@ -945,4 +961,4 @@ tr[data-vaid]:hover {
.detail-item span {
text-align: left;
}
-}
\ No newline at end of file
+}
diff --git a/public/css/va-register.css b/public/css/va-register.css
index 659500e..3acf759 100644
--- a/public/css/va-register.css
+++ b/public/css/va-register.css
@@ -475,4 +475,43 @@ body {
font-size: 40px;
margin-bottom: 40px;
}
- }
\ No newline at end of file
+ }
+
+ .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);
+ }
+}
\ No newline at end of file
diff --git a/public/js/va-client-register.js b/public/js/va-client-register.js
index c44cba4..3fa242e 100644
--- a/public/js/va-client-register.js
+++ b/public/js/va-client-register.js
@@ -1,6 +1,6 @@
// Email validation function
function validateEmail(email) {
- const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
+ 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);
}
@@ -35,12 +35,27 @@ function validateCardNumber(cardNumber) {
return false;
}
+ // Check for invalid patterns
+ if (isInvalidCardPattern(cleanNumber)) {
+ return false;
+ }
+
+ // Check for invalid BIN ranges
+ if (hasInvalidBIN(cleanNumber)) {
+ return false;
+ }
+
// Luhn algorithm
+ return isValidLuhn(cleanNumber);
+}
+
+// Enhanced Luhn algorithm
+function isValidLuhn(cardNumber) {
let sum = 0;
let isEven = false;
- for (let i = cleanNumber.length - 1; i >= 0; i--) {
- let digit = parseInt(cleanNumber.charAt(i));
+ for (let i = cardNumber.length - 1; i >= 0; i--) {
+ let digit = parseInt(cardNumber.charAt(i));
if (isEven) {
digit *= 2;
@@ -56,6 +71,66 @@ function validateCardNumber(cardNumber) {
return sum % 10 === 0;
}
+// Function to check for invalid card patterns
+function isInvalidCardPattern(cardNumber) {
+ const invalidPatterns = [
+ /^0+$/, // All zeros
+ /^1+$/, // All ones
+ /^2+$/, // All twos
+ /^3+$/, // All threes
+ /^4+$/, // All fours
+ /^5+$/, // All fives
+ /^6+$/, // All sixes
+ /^7+$/, // All sevens
+ /^8+$/, // All eights
+ /^9+$/, // All nines
+ /^0123/, // Sequential
+ /^1234/, // Sequential
+ ];
+
+ return invalidPatterns.some(pattern => pattern.test(cardNumber));
+}
+
+// Function to check for invalid BIN ranges
+function hasInvalidBIN(cardNumber) {
+ const firstSix = cardNumber.substring(0, 6);
+ const firstFour = cardNumber.substring(0, 4);
+ const firstTwo = cardNumber.substring(0, 2);
+
+ // Invalid BIN ranges
+ const invalidBINs = [
+ '000000', '000001', '000002', '000003', // Test cards should be handled separately
+ '111111', '222222', '333333', '444444',
+ '555555', '666666', '777777', '888888',
+ '999999', '123456', '654321'
+ ];
+
+ // Check specific invalid ranges
+ if (invalidBINs.includes(firstSix)) {
+ return true;
+ }
+
+ // Check for known invalid starting digits
+ if (['0', '3', '6', '7', '8'].includes(firstTwo)) {
+ // 0 - Reserved
+ // 3 - Should only be 34, 37 (AMEX)
+ // 6 - Should follow Discover patterns
+ // 7, 8 - Not currently assigned to major networks
+ if (!isValidCardPrefix(cardNumber)) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+// Validate card prefix
+function isValidCardPrefix(cardNumber) {
+ const type = detectCardType(cardNumber);
+ return type !== 'unknown';
+}
+
+
// Expiry date validation
function validateExpiryDate(expiryDate) {
const expiryRegex = /^(0[1-9]|1[0-2])\/([0-9]{2})$/;
@@ -108,11 +183,41 @@ function validateRoutingNumber(routingNumber) {
return false;
}
- // Simple checksum validation
- const digits = cleanRouting.split('').map(Number);
+ // Check for invalid patterns
+ if (isInvalidRoutingNumberPattern(cleanRouting)) {
+ return false;
+ }
+
+ // ABA routing number checksum validation
+ return isValidABAChecksum(cleanRouting);
+}
+
+function isInvalidRoutingNumberPattern(routingNumber) {
+ const invalidPatterns = [
+ /^0+$/, // All zeros
+ /^1+$/, // All ones
+ /^2+$/, // All twos
+ /^3+$/, // All threes
+ /^4+$/, // All fours
+ /^5+$/, // All fives
+ /^6+$/, // All sixes
+ /^7+$/, // All sevens
+ /^8+$/, // All eights
+ /^9+$/, // All nines
+ /^012345678/, // Sequential
+ /^876543210/, // Reverse sequential
+ ];
+
+ return invalidPatterns.some(pattern => pattern.test(routingNumber));
+}
+
+function isValidABAChecksum(routingNumber) {
+ const digits = routingNumber.split('').map(Number);
+
+ // ABA routing number checksum formula
const sum = (3 * (digits[0] + digits[3] + digits[6])) +
(7 * (digits[1] + digits[4] + digits[7])) +
- (digits[2] + digits[5] + digits[8]);
+ (1 * (digits[2] + digits[5] + digits[8]));
return sum % 10 === 0;
}
@@ -235,10 +340,14 @@ function getMaxCompletedStep() {
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;
}
@@ -288,7 +397,7 @@ function prevStep(step) {
}
// Function to validate current step
-function validateStep(step) {
+async function validateStep(step) {
let isValid = true;
const form = document.getElementById(getFormId(step));
@@ -297,7 +406,7 @@ function validateStep(step) {
const selectedPayment = document.querySelector('.payment-option.selected');
if (!selectedPayment) {
isValid = false;
- alert('Please select a payment method');
+ showError('Please select a payment method', null, step);
return isValid;
}
@@ -311,10 +420,9 @@ function validateStep(step) {
const cardNumber = document.getElementById('card-number').value;
if (!validateCardNumber(cardNumber)) {
isValid = false;
- document.getElementById('card-number').style.border = '1px solid #ef4444';
- alert('Please enter a valid card number');
+ showError('Please enter a valid card number', 'card-number', step);
+ return isValid;
} else {
- document.getElementById('card-number').style.border = '';
// Show card type
const cardType = detectCardType(cardNumber);
if (cardType !== 'unknown') {
@@ -327,10 +435,8 @@ function validateStep(step) {
const expiryDate = document.getElementById('expiry-date').value;
if (!validateExpiryDate(expiryDate)) {
isValid = false;
- document.getElementById('expiry-date').style.border = '1px solid #ef4444';
- alert('Please enter a valid expiry date (MM/YY)');
- } else {
- document.getElementById('expiry-date').style.border = '';
+ showError('Please enter a valid expiry date (MM/YY)', 'expiry-date', step);
+ return isValid;
}
// CVV validation
@@ -338,20 +444,16 @@ function validateStep(step) {
const cardType = detectCardType(cardNumber);
if (!validateCVV(cvv, cardType)) {
isValid = false;
- document.getElementById('security-code').style.border = '1px solid #ef4444';
- alert(`Please enter a valid ${cardType === 'amex' ? '4-digit' : '3-digit'} CVV`);
- } else {
- document.getElementById('security-code').style.border = '';
+ showError(`Please enter a valid ${cardType === 'amex' ? '4-digit' : '3-digit'} CVV`, 'security-code', step);
+ return isValid;
}
// ZIP code validation
const zipCode = document.getElementById('card-zip').value;
if (!validateZipCode(zipCode)) {
isValid = false;
- document.getElementById('card-zip').style.border = '1px solid #ef4444';
- alert('Please enter a valid ZIP code');
- } else {
- document.getElementById('card-zip').style.border = '';
+ showError('Please enter a valid ZIP code', 'card-zip', step);
+ return isValid;
}
}
@@ -361,41 +463,63 @@ function validateStep(step) {
const accountNumber = document.getElementById('account-number').value;
if (!validateBankAccount(accountNumber)) {
isValid = false;
- document.getElementById('account-number').style.border = '1px solid #ef4444';
- alert('Please enter a valid account number (8-17 digits)');
- } else {
- document.getElementById('account-number').style.border = '';
+ showError('Please enter a valid account number (8-17 digits)', 'account-number', step);
+ return isValid;
}
// Routing number validation
const routingNumber = document.getElementById('routing-number').value;
if (!validateRoutingNumber(routingNumber)) {
isValid = false;
- document.getElementById('routing-number').style.border = '1px solid #ef4444';
- alert('Please enter a valid routing number');
- } else {
- document.getElementById('routing-number').style.border = '';
+ showError('Please enter a valid routing numbers', 'routing-number', step);
+ return isValid;
}
// Basic validation for other bank fields
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');
}
});
}
}
} else if (step === 1) {
const email = document.getElementById('email').value;
+ const companyName = document.getElementById('company-name').value;
+ if (!validateEmail(email) && !companyName) {
+ isValid = false;
+ showError('Both Fields are Required', null, step);
+ let requiredFields1 = form.querySelectorAll('[required]');
+ requiredFields1.forEach(field => {
+ if (!field.value.trim()) {
+ isValid = false;
+ field.classList.add('field-error');
+ } else if (field.id !== 'email') {
+ field.classList.remove('field-error');
+ }
+ });
+ return isValid;
+ }
if (!validateEmail(email)) {
isValid = false;
- document.getElementById('email').style.border = '1px solid #ef4444';
- alert('Please enter a valid email address');
+ showError('Please enter a valid email address', 'email', step);
} else {
- document.getElementById('email').style.border = '';
+ const isEmailVerified = await verifyEmail(email);
+ if (!isEmailVerified) {
+ showError('Email already registered. Please use a different email address.', 'email', step);
+ isValid = false;
+ }
+ }
+ if (!companyName) {
+ isValid = false;
+ showError('Company / Client Name is Required', null, step);
+ return isValid;
}
// Validate other required fields
@@ -403,19 +527,16 @@ function validateStep(step) {
requiredFields.forEach(field => {
if (field.id !== 'email' && !field.value.trim()) {
isValid = false;
- field.style.border = '1px solid #ef4444';
+ field.classList.add('field-error');
} else if (field.id !== 'email') {
- field.style.border = '';
+ field.classList.remove('field-error');
}
});
} else if (step === 3) {
const email = document.getElementById('billing-email').value;
if (!validateEmail(email)) {
isValid = false;
- document.getElementById('billing-email').style.border = '1px solid #ef4444';
- alert('Please enter a valid email address');
- } else {
- document.getElementById('billing-email').style.border = '';
+ showError('Please enter a valid email address', 'billing-email', step);
}
// Validate other required fields
@@ -423,19 +544,27 @@ function validateStep(step) {
requiredFields.forEach(field => {
if (field.id !== 'email' && !field.value.trim()) {
isValid = false;
- field.style.border = '1px solid #ef4444';
+ field.classList.add('field-error');
} else if (field.id !== 'email') {
- field.style.border = '';
+ field.classList.remove('field-error');
}
});
} else if (step === 4) {
const email = document.getElementById('signatory-email').value;
if (!validateEmail(email)) {
isValid = false;
- document.getElementById('signatory-email').style.border = '1px solid #ef4444';
- alert('Please enter a valid email address');
- } else {
- document.getElementById('signatory-email').style.border = '';
+ showError('Please enter a valid email address', 'signatory-email', step);
+ }
+ const signatoryName = document.getElementById('signatory-name').value;
+ if (!signatoryName) {
+ isValid = false;
+ showError('Please enter a valid name', 'signatory-name', step);
+ }
+
+ if (!signatoryName && !validateEmail(email)) {
+ isValid = false;
+ showError('Name and Email are Required', 'signatory-name', step);
+ return isValid;
}
// Validate other required fields
@@ -443,9 +572,9 @@ function validateStep(step) {
requiredFields.forEach(field => {
if (field.id !== 'email' && !field.value.trim()) {
isValid = false;
- field.style.border = '1px solid #ef4444';
+ field.classList.add('field-error');
} else if (field.id !== 'email') {
- field.style.border = '';
+ field.classList.remove('field-error');
}
});
} else {
@@ -454,9 +583,9 @@ function validateStep(step) {
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');
}
});
}
@@ -553,7 +682,7 @@ function selectPaymentOption(type) {
// Function to submit the form
async function submitForm() {
// Validate current step
- if (!validateStep(currentStep)) {
+ if (!(await validateStep(currentStep))) {
return;
}
@@ -610,15 +739,15 @@ async function submitForm() {
} else {
// Handle server errors
if (response.status === 409) {
- alert('A user with this email already exists. Please use a different email.');
+ showError('A user with this email already exists. Please use a different email.', null, 5);
} else {
- alert('There was an error submitting your registration. Please try again.');
+ showError('There was an error submitting your registration. Please try again.', null, 5);
}
}
} 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, 5);
} finally {
// Restore button state
submitBtn.textContent = originalText;
@@ -682,14 +811,14 @@ async function detectLocation() {
? data.country_code
: 'US';
const region = data.region || '';
- const city = data.city || '';
- const zip = data.postal || '';
+ // const city = data.city || '';
+ // const zip = data.postal || '';
// Populate city and state based on detected location
- const cityInput = document.getElementById('city');
- const zipInput = document.getElementById('zip');
- if (cityInput) cityInput.value = city;
- if (zipInput) zipInput.value = zip;
+ // const cityInput = document.getElementById('city');
+ // const zipInput = document.getElementById('zip');
+ // if (cityInput) cityInput.value = city;
+ // if (zipInput) zipInput.value = zip;
// Populate states dropdown for the detected country
await populateStates(countryCode, 'state');
@@ -736,6 +865,8 @@ document.addEventListener('DOMContentLoaded', function() {
const cardNumberInput = document.getElementById('card-number');
if (cardNumberInput) {
cardNumberInput.addEventListener('input', function(e) {
+ const cursorPosition = e.target.selectionStart;
+ const originalLength = e.target.value.length;
// Format card number with spaces
let value = e.target.value.replace(/\s/g, '');
let formattedValue = '';
@@ -748,6 +879,39 @@ document.addEventListener('DOMContentLoaded', function() {
}
e.target.value = formattedValue;
+
+ let newCursorPosition = cursorPosition;
+
+ if (formattedValue.length > originalLength) {
+ const addedChars = formattedValue.length - originalLength;
+
+ // Count how many spaces were added before the cursor position
+ let spacesAddedBeforeCursor = 0;
+ for (let i = 0; i < cursorPosition && i < formattedValue.length; i++) {
+ if (formattedValue[i] === ' ' && (i < cursorPosition ||
+ (i === cursorPosition && formattedValue[i] === ' '))) {
+ spacesAddedBeforeCursor++;
+ }
+ }
+
+ newCursorPosition = cursorPosition + spacesAddedBeforeCursor;
+ } else if (formattedValue.length < originalLength) {
+ const removedChars = originalLength - formattedValue.length;
+
+ // Count how many spaces were removed before the cursor position
+ let spacesRemovedBeforeCursor = 0;
+ const oldValue = e.target.getAttribute('data-old-value') || '';
+ for (let i = 0; i < cursorPosition && i < oldValue.length; i++) {
+ if (oldValue[i] === ' ' && i < cursorPosition) {
+ spacesRemovedBeforeCursor++;
+ }
+ }
+
+ newCursorPosition = Math.max(0, cursorPosition - spacesRemovedBeforeCursor);
+ }
+
+ e.target.setAttribute('data-old-value', formattedValue);
+ e.target.setSelectionRange(newCursorPosition, newCursorPosition);
// Real-time card type detection
const cardType = detectCardType(value);
@@ -756,6 +920,24 @@ document.addEventListener('DOMContentLoaded', function() {
console.log('Card type:', cardType);
}
});
+
+ cardNumberInput.addEventListener('keydown', function(e) {
+ // Allow: backspace, delete, tab, escape, enter, arrows, home, end
+ if ([8, 9, 13, 27, 37, 38, 39, 40, 46].includes(e.keyCode) ||
+ // Allow: Ctrl+A, Ctrl+C, Ctrl+V, Ctrl+X
+ (e.keyCode === 65 && e.ctrlKey === true) ||
+ (e.keyCode === 67 && e.ctrlKey === true) ||
+ (e.keyCode === 86 && e.ctrlKey === true) ||
+ (e.keyCode === 88 && e.ctrlKey === true) ||
+ // Allow: numbers and numpad numbers
+ (e.keyCode >= 48 && e.keyCode <= 57) ||
+ (e.keyCode >= 96 && e.keyCode <= 105)) {
+ return;
+ }
+
+ // Prevent all other keys
+ e.preventDefault();
+ });
}
// Real-time expiry date formatting
@@ -863,6 +1045,20 @@ document.addEventListener('DOMContentLoaded', function() {
input.dispatchEvent(new Event('input'));
});
+ // Trigger change event for select
+ document.getElementById('billing-state').dispatchEvent(new Event('change'));
+ } else {
+ document.getElementById('billing-name').value = '';
+ document.getElementById('billing-address').value = '';
+ document.getElementById('billing-city').value = '';
+ document.getElementById('billing-state').value = '';
+ document.getElementById('billing-zip').value = '';
+
+ // Trigger input events to update labels
+ document.querySelectorAll('#billing-form .input-field').forEach(input => {
+ input.dispatchEvent(new Event('input'));
+ });
+
// Trigger change event for select
document.getElementById('billing-state').dispatchEvent(new Event('change'));
}
@@ -873,8 +1069,856 @@ document.addEventListener('DOMContentLoaded', function() {
if (this.checked) {
document.getElementById('billing-email').value = formData.userDetails.email || '';
document.getElementById('billing-email').dispatchEvent(new Event('input'));
+ } else {
+ document.getElementById('billing-email').value = '';
+ document.getElementById('billing-email').dispatchEvent(new Event('input'));
}
});
// Detect user location when the page loads
detectLocation();
-});
\ No newline at end of file
+});
+
+async function verifyEmail(email) {
+
+ try {
+ const response = await fetch('/api/v1/client/verify-client-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 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;
+}
+
+// Function to validate signatory name (no numbers allowed)
+function validateSignatoryName(input) {
+ const errorElement = document.getElementById('signatory-name-error');
+ const value = input.value;
+
+ // Remove any numbers from the input
+ const cleanValue = value.replace(/[0-9]/g, '');
+
+ // If numbers were removed, update the input value
+ if (value !== cleanValue) {
+ input.value = cleanValue;
+ }
+
+ // Show error if original value contained numbers
+ if (value !== cleanValue && value.length > 0) {
+ errorElement.style.display = 'block';
+ input.classList.add('field-error');
+ return false;
+ } else {
+ errorElement.style.display = 'none';
+ input.classList.remove('field-error');
+ return true;
+ }
+}
+
+// Function to prevent number input in real-time
+function preventNumbers(event) {
+ const charCode = event.which ? event.which : event.keyCode;
+
+ // Allow letters, spaces, hyphens, apostrophes, and backspace
+ if ((charCode >= 65 && charCode <= 90) || // A-Z
+ (charCode >= 97 && charCode <= 122) || // a-z
+ charCode === 32 || // space
+ charCode === 45 || // hyphen
+ charCode === 39 || // apostrophe
+ charCode === 8 || // backspace
+ charCode === 46) { // delete
+ return true;
+ }
+
+ // Prevent numbers and other characters
+ event.preventDefault();
+ return false;
+}
+
+// Function to validate company/client name
+function validateCompanyName(input) {
+ const errorElement = document.getElementById('company-name-error');
+ const value = input.value;
+
+ // Check various invalid patterns
+ const validationResult = validateCompanyNameContent(value);
+
+ if (!validationResult.isValid && value.trim().length > 0) {
+ errorElement.textContent = validationResult.message;
+ errorElement.style.display = 'block';
+ input.classList.add('field-error');
+ return false;
+ } else {
+ errorElement.style.display = 'none';
+ input.classList.remove('field-error');
+ return true;
+ }
+}
+
+function validateCompanyNameContent(companyName) {
+ const trimmed = companyName.trim();
+
+ if (trimmed.length === 0) {
+ return { isValid: false, message: 'Company name is required' };
+ }
+
+ // Check minimum length
+ if (trimmed.length < 2) {
+ return { isValid: false, message: 'Company name must be at least 2 characters' };
+ }
+
+ // Remove whitespace for content analysis
+ const content = trimmed.replace(/\s/g, '');
+
+ // Check if contains only special characters
+ if (/^[^a-zA-Z0-9]+$/.test(content)) {
+ return { isValid: false, message: 'Company name cannot be only special characters' };
+ }
+
+ // Check if contains at least some valid content (letters or numbers)
+ if (!/[a-zA-Z0-9]/.test(content)) {
+ return { isValid: false, message: 'Company name must contain letters or numbers' };
+ }
+
+ // Optional: Check for excessive special characters (more than 50%)
+ const specialCharCount = (content.match(/[^a-zA-Z0-9]/g) || []).length;
+ if (specialCharCount > content.length / 2) {
+ return { isValid: false, message: 'Company name contains too many special characters' };
+ }
+
+ return { isValid: true, message: '' };
+}
+
+// Function to prevent invalid character input in real-time
+function preventInvalidCompanyChars(event) {
+ const charCode = event.which ? event.which : event.keyCode;
+ const key = event.key;
+
+ // Allow: letters, numbers, spaces, hyphens, apostrophes, ampersands, periods, commas
+ // Also allow common editing keys
+ if ((charCode >= 65 && charCode <= 90) || // A-Z
+ (charCode >= 97 && charCode <= 122) || // a-z
+ (charCode >= 48 && charCode <= 57) || // 0-9
+ charCode === 32 || // space
+ charCode === 45 || // hyphen
+ charCode === 39 || // apostrophe
+ charCode === 38 || // ampersand (&)
+ charCode === 46 || // period (.)
+ charCode === 44 || // comma (,)
+ charCode === 8 || // backspace
+ charCode === 9 || // tab
+ charCode === 37 || // left arrow
+ charCode === 39 || // right arrow
+ charCode === 16 || // shift
+ charCode === 17 || // ctrl
+ charCode === 18 || // alt
+ charCode === 91 || // cmd (Mac)
+ /^[a-zA-Z0-9\s\-'&.,]$/.test(key)) { // allowed characters
+ return true;
+ }
+
+ // Prevent invalid characters
+ event.preventDefault();
+ return false;
+}
+
+// Function to validate ZIP code in real-time
+function validateZipCode(input) {
+ let errorElement;
+
+ // Determine which error element to use based on input ID
+ if (input.id === 'billing-zip') {
+ errorElement = document.getElementById('billing-zip-error');
+ } else if (input.id === 'card-zip') {
+ errorElement = document.getElementById('card-zip-error');
+ } else {
+ errorElement = document.getElementById('zip-error');
+ }
+
+ const value = input.value;
+
+ // Remove any non-numeric characters except hyphens
+ const cleanValue = value.replace(/[^0-9\-]/g, '');
+
+ // If invalid characters were removed, update the input value
+ if (value !== cleanValue) {
+ input.value = cleanValue;
+ }
+
+ // Format ZIP code as user types (add hyphen after 5 digits)
+ if (cleanValue.length === 5 && !cleanValue.includes('-')) {
+ input.value = cleanValue;
+ } else if (cleanValue.length > 5 && !cleanValue.includes('-')) {
+ input.value = cleanValue.substring(0, 5) + '' + cleanValue.substring(5, 9);
+ }
+
+
+ if (input.value.length < 4) {
+ errorElement.style.display = 'block';
+ input.classList.add('field-error');
+ return false;
+ } else {
+ errorElement.style.display = 'none';
+ input.classList.remove('field-error');
+ return true;
+ }
+}
+
+// Function to prevent non-numeric input in ZIP fields
+function preventNonNumericZip(event) {
+ const charCode = event.which ? event.which : event.keyCode;
+ const key = event.key;
+ const currentValue = event.target.value;
+
+ // Allow: numbers, hyphens (only in specific positions), and editing keys
+ if ((charCode >= 48 && charCode <= 57) || // 0-9
+ (charCode === 45 && currentValue.length === 5 && !currentValue.includes('-')) || // hyphen only after 5 digits
+ charCode === 8 || // backspace
+ charCode === 9 || // tab
+ charCode === 37 || // left arrow
+ charCode === 39 || // right arrow
+ charCode === 46 || // delete
+ charCode === 16 || // shift
+ charCode === 17 || // ctrl
+ charCode === 18 || // alt
+ charCode === 91) { // cmd (Mac)
+ return true;
+ }
+
+ // Prevent all other characters including letters and special characters
+ event.preventDefault();
+ return false;
+}
+
+// Function to validate city name in real-time
+function validateCity(input) {
+ let errorElement;
+
+ // Determine which error element to use based on input ID
+ if (input.id === 'billing-city') {
+ errorElement = document.getElementById('billing-city-error');
+ } else {
+ errorElement = document.getElementById('city-error');
+ }
+
+ const value = input.value;
+
+ // Remove invalid special characters (keep only letters, spaces, hyphens, apostrophes)
+ const cleanValue = value.replace(/[^a-zA-Z\s\-']/g, '');
+
+ // If invalid characters were removed, update the input value
+ if (value !== cleanValue) {
+ input.value = cleanValue;
+ }
+
+ // Check if the value contains only numbers
+ const numbersOnlyCheck = value.replace(/[\s\-']/g, '');
+ const hasOnlyNumbers = /^[0-9]+$/.test(numbersOnlyCheck) && numbersOnlyCheck.length > 0;
+
+ // Check if the value contains only special characters
+ const specialCharsOnlyCheck = value.replace(/[\s]/g, '');
+ const hasOnlySpecialChars = /^[^a-zA-Z0-9]+$/.test(specialCharsOnlyCheck) && specialCharsOnlyCheck.length > 0;
+
+ // Check if we have at least one letter
+ const hasLetters = /[a-zA-Z]/.test(value);
+
+ // Show appropriate error messages
+ if (value !== cleanValue && value.length > 0) {
+ errorElement.textContent = 'City name can only contain letters, spaces, hyphens, and apostrophes';
+ errorElement.style.display = 'block';
+ input.classList.add('field-error');
+ return false;
+ } else if (hasOnlyNumbers) {
+ errorElement.textContent = 'City name cannot be only numbers. Please include letters.';
+ errorElement.style.display = 'block';
+ input.classList.add('field-error');
+ return false;
+ } else if (hasOnlySpecialChars) {
+ errorElement.textContent = 'City name cannot be only special characters. Please include letters.';
+ errorElement.style.display = 'block';
+ input.classList.add('field-error');
+ return false;
+ } else if (!hasLetters && value.trim().length > 0) {
+ errorElement.textContent = 'City name must include at least one letter';
+ errorElement.style.display = 'block';
+ input.classList.add('field-error');
+ return false;
+ } else {
+ errorElement.style.display = 'none';
+ input.classList.remove('field-error');
+ return true;
+ }
+}
+
+// Function to prevent invalid character input in city fields
+function preventInvalidCityChars(event) {
+ const charCode = event.which ? event.which : event.keyCode;
+ const key = event.key;
+
+ // Allow: letters, spaces, hyphens, apostrophes, and editing keys
+ if ((charCode >= 65 && charCode <= 90) || // A-Z
+ (charCode >= 97 && charCode <= 122) || // a-z
+ charCode === 32 || // space
+ charCode === 45 || // hyphen
+ charCode === 39 || // apostrophe
+ charCode === 8 || // backspace
+ charCode === 9 || // tab
+ charCode === 37 || // left arrow
+ charCode === 39 || // right arrow
+ charCode === 46 || // delete
+ charCode === 16 || // shift
+ charCode === 17 || // ctrl
+ charCode === 18 || // alt
+ charCode === 91 || // cmd (Mac)
+ /^[a-zA-Z\s\-']$/.test(key)) { // allowed characters
+ return true;
+ }
+
+ // Prevent numbers and other special characters
+ event.preventDefault();
+ return false;
+}
+
+function validatePosition(input) {
+ const errorElement = document.getElementById('position-error');
+ const value = input.value;
+
+ // Remove numbers and invalid special characters
+ // Allow: letters, spaces, hyphens, apostrophes, periods, commas, ampersands, slashes
+ const cleanValue = value.replace(/[^a-zA-Z\s\-'.,&\/]/g, '');
+
+ // If invalid characters were removed, update the input value
+ if (value !== cleanValue) {
+ input.value = cleanValue;
+ }
+
+ // Check if the value contains only numbers
+ const numbersOnlyCheck = value.replace(/[\s\-'.,&\/]/g, '');
+ const hasOnlyNumbers = /^[0-9]+$/.test(numbersOnlyCheck) && numbersOnlyCheck.length > 0;
+
+ // Check if the value contains only special characters
+ const specialCharsOnlyCheck = value.replace(/[\s]/g, '');
+ const hasOnlySpecialChars = /^[^a-zA-Z0-9]+$/.test(specialCharsOnlyCheck) && specialCharsOnlyCheck.length > 0;
+
+ // Check if we have at least one letter
+ const hasLetters = /[a-zA-Z]/.test(value);
+
+ // Show appropriate error messages
+ if (value !== cleanValue && value.length > 0) {
+ errorElement.textContent = 'Position can only contain letters, spaces, hyphens, apostrophes, periods, commas, ampersands, and slashes';
+ errorElement.style.display = 'block';
+ input.classList.add('field-error');
+ return false;
+ } else if (hasOnlyNumbers) {
+ errorElement.textContent = 'Position cannot be only numbers. Please include letters.';
+ errorElement.style.display = 'block';
+ input.classList.add('field-error');
+ return false;
+ } else if (hasOnlySpecialChars) {
+ errorElement.textContent = 'Position cannot be only special characters. Please include letters.';
+ errorElement.style.display = 'block';
+ input.classList.add('field-error');
+ return false;
+ } else if (!hasLetters && value.trim().length > 0) {
+ errorElement.textContent = 'Position must include at least one letter';
+ errorElement.style.display = 'block';
+ input.classList.add('field-error');
+ return false;
+ } else {
+ errorElement.style.display = 'none';
+ input.classList.remove('field-error');
+ return true;
+ }
+}
+
+// Function to prevent invalid character input in position field
+function preventInvalidPositionChars(event) {
+ const charCode = event.which ? event.which : event.keyCode;
+ const key = event.key;
+
+ // Allow: letters, spaces, hyphens, apostrophes, periods, commas, ampersands, slashes
+ // Also allow common editing keys
+ if ((charCode >= 65 && charCode <= 90) || // A-Z
+ (charCode >= 97 && charCode <= 122) || // a-z
+ charCode === 32 || // space
+ charCode === 45 || // hyphen
+ charCode === 39 || // apostrophe
+ charCode === 46 || // period
+ charCode === 44 || // comma
+ charCode === 38 || // ampersand (&)
+ charCode === 47 || // slash (/)
+ charCode === 8 || // backspace
+ charCode === 9 || // tab
+ charCode === 37 || // left arrow
+ charCode === 39 || // right arrow
+ charCode === 46 || // delete
+ charCode === 16 || // shift
+ charCode === 17 || // ctrl
+ charCode === 18 || // alt
+ charCode === 91 || // cmd (Mac)
+ /^[a-zA-Z\s\-'.,&\/]$/.test(key)) { // allowed characters
+ return true;
+ }
+
+ // Prevent numbers and other special characters
+ event.preventDefault();
+ return false;
+}
+
+// Function to validate CVV in real-time
+function validateCVVVV(input) {
+ const errorElement = document.getElementById('security-code-error');
+ const value = input.value;
+
+ // Remove any non-digit characters
+ const cleanValue = value.replace(/\D/g, '');
+
+ // If non-digits were removed, update the input value
+ if (value !== cleanValue) {
+ input.value = cleanValue;
+ }
+
+ // Get card type to determine expected CVV length
+ const cardNumber = document.getElementById('card-number').value;
+ const cardType = detectCardType(cardNumber);
+
+ // Validate CVV format and content
+ const isValid = validateCVVStrict(cleanValue, cardType);
+
+ if (!isValid && cleanValue.length > 0) {
+ errorElement.textContent = getCVVErrorMessage(cleanValue, cardType);
+ errorElement.style.display = 'block';
+ input.classList.add('field-error');
+ return false;
+ } else {
+ errorElement.style.display = 'none';
+ input.classList.remove('field-error');
+ return true;
+ }
+}
+
+// Function to prevent invalid character input in CVV field
+function preventInvalidCVVChars(event) {
+ const charCode = event.which ? event.which : event.keyCode;
+ const key = event.key;
+
+ // Allow only digits and common editing keys
+ if ((charCode >= 48 && charCode <= 57) || // 0-9
+ charCode === 8 || // backspace
+ charCode === 9 || // tab
+ charCode === 37 || // left arrow
+ charCode === 39 || // right arrow
+ charCode === 46 || // delete
+ charCode === 16 || // shift
+ charCode === 17 || // ctrl
+ charCode === 18 || // alt
+ charCode === 91) { // cmd (Mac)
+ return true;
+ }
+
+ // Prevent all non-digit characters
+ event.preventDefault();
+ return false;
+}
+
+// Enhanced CVV validation function
+function validateCVVStrict(cvv, cardType) {
+ if (!cvv || !/^\d+$/.test(cvv)) {
+ return false;
+ }
+
+ // Check for invalid patterns
+ if (isInvalidCVVPattern(cvv)) {
+ return false;
+ }
+
+ const cvvLengths = {
+ visa: 3,
+ mastercard: 3,
+ amex: 4,
+ discover: 3,
+ diners: 3,
+ jcb: 3
+ };
+
+ const expectedLength = cvvLengths[cardType] || 3;
+
+ // Check length
+ if (cvv.length !== expectedLength) {
+ return false;
+ }
+
+ return true;
+}
+
+// Function to check for invalid CVV patterns
+function isInvalidCVVPattern(cvv) {
+ const invalidPatterns = [
+ /^0+$/, // All zeros: 000, 0000
+ /^1+$/, // All ones: 111, 1111
+ /^2+$/, // All twos: 222, 2222
+ /^3+$/, // All threes: 333, 3333
+ /^4+$/, // All fours: 444, 4444
+ /^5+$/, // All fives: 555, 5555
+ /^6+$/, // All sixes: 666, 6666
+ /^7+$/, // All sevens: 777, 7777
+ /^8+$/, // All eights: 888, 8888
+ /^9+$/, // All nines: 999, 9999
+ ];
+
+ return invalidPatterns.some(pattern => pattern.test(cvv));
+}
+
+// Function to get appropriate CVV error message
+function getCVVErrorMessage(cvv, cardType) {
+ if (!cvv) {
+ return 'Security code is required';
+ }
+
+ if (!/^\d+$/.test(cvv)) {
+ return 'Security code must contain only numbers';
+ }
+
+ if (isInvalidCVVPattern(cvv)) {
+ return 'Please enter a valid security code';
+ }
+
+ const cvvLengths = {
+ visa: 3,
+ mastercard: 3,
+ amex: 4,
+ discover: 3,
+ diners: 3,
+ jcb: 3
+ };
+
+ const expectedLength = cvvLengths[cardType] || 3;
+
+ if (cvv.length !== expectedLength) {
+ if (cardType === 'amex') {
+ return 'American Express cards require a 4-digit security code';
+ } else {
+ return `Security code must be ${expectedLength} digits`;
+ }
+ }
+
+ return 'Please enter a valid security code';
+}
+
+// Bank Name validation
+function validateBankName(input) {
+ const errorElement = document.getElementById('bank-name-error');
+ const value = input.value;
+
+ // Remove numbers and invalid special characters
+ const cleanValue = value.replace(/[^a-zA-Z\s\-'&.,()]/g, '');
+
+ if (value !== cleanValue) {
+ input.value = cleanValue;
+ }
+
+ // Check content
+ const contentCheck = value.replace(/[\s\-'&.,()]/g, '');
+ const hasOnlyNumbers = /^[0-9]+$/.test(contentCheck) && contentCheck.length > 0;
+ const hasOnlySpecialChars = /^[^a-zA-Z0-9]+$/.test(contentCheck) && contentCheck.length > 0;
+ const hasLetters = /[a-zA-Z]/.test(value);
+
+ if (value !== cleanValue && value.length > 0) {
+ errorElement.textContent = 'Bank name can only contain letters, spaces, and common punctuation';
+ errorElement.style.display = 'block';
+ input.classList.add('field-error');
+ return false;
+ } else if (hasOnlyNumbers) {
+ errorElement.textContent = 'Bank name cannot be only numbers';
+ errorElement.style.display = 'block';
+ input.classList.add('field-error');
+ return false;
+ } else if (hasOnlySpecialChars) {
+ errorElement.textContent = 'Bank name cannot be only special characters';
+ errorElement.style.display = 'block';
+ input.classList.add('field-error');
+ return false;
+ } else if (!hasLetters && value.trim().length > 0) {
+ errorElement.textContent = 'Bank name must include letters';
+ errorElement.style.display = 'block';
+ input.classList.add('field-error');
+ return false;
+ } else {
+ errorElement.style.display = 'none';
+ input.classList.remove('field-error');
+ return true;
+ }
+}
+
+function preventInvalidBankNameChars(event) {
+ const charCode = event.which ? event.which : event.keyCode;
+ const key = event.key;
+
+ if ((charCode >= 65 && charCode <= 90) ||
+ (charCode >= 97 && charCode <= 122) ||
+ charCode === 32 || charCode === 45 || charCode === 39 ||
+ charCode === 38 || charCode === 46 || charCode === 44 ||
+ charCode === 40 || charCode === 41 ||
+ charCode === 8 || charCode === 9 || charCode === 37 ||
+ charCode === 39 || charCode === 46 || charCode === 16 ||
+ charCode === 17 || charCode === 18 || charCode === 91 ||
+ /^[a-zA-Z\s\-'&.,()]$/.test(key)) {
+ return true;
+ }
+
+ event.preventDefault();
+ return false;
+}
+
+// Enhanced Account Number validation
+function validateAccountNumber(input) {
+ const errorElement = document.getElementById('account-number-error');
+ const value = input.value.replace(/\s/g, '');
+
+ // Remove any non-digit characters
+ const cleanValue = value.replace(/\D/g, '');
+
+ if (value !== cleanValue) {
+ input.value = cleanValue;
+ }
+
+ // Validate account number
+ const isValid = validateAccountNumberStrict(cleanValue);
+
+ if (!isValid && cleanValue.length > 0) {
+ errorElement.textContent = getAccountNumberErrorMessage(cleanValue);
+ errorElement.style.display = 'block';
+ input.classList.add('field-error');
+ return false;
+ } else {
+ errorElement.style.display = 'none';
+ input.classList.remove('field-error');
+ return true;
+ }
+}
+
+function preventInvalidAccountNumberChars(event) {
+ const charCode = event.which ? event.which : event.keyCode;
+
+ if ((charCode >= 48 && charCode <= 57) ||
+ charCode === 8 || charCode === 9 || charCode === 37 ||
+ charCode === 39 || charCode === 46 || charCode === 16 ||
+ charCode === 17 || charCode === 18 || charCode === 91) {
+ return true;
+ }
+
+ event.preventDefault();
+ return false;
+}
+
+function validateAccountNumberStrict(accountNumber) {
+ if (!accountNumber || !/^\d+$/.test(accountNumber)) {
+ return false;
+ }
+
+ // Check length
+ if (accountNumber.length < 8 || accountNumber.length > 17) {
+ return false;
+ }
+
+ // Check for invalid patterns
+ if (isInvalidAccountNumberPattern(accountNumber)) {
+ return false;
+ }
+
+ // Simple checksum validation (basic example)
+ if (!isValidAccountNumberChecksum(accountNumber)) {
+ return false;
+ }
+
+ return true;
+}
+
+function isInvalidAccountNumberPattern(accountNumber) {
+ const invalidPatterns = [
+ /^0+$/, // All zeros
+ /^1+$/, // All ones
+ /^2+$/, // All twos
+ /^3+$/, // All threes
+ /^4+$/, // All fours
+ /^5+$/, // All fives
+ /^6+$/, // All sixes
+ /^7+$/, // All sevens
+ /^8+$/, // All eights
+ /^9+$/, // All nines
+ /^12345678/, // Sequential
+ /^87654321/, // Reverse sequential
+ ];
+
+ return invalidPatterns.some(pattern => pattern.test(accountNumber));
+}
+
+function isValidAccountNumberChecksum(accountNumber) {
+ // Basic checksum validation - you might want to implement
+ // a more specific algorithm based on bank requirements
+ let sum = 0;
+ for (let i = 0; i < accountNumber.length; i++) {
+ sum += parseInt(accountNumber.charAt(i));
+ }
+
+ // Simple check: sum shouldn't be too low (e.g., all zeros)
+ return sum > 0;
+}
+
+function getAccountNumberErrorMessage(accountNumber) {
+ if (!accountNumber) {
+ return 'Account number is required';
+ }
+
+ if (!/^\d+$/.test(accountNumber)) {
+ return 'Account number must contain only numbers';
+ }
+
+ if (accountNumber.length < 8) {
+ return 'Account number must be at least 8 digits';
+ }
+
+ if (accountNumber.length > 17) {
+ return 'Account number cannot exceed 17 digits';
+ }
+
+ if (isInvalidAccountNumberPattern(accountNumber)) {
+ return 'Please enter a valid account number';
+ }
+
+ return 'Please enter a valid account number';
+}
+
+// Account Holder Name validation
+function validateAccountHolderName(input) {
+ const errorElement = document.getElementById('account-holder-name-error');
+ const value = input.value;
+
+ // Remove numbers and special characters (keep only letters and spaces)
+ const cleanValue = value.replace(/[^a-zA-Z\s]/g, '');
+
+ if (value !== cleanValue) {
+ input.value = cleanValue;
+ }
+
+ // Check content
+ const hasOnlyNumbers = /^[0-9]+$/.test(value.replace(/\s/g, ''));
+ const hasOnlySpecialChars = /^[^a-zA-Z0-9]+$/.test(value.replace(/\s/g, ''));
+ const hasLetters = /[a-zA-Z]/.test(value);
+
+ if (value !== cleanValue && value.length > 0) {
+ errorElement.textContent = 'Account holder name can only contain letters and spaces';
+ errorElement.style.display = 'block';
+ input.classList.add('field-error');
+ return false;
+ } else if (hasOnlyNumbers) {
+ errorElement.textContent = 'Account holder name cannot contain only numbers';
+ errorElement.style.display = 'block';
+ input.classList.add('field-error');
+ return false;
+ } else if (hasOnlySpecialChars) {
+ errorElement.textContent = 'Account holder name cannot contain only special characters';
+ errorElement.style.display = 'block';
+ input.classList.add('field-error');
+ return false;
+ } else if (!hasLetters && value.trim().length > 0) {
+ errorElement.textContent = 'Account holder name must include letters';
+ errorElement.style.display = 'block';
+ input.classList.add('field-error');
+ return false;
+ } else {
+ errorElement.style.display = 'none';
+ input.classList.remove('field-error');
+ return true;
+ }
+}
+
+function preventInvalidAccountHolderChars(event) {
+ const charCode = event.which ? event.which : event.keyCode;
+ const key = event.key;
+
+ if ((charCode >= 65 && charCode <= 90) ||
+ (charCode >= 97 && charCode <= 122) ||
+ charCode === 32 ||
+ charCode === 8 || charCode === 9 || charCode === 37 ||
+ charCode === 39 || charCode === 46 || charCode === 16 ||
+ charCode === 17 || charCode === 18 || charCode === 91 ||
+ /^[a-zA-Z\s]$/.test(key)) {
+ return true;
+ }
+
+ event.preventDefault();
+ return false;
+}
\ No newline at end of file
diff --git a/public/js/va-register.js b/public/js/va-register.js
index 2c65a26..32928e0 100644
--- a/public/js/va-register.js
+++ b/public/js/va-register.js
@@ -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,9 +332,13 @@
}
// 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
@@ -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;
+ }
\ No newline at end of file
diff --git a/src/controllers/client/dashboard/clientDashboardProjects.controller.js b/src/controllers/client/dashboard/clientDashboardProjects.controller.js
new file mode 100644
index 0000000..d7b77cc
--- /dev/null
+++ b/src/controllers/client/dashboard/clientDashboardProjects.controller.js
@@ -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 };
\ No newline at end of file
diff --git a/src/controllers/client/vaClientEmailVerify.controller.js b/src/controllers/client/vaClientEmailVerify.controller.js
new file mode 100644
index 0000000..5813e41
--- /dev/null
+++ b/src/controllers/client/vaClientEmailVerify.controller.js
@@ -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 };
\ No newline at end of file
diff --git a/src/controllers/va/vaAuth/vaLogin.controller.js b/src/controllers/va/vaAuth/vaLogin.controller.js
index 08a173b..7e00699 100644
--- a/src/controllers/va/vaAuth/vaLogin.controller.js
+++ b/src/controllers/va/vaAuth/vaLogin.controller.js
@@ -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);
diff --git a/src/controllers/va/vaAuth/vaVerifyEmail.controller.js b/src/controllers/va/vaAuth/vaVerifyEmail.controller.js
new file mode 100644
index 0000000..4aa278e
--- /dev/null
+++ b/src/controllers/va/vaAuth/vaVerifyEmail.controller.js
@@ -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 };
\ No newline at end of file
diff --git a/src/controllers/va/vaLeaves/vaLeaveList.controller.js b/src/controllers/va/vaLeaves/vaLeaveList.controller.js
new file mode 100644
index 0000000..d79a9e1
--- /dev/null
+++ b/src/controllers/va/vaLeaves/vaLeaveList.controller.js
@@ -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 };
\ No newline at end of file
diff --git a/src/controllers/va/vaLeaves/vaLeaveManage.controller.js b/src/controllers/va/vaLeaves/vaLeaveManage.controller.js
new file mode 100644
index 0000000..09bd226
--- /dev/null
+++ b/src/controllers/va/vaLeaves/vaLeaveManage.controller.js
@@ -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 };
\ No newline at end of file
diff --git a/src/controllers/va/vaLeaves/vaLeavesApply.controller.js b/src/controllers/va/vaLeaves/vaLeavesApply.controller.js
new file mode 100644
index 0000000..489a6e3
--- /dev/null
+++ b/src/controllers/va/vaLeaves/vaLeavesApply.controller.js
@@ -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 };
\ No newline at end of file
diff --git a/src/controllers/va/vaLog/vaLogTrackWork.controller.js b/src/controllers/va/vaLog/vaLogTrackWork.controller.js
index 89366f6..ddf5d22 100644
--- a/src/controllers/va/vaLog/vaLogTrackWork.controller.js
+++ b/src/controllers/va/vaLog/vaLogTrackWork.controller.js
@@ -65,13 +65,52 @@ async function checkDailyHoursAndLogout(vaId, vaEmail, projectId, project, curre
vaEmail,
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 {
- shouldLogout: true,
- totalTodayHours: totalTodayHours,
- dedicatedHours: project.dedicatedHours,
- logoutLogId: logoutLog._id
- };
+ 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 {
diff --git a/src/controllers/va/vaProjects/vaProjectLog.controller.js b/src/controllers/va/vaProjects/vaProjectLog.controller.js
index a06daaa..1fcc9c6 100644
--- a/src/controllers/va/vaProjects/vaProjectLog.controller.js
+++ b/src/controllers/va/vaProjects/vaProjectLog.controller.js
@@ -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,
+ }
+
+ 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
- };
+ 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");
diff --git a/src/controllers/va/vaTimesheetAdjustment/vaTimesheetAdjustment.controller.js b/src/controllers/va/vaTimesheetAdjustment/vaTimesheetAdjustment.controller.js
index d659168..6931442 100644
--- a/src/controllers/va/vaTimesheetAdjustment/vaTimesheetAdjustment.controller.js
+++ b/src/controllers/va/vaTimesheetAdjustment/vaTimesheetAdjustment.controller.js
@@ -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);
diff --git a/src/helper/updateEntityRelationships.js b/src/helper/updateEntityRelationships.js
index 060ce63..4a81d1f 100644
--- a/src/helper/updateEntityRelationships.js
+++ b/src/helper/updateEntityRelationships.js
@@ -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';
diff --git a/src/models/vaModel.js b/src/models/vaModel.js
index 5b31afc..b33241f 100644
--- a/src/models/vaModel.js
+++ b/src/models/vaModel.js
@@ -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 };
\ No newline at end of file
+export { VAModel, ClientModel, ProjectModel, LoggedModel, WorkTrackModel, PasswordResetModel, AdjustedHoursModel, LeavesModel };
\ No newline at end of file
diff --git a/src/routes/v1/client.routes.js b/src/routes/v1/client.routes.js
index 941632f..382d722 100644
--- a/src/routes/v1/client.routes.js
+++ b/src/routes/v1/client.routes.js
@@ -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
diff --git a/src/routes/v1/va.routes.js b/src/routes/v1/va.routes.js
index 7beaf43..087e684 100644
--- a/src/routes/v1/va.routes.js
+++ b/src/routes/v1/va.routes.js
@@ -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)
diff --git a/src/server.js b/src/server.js
index 5e7972b..5e99b37 100644
--- a/src/server.js
+++ b/src/server.js
@@ -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;
diff --git a/src/services/passwordReset.service.js b/src/services/passwordReset.service.js
index 8f0b6fa..f435c4f 100644
--- a/src/services/passwordReset.service.js
+++ b/src/services/passwordReset.service.js
@@ -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."
};
}
diff --git a/views/client/dashboard-dashboard.ejs b/views/client/dashboard-dashboard.ejs
index 12481a8..5cf23ad 100644
--- a/views/client/dashboard-dashboard.ejs
+++ b/views/client/dashboard-dashboard.ejs
@@ -29,12 +29,12 @@
Virtual Assistants
-
+
+
+
TT
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ | Name |
+ Hourly Rate |
+ Total VA Assigned |
+ Dedicated Hours |
+ Status |
+
+
+
+
+
+
+
+
+
+
+
No Projects
+
You don't have any projects yet.
+
+
+
+
+
+
+
+
+
+
+
+
+
+ | Name |
+ Email |
+ Status |
+
+
+
+
+
+
+
+
+
+
+
No Virtual Assistants
+
You don't have any virtual assistants assigned yet.
+
+
+
+
+
+
+
+
+
+