Compare commits

...

4 Commits

12 changed files with 231 additions and 123 deletions

View File

@ -492,9 +492,11 @@ async function validateStep(step) {
} else if (step === 1) { } else if (step === 1) {
const email = document.getElementById('email').value; const email = document.getElementById('email').value;
const companyName = document.getElementById('company-name').value; const companyName = document.getElementById('company-name').value;
if (!validateEmail(email) && !companyName) { const password = document.getElementById('password').value;
const confirmPassword = document.getElementById('confirmPassword').value;
if (!validateEmail(email) && !companyName && !password && !confirmPassword) {
isValid = false; isValid = false;
showError('Both Fields are Required', null, step); showError('Fields are Required', null, step);
let requiredFields1 = form.querySelectorAll('[required]'); let requiredFields1 = form.querySelectorAll('[required]');
requiredFields1.forEach(field => { requiredFields1.forEach(field => {
if (!field.value.trim()) { if (!field.value.trim()) {
@ -522,6 +524,21 @@ async function validateStep(step) {
return isValid; return isValid;
} }
if (password.length < 8) {
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');
}
// Validate other required fields // Validate other required fields
const requiredFields = form.querySelectorAll('[required]'); const requiredFields = form.querySelectorAll('[required]');
requiredFields.forEach(field => { requiredFields.forEach(field => {
@ -612,6 +629,7 @@ function saveStepData(step) {
case 1: case 1:
formData.userDetails.name = document.getElementById('company-name').value; formData.userDetails.name = document.getElementById('company-name').value;
formData.userDetails.email = document.getElementById('email').value; formData.userDetails.email = document.getElementById('email').value;
formData.userDetails.password = document.getElementById('password').value;
break; break;
case 2: case 2:
formData.userDetails.address = document.getElementById('address').value; formData.userDetails.address = document.getElementById('address').value;
@ -1955,3 +1973,51 @@ function preventInvalidAccountHolderChars(event) {
event.preventDefault(); event.preventDefault();
return false; return false;
} }
// Function to generate a strong password
function generatePassword() {
const length = 12;
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*";
let password = "";
// Ensure at least one of each required character type
password += "ABCDEFGHIJKLMNOPQRSTUVWXYZ"[Math.floor(Math.random() * 26)];
password += "abcdefghijklmnopqrstuvwxyz"[Math.floor(Math.random() * 26)];
password += "0123456789"[Math.floor(Math.random() * 10)];
password += "!@#$%^&*"[Math.floor(Math.random() * 8)];
// Fill the rest randomly
for (let i = password.length; i < length; i++) {
password += charset.charAt(Math.floor(Math.random() * charset.length));
}
// Shuffle the password
password = password.split('').sort(() => 0.5 - Math.random()).join('');
// Set the password field
document.getElementById('password').value = password;
document.getElementById('confirmPassword').value = password;
// Trigger input events to update UI
document.getElementById('password').dispatchEvent(new Event('input'));
document.getElementById('confirmPassword').dispatchEvent(new Event('input'));
}
// Function to toggle password visibility
function togglePasswordVisibility() {
const passwordField = document.getElementById('password');
const confirmField = document.getElementById('confirmPassword');
const toggleButton = document.querySelector('.password-toggle');
if (passwordField.type === 'password') {
passwordField.type = 'text';
confirmField.type = 'text';
toggleButton.textContent = 'Hide Password';
} else {
passwordField.type = 'password';
confirmField.type = 'password';
toggleButton.textContent = 'Show Password';
}
}

View File

@ -1,5 +1,6 @@
import { VAModel, LoggedModel, ClientModel, ProjectModel } from "../../../models/vaModel.js"; import { VAModel, LoggedModel, ClientModel, ProjectModel } from "../../../models/vaModel.js";
import jwt from "jsonwebtoken"; import jwt from "jsonwebtoken";
import getDayNumber from "../../../utils/getDayNumber.js";
// Getting client dashboard data - all VAs, projects, and active VAs // Getting client dashboard data - all VAs, projects, and active VAs
async function client_dashboard(req, res) { async function client_dashboard(req, res) {
@ -314,9 +315,11 @@ async function client_dashboard(req, res) {
}; };
}); });
const dayNumber = getDayNumber(logTime);
// Calculate dashboard statistics // Calculate dashboard statistics
const totalHoursLogged = clientProjects.reduce((sum, p) => sum + (p.totalLoggedHours || 0), 0); const totalHoursLogged = clientProjects.reduce((sum, p) => sum + (p.totalLoggedHours || 0), 0);
const totalDedicatedHours = clientProjects.reduce((sum, p) => sum + (p.dedicatedHours || 0), 0); const totalDedicatedHours = clientProjects.reduce((sum, p) => sum + (p.dedicatedHours[dayNumber] || 0), 0);
const currentlyActiveVAs = allVAsFormatted.filter(va => va.isCurrentlyActive); const currentlyActiveVAs = allVAsFormatted.filter(va => va.isCurrentlyActive);
const inactiveVAs = allVAsFormatted.filter(va => !va.isCurrentlyActive); const inactiveVAs = allVAsFormatted.filter(va => !va.isCurrentlyActive);

View File

@ -1,5 +1,6 @@
import { ClientModel, ProjectModel, VAModel, LoggedModel } from "../../../models/vaModel.js"; import { ClientModel, ProjectModel, VAModel, LoggedModel } from "../../../models/vaModel.js";
import jwt from "jsonwebtoken"; import jwt from "jsonwebtoken";
import getDayNumber from "../../../utils/getDayNumber.js";
function formatHoursToReadable(hours) { function formatHoursToReadable(hours) {
const wholeHours = Math.floor(hours); const wholeHours = Math.floor(hours);
@ -112,13 +113,15 @@ async function client_dashboard_projects(req, res) {
const validVADetails = assignedVADetails.filter(Boolean); const validVADetails = assignedVADetails.filter(Boolean);
const activeVAsCount = validVADetails.filter(va => va.isActive).length; const activeVAsCount = validVADetails.filter(va => va.isActive).length;
const dayNumber = getDayNumber();
return { return {
id: project._id, id: project._id,
name: project.name, name: project.name,
hourlyRate: project.hourlyRate, hourlyRate: project.hourlyRate,
vaAssigned: validVADetails.length, vaAssigned: validVADetails.length,
activeVAs: activeVAsCount, activeVAs: activeVAsCount,
dedicatedHours: formatHoursToReadable(project.dedicatedHours) || "0h", dedicatedHours: formatHoursToReadable(project.dedicatedHours[dayNumber]) || "0h",
status: project.status || '', status: project.status || '',
assignedVADetails: validVADetails assignedVADetails: validVADetails
}; };

View File

@ -25,10 +25,10 @@ async function va_register_client(req, res) {
try { try {
const payload = req.body || {}; const payload = req.body || {};
const { userDetails } = payload; const { userDetails } = payload;
if (!userDetails || !userDetails.email || !userDetails.name) { if (!userDetails || !userDetails.email || !userDetails.name || !userDetails.password) {
return res.status(400).json({ return res.status(400).json({
success: false, success: false,
message: "Missing required fields: name and email are required" message: "Missing required fields: name, email, and password are required"
}); });
} }

View File

@ -1,7 +1,6 @@
import bcrypt from "bcrypt"; import bcrypt from "bcrypt";
import jwt from "jsonwebtoken"; import jwt from "jsonwebtoken";
import { ClientModel, VAModel } from "../../models/vaModel.js"; import { ClientModel, VAModel } from "../../models/vaModel.js";
import { va_timesheet_adjustment_function } from "../va/vaTimesheetAdjustment/vaTimesheetAdjustment.controller.js";
// Login VA // Login VA
async function va_login_client(req, res) { async function va_login_client(req, res) {

View File

@ -4,99 +4,99 @@ import { VAModel, LoggedModel, WorkTrackModel } from "../../../models/vaModel.js
import authFetch from "../../../utils/authFetch.js"; import authFetch from "../../../utils/authFetch.js";
// function to close open work tracks and create logout // function to close open work tracks and create logout
async function closePreviousSession(vaId, lastLog) { // async function closePreviousSession(vaId, lastLog) {
try { // try {
const currentTime = new Date(); // const currentTime = new Date();
// Find all open work tracks for this VA and project (where to is not set or totalTime is 0) // // Find all open work tracks for this VA and project (where to is not set or totalTime is 0)
const openWorkTracks = await WorkTrackModel.find({ // const openWorkTracks = await WorkTrackModel.find({
vaId, // vaId,
logId: lastLog._id, // logId: lastLog._id,
$or: [ // $or: [
{ to: { $exists: false } }, // { to: { $exists: false } },
{ totalTime: 0 }, // { totalTime: 0 },
{ to: null } // { to: null }
] // ]
}); // });
// console.log(`Found ${openWorkTracks.length} open work tracks to close`); // console.log(`Found ${openWorkTracks.length} open work tracks to close`);
// Close all open work tracks // // Close all open work tracks
for (const workTrack of openWorkTracks) { // for (const workTrack of openWorkTracks) {
const fromTime = new Date(workTrack.from); // const fromTime = new Date(workTrack.from);
const totalTimeMs = currentTime - fromTime; // const totalTimeMs = currentTime - fromTime;
// Update the work track with end time and total time // // Update the work track with end time and total time
workTrack.to = currentTime; // workTrack.to = currentTime;
workTrack.totalTime = totalTimeMs; // workTrack.totalTime = totalTimeMs;
await workTrack.save(); // await workTrack.save();
// console.log(`Closed work track ${workTrack._id} with duration: ${totalTimeMs}ms`); // // console.log(`Closed work track ${workTrack._id} with duration: ${totalTimeMs}ms`);
} // }
// Create a logout log entry // // Create a logout log entry
const logoutLog = await LoggedModel.create({ // const logoutLog = await LoggedModel.create({
loggedType: "LOGOUT", // loggedType: "LOGOUT",
vaId, // vaId,
vaEmail: lastLog.vaEmail, // Use email from previous log // vaEmail: lastLog.vaEmail, // Use email from previous log
projectId: lastLog.projectId, // projectId: lastLog.projectId,
time: currentTime // Use same timestamp for consistent logging // time: currentTime // Use same timestamp for consistent logging
}); // });
// console.log(`Created logout log: ${logoutLog._id}`); // // console.log(`Created logout log: ${logoutLog._id}`);
try { // try {
const salesforceUrl = `${process.env.SF_DOCTRACKER_API_URL}services/apexrest/docva/api/va-project-log`; // const salesforceUrl = `${process.env.SF_DOCTRACKER_API_URL}services/apexrest/docva/api/va-project-log`;
const sfDataSync = { // const sfDataSync = {
"_id": logoutLog._id, // "_id": logoutLog._id,
"loggedType": "LOGOUT", // "loggedType": "LOGOUT",
"vaId": vaId, // "vaId": vaId,
"vaEmail": lastLog.vaEmail, // "vaEmail": lastLog.vaEmail,
"projectId": lastLog.projectId, // "projectId": lastLog.projectId,
"time": logoutLog.time, // "time": logoutLog.time,
} // }
const response = await authFetch(salesforceUrl, { // const response = await authFetch(salesforceUrl, {
method: 'POST', // method: 'POST',
headers: { // headers: {
'Content-Type': 'application/json', // 'Content-Type': 'application/json',
// Add any required headers for Salesforce authentication // // Add any required headers for Salesforce authentication
// 'Authorization': 'Bearer ' + process.env.SF_ACCESS_TOKEN // // 'Authorization': 'Bearer ' + process.env.SF_ACCESS_TOKEN
}, // },
body: JSON.stringify(sfDataSync) // body: JSON.stringify(sfDataSync)
}); // });
if (!response.ok) { // if (!response.ok) {
const errorText = await response.text(); // const errorText = await response.text();
throw new Error(`Salesforce API responded with status ${response.status}: ${errorText}`); // throw new Error(`Salesforce API responded with status ${response.status}: ${errorText}`);
} // }
const result = await response.json(); // const result = await response.json();
// console.log("✅ Successfully synced VA data to Salesforce:", result); // // console.log("✅ Successfully synced VA data to Salesforce:", result);
return { // return {
closedWorkTracks: openWorkTracks.length, // closedWorkTracks: openWorkTracks.length,
logoutLogId: logoutLog._id, // logoutLogId: logoutLog._id,
SyncSalesforce: result // SyncSalesforce: result
}; // };
} catch (error) { // } catch (error) {
console.error("Failed to sync with Salesforce:", error.message); // console.error("Failed to sync with Salesforce:", error.message);
console.log(error) // console.log(error)
// Create a custom error to distinguish Salesforce errors // // Create a custom error to distinguish Salesforce errors
const salesforceError = new Error(`Salesforce sync failed: ${error.message}`); // const salesforceError = new Error(`Salesforce sync failed: ${error.message}`);
salesforceError.isSalesforceError = true; // salesforceError.isSalesforceError = true;
throw salesforceError; // throw salesforceError;
} // }
// return { // // return {
// closedWorkTracks: openWorkTracks.length, // // closedWorkTracks: openWorkTracks.length,
// logoutLogId: logoutLog._id, // // logoutLogId: logoutLog._id,
// }; // // };
} catch (error) { // } catch (error) {
console.error("Error closing previous session:", error); // console.error("Error closing previous session:", error);
throw error; // throw error;
} // }
} // }
// Login VA // Login VA
async function va_login_callback(req, res) { async function va_login_callback(req, res) {
@ -132,17 +132,17 @@ async function va_login_callback(req, res) {
// also on main login when user try to login into app then logout project and workModel track then allow login on same timestamp // also on main login when user try to login into app then logout project and workModel track then allow login on same timestamp
// Check if VA has any active sessions (LOGIN without LOGOUT) // Check if VA has any active sessions (LOGIN without LOGOUT)
const activeSessions = await LoggedModel.find({ // const activeSessions = await LoggedModel.find({
vaId: user._id // vaId: user._id
}).sort({ time: -1 }); // }).sort({ time: -1 });
let closeResult = []; // let closeResult = [];
if (activeSessions.length > 0) { // if (activeSessions.length > 0) {
// Get the latest login to check if there's no corresponding logout // // Get the latest login to check if there's no corresponding logout
const latestLog = activeSessions[0]; // const latestLog = activeSessions[0];
closeResult = await closePreviousSession(user._id, latestLog); // closeResult = await closePreviousSession(user._id, latestLog);
} // }
// Generate JWT accessToken with 24h expiry // Generate JWT accessToken with 24h expiry
const accessToken = jwt.sign( const accessToken = jwt.sign(
@ -172,23 +172,23 @@ async function va_login_callback(req, res) {
let responseData = []; let responseData = [];
if (closeResult) { // if (closeResult) {
responseData = { // responseData = {
success: true, // success: true,
message: "Login Successfull", // message: "Login Successfull",
accessToken, // accessToken,
refreshToken, // refreshToken,
user: { // user: {
id: user._id, // id: user._id,
name: user.userDetails.firstName, // name: user.userDetails.firstName,
email: user.userDetails.email, // email: user.userDetails.email,
}, // },
previousSession: { // previousSession: {
closedWorkTracks: closeResult.closedWorkTracks, // closedWorkTracks: closeResult.closedWorkTracks,
logoutLogId: closeResult.logoutLogId // logoutLogId: closeResult.logoutLogId
} // }
} // }
} else { // } else {
responseData = { responseData = {
success: true, success: true,
message: "Login Successfull", message: "Login Successfull",
@ -200,7 +200,7 @@ async function va_login_callback(req, res) {
email: user.userDetails.email, email: user.userDetails.email,
} }
} }
} // }
// Return accessToken and user info // Return accessToken and user info
return res.status(200).json(responseData); return res.status(200).json(responseData);

View File

@ -1,5 +1,6 @@
import { LoggedModel, WorkTrackModel, ProjectModel, VAModel } from "../../../models/vaModel.js"; import { LoggedModel, WorkTrackModel, ProjectModel, VAModel } from "../../../models/vaModel.js";
import authFetch from "../../../utils/authFetch.js"; import authFetch from "../../../utils/authFetch.js";
import getDayNumber from "../../../utils/getDayNumber.js";
// Helper function to get start and end of day in UTC // Helper function to get start and end of day in UTC
function getDayRange(date = new Date()) { function getDayRange(date = new Date()) {
@ -429,11 +430,13 @@ async function va_project_log_callback(req, res) {
const totalTodayHours = totalTodayMs / (1000 * 60 * 60); const totalTodayHours = totalTodayMs / (1000 * 60 * 60);
const dayNumber = getDayNumber(logTime);
// Check if daily hours are already completed // Check if daily hours are already completed
if (project.dedicatedHours > 0 && totalTodayHours >= project.dedicatedHours) { if (project.dedicatedHours[dayNumber] > 0 && totalTodayHours >= project.dedicatedHours[dayNumber]) {
return res.status(400).json({ return res.status(400).json({
success: false, success: false,
message: `Cannot login - daily hours already completed (${formatHoursToReadable(totalTodayHours)} / ${formatHoursToReadable(project.dedicatedHours)})`, message: `Cannot login - daily hours already completed (${formatHoursToReadable(totalTodayHours)} / ${formatHoursToReadable(project.dedicatedHours[dayNumber])})`,
// warnings: scheduleCheck.warnings // warnings: scheduleCheck.warnings
}); });
} }

View File

@ -1,5 +1,6 @@
import jwt from "jsonwebtoken"; import jwt from "jsonwebtoken";
import { ProjectModel, VAModel, LoggedModel } from "../../../models/vaModel.js"; import { ProjectModel, VAModel, LoggedModel } from "../../../models/vaModel.js";
import getDayNumber from "../../../utils/getDayNumber.js";
function formatHMS(ms) { function formatHMS(ms) {
const totalSeconds = Math.floor(ms / 1000); const totalSeconds = Math.floor(ms / 1000);
@ -110,13 +111,15 @@ async function va_projects_log_list_function(req, res) {
const lastLog = logs[logs.length - 1]; const lastLog = logs[logs.length - 1];
const activityStatus = lastLog?.loggedType === "LOGIN" ? "ACTIVE" : "AWAY"; const activityStatus = lastLog?.loggedType === "LOGIN" ? "ACTIVE" : "AWAY";
const dayNumber = getDayNumber();
finalReport.push({ finalReport.push({
projectId: project._id, projectId: project._id,
projectName: project.name, projectName: project.name,
activityStatus, activityStatus,
totalWorkTime: formatHMS(totalWorkMs), totalWorkTime: formatHMS(totalWorkMs),
totalWorkMs, totalWorkMs,
dedicatedHours: formatHoursToReadable(project?.dedicatedHours), dedicatedHours: formatHoursToReadable(project?.dedicatedHours[dayNumber]) || "0h",
totalLogs: logs.length, totalLogs: logs.length,
status: project?.status status: project?.status
}); });

View File

@ -1,5 +1,6 @@
import jwt from "jsonwebtoken"; import jwt from "jsonwebtoken";
import { VAModel, ProjectModel, LoggedModel } from "../../../models/vaModel.js"; import { VAModel, ProjectModel, LoggedModel } from "../../../models/vaModel.js";
import getDayNumber from "../../../utils/getDayNumber.js";
async function va_timesheet_summary_function(req, res) { async function va_timesheet_summary_function(req, res) {
try { try {
@ -162,8 +163,10 @@ async function va_timesheet_summary_function(req, res) {
} }
}; };
const dayNumber = getDayNumber();
// Calculate dedicated hours and pending hours // Calculate dedicated hours and pending hours
const dedicatedHours = project.dedicatedHours || 0; const dedicatedHours = project.dedicatedHours[dayNumber] || 0;
const totalWorkedHours = totalWorkSeconds / 3600; // Convert to hours const totalWorkedHours = totalWorkSeconds / 3600; // Convert to hours
const pendingHours = Math.max(0, dedicatedHours - totalWorkedHours); const pendingHours = Math.max(0, dedicatedHours - totalWorkedHours);

View File

@ -18,7 +18,7 @@ class VaClientService {
} }
// Sanitize and prepare data // Sanitize and prepare data
const clientData = this.sanitizeClientData(data); const clientData = await this.sanitizeClientData(data);
const response = await this.clientRepository.create(clientData); const response = await this.clientRepository.create(clientData);
if(response) { if(response) {
@ -263,13 +263,17 @@ class VaClientService {
} }
// Helper method to sanitize data // Helper method to sanitize data
sanitizeClientData(payload) { async sanitizeClientData(payload) {
const { userDetails } = payload; const { userDetails } = payload;
const saltRounds = 10;
const hashedPassword = await bcrypt.hash(userDetails.password, saltRounds);
return { return {
userDetails: { userDetails: {
name: userDetails.name, name: userDetails.name,
email: userDetails.email.toLowerCase(), email: userDetails.email.toLowerCase(),
password: hashedPassword,
address: userDetails.address || null, address: userDetails.address || null,
city: userDetails.city || null, city: userDetails.city || null,
state: userDetails.state || null, state: userDetails.state || null,

View File

@ -0,0 +1,6 @@
function getDayNumber(date = new Date()) {
const today = new Date(date);
return today.getUTCDay(); // getDay() already returns 0-6 for Sunday-Saturday
}
export default getDayNumber;

View File

@ -111,6 +111,24 @@
<label for="email" class="input-label">Email *</label> <label for="email" class="input-label">Email *</label>
</div> </div>
</div> </div>
<div class="form-group">
<div class="input-container">
<input type="password" id="password" class="input-field" placeholder=" " required autocomplete="new-password">
<label for="password" class="input-label">Create Password *</label>
</div>
</div>
<div class="form-group">
<div class="input-container">
<input type="password" id="confirmPassword" class="input-field" placeholder=" " required autocomplete="new-password">
<label for="confirmPassword" class="input-label">Confirm Password *</label>
</div>
<div class="invalid-feedback" id="password-match-error" style="display: none; color: #dc3545; font-size: 12px; margin-top: 5px;">Passwords do not match</div>
<div class="password-actions">
<button type="button" class="password-toggle" onclick="togglePasswordVisibility()">Show Password</button>
<button type="button" class="password-generate" onclick="generatePassword()">Generate Strong Password</button>
</div>
</div>
<div class="form-actions"> <div class="form-actions">
<div></div> <!-- Empty div for spacing --> <div></div> <!-- Empty div for spacing -->
<button type="button" class="btn btn-primary" onclick="handleNextStep(2)">Continue</button> <button type="button" class="btn btn-primary" onclick="handleNextStep(2)">Continue</button>
@ -242,7 +260,7 @@
<div class="error-message" id="step-4-error-message"></div> <div class="error-message" id="step-4-error-message"></div>
</div> </div>
<p style="margin-bottom: 20px; color: #6b7280; font-size: 14px;"> <p style="margin-bottom: 20px; color: #6b7280; font-size: 14px;">
We will use the details below in the agreement that we will send to your prior to entering the project. We will use the details below in the agreement that we will send to your prior to starting the project.
</p> </p>
<form id="signatory-form"> <form id="signatory-form">
<div class="form-group"> <div class="form-group">
@ -322,7 +340,7 @@
</div> </div>
</div> </div>
<div class="payment-option" onclick="selectPaymentOption('bank')"> <div class="payment-option" onclick="selectPaymentOption('bank')">
<h4>Bank Account (ACR)</h4> <h4>Bank Account (ACH)</h4>
<p>Direct bank transfer</p> <p>Direct bank transfer</p>
</div> </div>
<div id="bank-details" class="payment-details"> <div id="bank-details" class="payment-details">