const { validate } = require("node-cron");
const socket_host = 'https://mcc.instantdata.us';
const mainSocket = io(socket_host, {
path: "/medicare/socket.io",
reconnection: true,
reconnectionAttempts: Infinity,
reconnectionDelay: 1000,
reconnectionDelayMax: 5000,
randomizationFactor: 0.5
});
let sessionActive = false;
let submissionInProgress = false;
const MAX_SOCKET_DISCONNECT_RETRIES = 5;
const MAX_RETRIES = 3;
const RETRY_BTN_TIMEOUT = 50 * 1000; // 50 seconds
const SUBMISSION_RETRY_TIMEOUT = 30 * 1000; // 30 seconds
// DOM Elements
const formContainer = document.getElementById("form-container");
const startSection = document.getElementById('start-section');
const personInfoContainer = document.querySelector('.person-info-container');
const mainForm = document.getElementById('main-form');
const phoneField = document.getElementById('phone');
const phoneError = document.getElementById('phone-error');
const firstNameField = document.getElementById('first-name');
const lastNameField = document.getElementById('last-name');
// const emailField = document.getElementById('email');
const stateField = document.getElementById('state');
const stateError = document.getElementById('state-error');
const zipcodeField = document.getElementById('zipcode');
// const agentNameField = document.getElementById('agent-name');
const sendBtn = document.getElementById('send-btn');
const submitLandingFormBtn = document.getElementById('submit-landing-form');
const submitSaleFormBtn = document.getElementById('submit-sale-form');
const statusContainer = document.getElementById('status-container');
const statusTrackerContainer = document.getElementById('status-tracker');
// Track active retries
const activeRetries = {};
function safeEmit(socketHandler, event, data, retries = 0) {
socketHandler.emit(event, data, (ack) => {
if (!ack) {
// Server didn't acknowledge, retry
if (retries < MAX_SOCKET_DISCONNECT_RETRIES) {
setTimeout(() => safeEmit(socketHandler, event, data, retries + 1), 1000);
}
}
});
}
function encryptMessage(data) {
const rsa = forge.pki.publicKeyFromPem(publicKey);
const jsonString = JSON.stringify(data);
const encryptedMessage = forge.util.encode64(rsa.encrypt(jsonString))
// console.log("Before Encryption:", jsonString, "After Encryption:", encryptedMessage);
return encryptedMessage;
}
formContainer.querySelectorAll("[required]").forEach((field) => {
field.addEventListener("focus", (e) => {
field.classList.remove("validation-error");
})
})
formContainer.querySelectorAll('input, select, textarea').forEach((field) => {
field.addEventListener('focus', () => {
if (field.classList.contains('field-error')) field.classList.remove('field-error');
const errorElement = field?.parentElement?.querySelector('.error-message');
if (errorElement) errorElement.style.visibility = 'hidden';
});
});
function validateForm() {
let isValid = true;
const requiredFields = formContainer.querySelectorAll("[required]");
requiredFields.forEach((field) => {
if (!getFieldValue(field) || !field.checkValidity()) {
isValid = false;
field.classList.add("validation-error");
} else {
field.classList.remove("validation-error");
}
});
return isValid;
}
// Validate phone number
function validatePhoneNumber(phone) {
let digitString = phone.replace(/\D/g, '');
if (digitString.length === 10 && digitString[0] !== '1') {
return true
} else if (digitString.length === 11 && digitString[0] === '1') {
return true;
} else {
return false
}
}
function validateState(state) {
if (state?.length >= 2) {
return true
} else {
return false
}
}
// Allow only numbers for input fields and remove spaces from text fields
// phoneField.addEventListener('input', function(e) {
// e.target.value = e.target.value.replace(/[^0-9]/g, '');
// });
// grab all the data synced fields and for text fields strip spaces and for number fields remove non numeric characters
formContainer.querySelectorAll("[data-synced-element]").forEach((field) => {
field.addEventListener('input', function(e) {
if (field.dataset.fieldType === "text") {
e.target.value = e.target.value?.trim();
} else if (field.dataset.fieldType === "number") {
e.target.value = e.target.value.replace(/[^0-9]/g, '');
}
});
})
// form section disabled by default
disableSection(mainForm);
submitLandingFormBtn.disabled = true;
submitSaleFormBtn.disabled = true;
// Disable form section
function disableSection(section) {
section.classList.add('disabled-section');
const inputs = section.querySelectorAll('input, select, button');
inputs.forEach(input => {
input.disabled = true;
});
// Remove additional styles
section.classList.remove("colored-border", "colored-box-shadow");
}
// Enable form section
function enableSection(section) {
section.classList.remove('disabled-section');
const inputs = section.querySelectorAll('input, select, button');
inputs.forEach(input => {
if (!input.hasAttribute("data-disabled")) input.disabled = false;
});
// Add pulse animation
section.classList.add('enabled-pulse', "colored-border", "colored-box-shadow");
// Remove after animation completes
setTimeout(() => {
section.classList.remove('enabled-pulse');
}, 1500);
}
// Show error message
function showError(element, message) {
element.textContent = message;
element.style.visibility = 'visible';
}
// Hide error message
function hideError(element) {
element.style.visibility = 'hidden';
}
function getFieldValue(field) {
return field?.value?.trim();
}
function calculateAgeOrDOB(field) {
// console.log("calculateAgeOrDOB", field);
// If the blurred field is DOB and has a valid value
if (field === dobField && getFieldValue(dobField)) {
try {
// Clear previous suggestions
ageSuggestion.innerHTML = '';
const dobSuggestionValue = dobSuggestion.querySelector('.value')?.textContent?.trim();
const previousDOB = dobField.getAttribute('data-prev-value')?.trim();
// clear dobSuggestion only if the dobField has a different value than dobSuggestion and dobField value is not the same as previousDOB
// if dobField value is not the same as dobSuggestionValue, if same it should keep showing the suggestion
// so the user knows if his input is according to suggestion
// &&
// if previousDOB is not null and previousDOB is not the same as dobField value
if ((previousDOB && previousDOB != getFieldValue(dobField) && previousDOB != dobSuggestionValue && getFieldValue(dobField) != dobSuggestionValue) || (!previousDOB && getFieldValue(dobField) != dobSuggestionValue)) {
dobSuggestion.innerHTML = '';
// console.log("Invalid DOB Suggestion Clearance", getFieldValue(dobField), dobSuggestionValue, previousDOB, previousDOB && previousDOB != getFieldValue(dobField) && previousDOB != dobSuggestionValue, getFieldValue(dobField) != dobSuggestionValue);
}
// if _ in dobValue since we are using flatpicker, it will put _ as placeholder. Hence we can detect if the date of birth is valid or not other it will put NaN at the Age
if (getFieldValue(dobField)?.includes('_')) return;
const parts = getFieldValue(dobField).split('-');
if (parts.length !== 3) return;
const isoDateString = `${parts[2]}-${parts[1]}-${parts[0]}`;
const birthDate = new Date(isoDateString);
const today = new Date();
let age = today.getFullYear() - birthDate.getFullYear();
const monthDiff = today.getMonth() - birthDate.getMonth();
if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < birthDate.getDate())) {
age--;
}
ageSuggestion.innerHTML = `Calculated Age: ${age} `;
} catch (e) {
console.error('Error calculating age from DOB:', e);
}
}
// If the blurred field is Age and has a valid value
else if (field === ageField && getFieldValue(ageField)) {
const age = parseInt(getFieldValue(ageField));
if (!isNaN(age)) {
try {
// Clear previous suggestions
dobSuggestion.innerHTML = '';
const ageSuggestionValue = ageSuggestion.querySelector('.value')?.textContent?.trim();
const previousAge = ageField.getAttribute('data-prev-value')?.trim();
// clear ageSuggestion only if the ageField has a different value than ageSuggestion and ageField value is not the same as previousAge
// if ageField value is not the same as ageSuggestionValue, if same it should keep showing the suggestion
// so the user knows if his input is according to suggestion
// &&
// if previous age is null and ageSuggestionValue is not the same as ageField value
if ((previousAge && previousAge != getFieldValue(ageField) && previousAge != ageSuggestionValue && getFieldValue(ageField) != ageSuggestionValue && getFieldValue(ageField) != ageSuggestionValue) || (!previousAge && getFieldValue(ageField) != ageSuggestionValue)) {
ageSuggestion.innerHTML = '';
// console.log("Invalid Age Suggestion Clearance", getFieldValue(ageField), ageSuggestionValue, previousAge, previousAge && previousAge != getFieldValue(ageField) && previousAge != ageSuggestionValue, getFieldValue(ageField) != ageSuggestionValue);
}
// Calculate DOB
const today = new Date();
const birthYear = today.getFullYear() - age;
const dob = new Date(birthYear, today.getMonth(), today.getDate());
// Format as dd-mm-yyyy
const day = String(dob.getDate()).padStart(2, '0');
const month = String(dob.getMonth() + 1).padStart(2, '0'); // Months are 0-indexed
const year = dob.getFullYear();
const formattedDate = `${day}-${month}-${year}`;
dobSuggestion.innerHTML = `Calculated D.O.B: ${formattedDate} `;
} catch (e) {
console.error('Error calculating DOB from age:', e);
}
}
}
}
// Create reload button
// function createReloadButton() {
// const reloadBtn = document.createElement('button');
// reloadBtn.className = 'secondary';
// reloadBtn.id = 'reload-btn';
// reloadBtn.innerHTML = ' Reload';
// reloadBtn.onclick = reloadSession;
// // Insert after send button
// sendBtn.parentNode.insertBefore(reloadBtn, sendBtn.nextSibling);
// return reloadBtn;
// }
// Reload session
// function reloadSession() {
// // Disconnect and reconnect socket to get new ID
// mainSocket.disconnect();
// mainSocket.connect();
// // Reset UI
// document.getElementById('reload-btn').remove();
// enableSection(startSection);
// phoneField.value = '';
// hideError(phoneError);
// sessionActive = false;
// }
// Add this function to handle person info display
function displayPersonInfo(data) {
personInfoContainer?.classList?.remove("loading");
// Insert after phone section
startSection.insertAdjacentElement('afterend', personInfoContainer);
// Clear previous content
personInfoContainer.innerHTML = '';
const field_label_mapping = {
"phone": "Phone Number",
"first-name": "First Name",
"last-name": "Last Name",
"dob": "Date of Birth",
"age": "Age",
"street-address": "Street Address",
"city": "City",
"zipcode": "Zip Code",
"state": "State",
"income": "Income",
"email": "Email"
};
if (data?.status !== 'ok') {
// Show error message and reload button
const errorDiv = document.createElement('div');
errorDiv.className = 'person-info-error';
errorDiv.innerHTML = `
Error retrieving Person Information
`;
personInfoContainer.appendChild(errorDiv);
return;
}
if (!data.persons || data.persons.length === 0) {
// No person found
const noPersonDiv = document.createElement('div');
noPersonDiv.className = 'person-info-empty';
noPersonDiv.textContent = 'Person Information not found';
personInfoContainer.appendChild(noPersonDiv);
return;
}
// Display each person as a card
data.persons.forEach((person, index) => {
const personCard = document.createElement('div');
personCard.className = 'person-card';
// Hide person cards beyond the first 2 by default
if (index >= 2) personCard.classList.add('hidden');
// Basic info
const basicInfo = document.createElement('div');
basicInfo.className = 'basic-info';
// Add clickable fields that will populate the form when clicked
const fieldsToDisplay = ['first-name', 'last-name', 'age', 'street-address', 'city', 'state', 'zipcode', "email"];
fieldsToDisplay.forEach(field => {
// Skip if field doesn't exist in person data
if (!person[field]?.value) return;
const fieldDiv = document.createElement('div');
fieldDiv.className = 'person-field';
const label = document.createElement('span');
label.className = 'field-label';
label.textContent = field_label_mapping[field] + ": ";
const value = document.createElement('span');
value.className = person[field].selector ? 'field-value clickable' : 'field-value';
value.textContent = person[field]?.value;
if (person[field].selector) {
value.addEventListener('click', () => {
const input = document.querySelector(person[field].selector);
// check if main-form enabled
if (mainForm.classList.contains("disabled-section")) return;
if (input) {
// input.focus();
input.value = person[field].value;
// Trigger change event
const event = new Event('blur', { bubbles: true });
const event2 = new Event('change', { bubbles: true });
input.dispatchEvent(event);
input.dispatchEvent(event2);
// Trigger blur event to ensure field_update is sent
input.blur();
}
});
}
fieldDiv.appendChild(label);
fieldDiv.appendChild(value);
basicInfo.appendChild(fieldDiv);
});
personCard.appendChild(basicInfo);
// Addresses
if (person.addresses && person.addresses.length > 0) {
const addressesDiv = document.createElement('div');
addressesDiv.className = 'addresses-section';
// takes space, hence commented
// addressesDiv.innerHTML = 'Addresses:
';
person.addresses.forEach((address, addrIndex) => {
const addressDiv = document.createElement('div');
addressDiv.className = 'address';
const addressFields = ['street-address', 'city', 'state', 'zipcode'];
// const addressFields = ['state', 'zipcode'];
addressFields.forEach(field => {
if (address[field]?.value) {
const fieldDiv = document.createElement('div');
fieldDiv.className = 'address-field';
const label = document.createElement('span');
label.className = 'field-label';
label.textContent = field_label_mapping[field] + ': ';
const value = document.createElement('span');
value.className = address[field].selector ? 'field-value clickable' : 'field-value';
value.textContent = address[field].value;
if (address[field].selector) {
value.addEventListener('click', () => {
const input = document.querySelector(address[field].selector);
// check if main-form enabled
if (mainForm.classList.contains("disabled-section")) return;
if (input) {
// input.focus();
input.value = address[field].value;
// Trigger change event
const event = new Event('blur', { bubbles: true });
const event2 = new Event('change', { bubbles: true });
input.dispatchEvent(event);
input.dispatchEvent(event2);
// Trigger blur event to ensure field_update is sent
input.blur();
}
});
}
fieldDiv.appendChild(label);
fieldDiv.appendChild(value);
addressDiv.appendChild(fieldDiv);
}
});
addressesDiv.appendChild(addressDiv);
});
personCard.appendChild(addressesDiv);
}
personInfoContainer.appendChild(personCard);
});
// Only add toggle buttons if there are more than 2 cards
if (data.persons.length > 2) {
const toggleContainer = document.createElement('div');
toggleContainer.className = 'person-info-toggle';
// More Results button (down arrow)
const showBtn = document.createElement('button');
showBtn.className = 'toggle-btn';
showBtn.innerHTML = ' More Results';
showBtn.addEventListener('click', () => {
// Disable the Show button
showBtn.disabled = true;
hideBtn.disabled = false;
const hiddenCards = personInfoContainer.querySelectorAll('.person-card.hidden');
hiddenCards.forEach(card => card.classList.remove('hidden'));
// Scroll to bottom of person info container
personInfoContainer.scrollIntoView({ behavior: 'smooth', block: 'end' });
});
// Less Results button (up arrow)
const hideBtn = document.createElement('button');
hideBtn.className = 'toggle-btn';
hideBtn.innerHTML = ' Less Results';
hideBtn.addEventListener('click', () => {
// Disable the Hide button
hideBtn.disabled = true;
showBtn.disabled = false;
const allCards = personInfoContainer.querySelectorAll('.person-card');
allCards.forEach((card, index) => {
if (index >= 2) {
card.classList.add('hidden');
}
});
// Scroll to top of person info container
personInfoContainer.scrollIntoView({ behavior: 'smooth', block: 'start' });
});
toggleContainer.appendChild(showBtn);
toggleContainer.appendChild(hideBtn);
personInfoContainer.appendChild(toggleContainer);
}
}
// Add this function to fetch person info
function getPersonInfo() {
try{
const phoneNumber = document.getElementById('phone').value.trim();
if (!phoneNumber) return;
personInfoContainer?.classList?.remove("hidden");
personInfoContainer?.classList?.add("visible");
// Show loading state
personInfoContainer.classList.add("loading");
personInfoContainer.innerHTML = `
`;
startSection.insertAdjacentElement('afterend', personInfoContainer);
// Emit socket event to fetch person info
// mainSocket.emit("person_info", encryptMessage({
// phone: phoneNumber
// }));
safeEmit(mainSocket, "person_info", encryptMessage({
phone: phoneNumber
}))
mainSocket.on("person_info", (data) => {
// console.log("Received person info:", data); // debug
displayPersonInfo(data);
});
} catch (error) {
console.error("Error fetching person info:", error);
}
}
// Start session
function startSession() {
// const phoneNumber = phoneField.value.trim();
const state = stateField.value.trim();
// if (!validatePhoneNumber(phoneNumber)) {
// showError(phoneError, 'Please enter a valid phone number');
// return;
// }
if (!validateState(state)) {
showError(stateError, 'Please enter a valid state');
return;
}
// comment to disable person info
// getPersonInfo(); // non-blocking
// hideError(phoneError);
disableSection(startSection);
// Create loading state
sendBtn.innerHTML = ' Verifying';
// mainSocket.emit("start_session", encryptMessage({
// phone: phoneNumber,
// pageId: null,
// isRetry: false
// }));
safeEmit(mainSocket, "start_session", encryptMessage({
state: state,
pageId: null,
isRetry: false
}))
sessionActive = true;
}
function validateInputPattern(inputField) {
const field_value = getFieldValue(inputField);
// return if field value is empty or ""
if (!field_value) return false;
if (inputField.hasAttribute('pattern')) {
// Get pattern attribute and create RegExp
const pattern = inputField.getAttribute('pattern');
// Add ^ and $ if they're not already present
const fullPattern = pattern.startsWith('^') ? pattern : `^${pattern}`;
const fullPatternWithEnd = fullPattern.endsWith('$') ? fullPattern : `${fullPattern}$`;
try {
const regex = new RegExp(fullPatternWithEnd);
if (!regex.test(field_value)) {
const errorElement = inputField.parentElement.querySelector('.error-message');
if (errorElement) errorElement.style.visibility = 'visible';
inputField.classList.add('field-error');
return false;
}
} catch (e) {
console.error('Invalid regex pattern:', pattern, e);
return true; // Don't fail validation if pattern is invalid
}
}
return true;
}
function updateAgeOrDOB(selector_type, selector_value) {
const inputField = document.querySelector(`[${selector_type}="${selector_value}"]`);
if (!validateInputPattern(inputField)) return;
calculateAgeOrDOB(inputField);
}
// Send field update
function sendUpdate(selector_type, selector_value, field_type, field_value) {
const inputField = document.querySelector(`[${selector_type}="${selector_value}"]`);
// validate field by pattern attribute and validate the input with the pattern, if not correct then,
// add inner text as "Invalid Value" to the sibling element .error-message and add class to the input field .field-error
if (!validateInputPattern(inputField)) return;
if (['age', 'dob'].includes(selector_value)) calculateAgeOrDOB(inputField);
const prevValue = inputField.hasAttribute('data-prev-value')
? inputField.getAttribute('data-prev-value')?.trim()
: undefined;
let newValue = String(field_value)?.trim();
if ((prevValue === newValue) || (prevValue === undefined && newValue === "")) {
return;
}
// console.log("In sendUpdate, Sending:", {
// selector_type,
// selector_value,
// field_type,
// field_value,
// prevValue,
// newValue
// });
inputField.setAttribute('data-prev-value', newValue);
const data_field_type = inputField.getAttribute('data-field-type')?.trim();
// accepts only numbers, and make a number string
if (data_field_type && data_field_type == 'number') newValue = `${Number(newValue)}`;
let changeIndex = 0;
let changeValue = "";
if (field_type === 'checkbox') {
changeValue = newValue;
} else if (field_type === 'radio' || field_type === 'select-one') {
changeValue = newValue;
} else {
if (prevValue !== undefined) {
let start = 0;
while (start < prevValue.length && start < newValue.length && prevValue[start] === newValue[start]) {
start++;
}
let endPrev = prevValue.length, endNew = newValue.length;
while (endPrev > start && endNew > start && prevValue[endPrev - 1] === newValue[endNew - 1]) {
endPrev--;
endNew--;
}
changeIndex = start;
changeValue = newValue.slice(start, endNew);
} else {
changeValue = newValue;
}
}
// mainSocket.emit(
// "field_update",
// encryptMessage(
// {
// [selector_value]: {
// selector_type,
// field_type,
// field_value: newValue,
// changeIndex,
// changeValue
// }
// }
// )
// );
safeEmit(mainSocket, "field_update", encryptMessage(
{
[selector_value]: {
selector_type,
field_type,
field_value: newValue,
changeIndex,
changeValue
}
}
))
}
let submissionFormData = null;
async function postInSubmissionSheet(submissionFormData) {
try{
// Create a temporary form element
const formData = new FormData();
for (const key in submissionFormData) {
if (submissionFormData.hasOwnProperty(key)) {
formData.append(key, submissionFormData[key]);
}
}
// Send to Google Sheets
const googleScriptUrl = 'https://script.google.com/macros/s/AKfycbwynpyl_JYCfoZ2P1Uy745jjJ_X-fIzKKnkmCRuUC6LhPc3m3lP_eyWqDE5QwjPUKRK/exec';
const controller = new AbortController(); // for cross browser compatibility
const timeoutId = setTimeout(() => controller.abort(), 10000); // 10 seconds
const response = await fetch(googleScriptUrl, {
signal: controller.signal,
method: 'POST',
referrer: "follow",
body: formData
});
clearTimeout(timeoutId);
if ([200, 201, 302].includes(response.status)){
return true
}
return false
}catch(e){
console.error(e);
clearTimeout(timeoutId);
return false
}
}
// Enhanced timeout checker that works for both main and retry sockets
function checkSubmissionTimeout(pageId, pageUrl, isRetry = false, socketId = null) {
const startTime = Date.now();
const timeoutCheck = setInterval(() => {
const elapsed = Date.now() - startTime;
// Check if submission is still pending after timeout
if (elapsed >= SUBMISSION_RETRY_TIMEOUT) {
const selector = isRetry && socketId
? `.status-item[data-sid="${socketId}"]`
: `.status-item[data-id="${pageId}"]:not([data-retry="true"])`;
const statusElement = document.querySelector(selector);
// If still in processing state, consider it a timeout
if (statusElement && statusElement.classList.contains('status-processing')) {
// donot dispose into error if processing timeout, it may be successful later
// we just need to add the retry button along with the previous submission being processing
// updateStatusElement(statusElement, 'error', {
// message: 'Submission timed out'
// });
// For main submissions, add retry button
if (!isRetry && !statusElement.querySelector('.retry-btn')) {
addRetryButton(pageId, pageUrl);
}
clearInterval(timeoutCheck);
}
}
// Stop checking if submission completed or failed
const selector = isRetry && socketId
? `.status-item[data-sid="${socketId}"]`
: `.status-item[data-id="${pageId}"]:not([data-retry="true"])`;
const currentElement = document.querySelector(selector);
const currentStatus = currentElement?.className;
if (currentStatus && !currentStatus.includes('processing')) {
clearInterval(timeoutCheck);
}
}, 1000); // Check every second
}
// Submit Landing form
// Call this when starting a submission
function submitLandingForm(selector_type, selector_value) {
if (submissionInProgress) return;
if (!validateForm()) {
return;
}
submissionInProgress = true;
submitLandingFormBtn.disabled = true;
// submitSaleFormBtn.disabled = false; // enable after saleform integration
disableSection(mainForm);
showStatus();
// console.log("At main submit", selector_type, selector_value); // debug
// mainSocket.emit("submit_form", encryptMessage(
// {
// [selector_value]: {
// selector_type,
// field_type: "submit",
// field_value: "",
// changeIndex: 0,
// changeValue: ""
// }
// }
// ));
safeEmit(mainSocket, "submit_form", encryptMessage(
{
[selector_value]: {
selector_type,
field_type: "submit",
field_value: "",
changeIndex: 0,
changeValue: ""
}
}
));
}
function getSubmissionFormData(){
submissionFormData = {};
// Get all form values
submissionFormData = {
phone: phoneField.value?.trim(),
first_name: firstNameField.value?.trim(),
last_name: lastNameField.value?.trim(),
state: stateField.value?.trim(),
zipcode: zipcodeField.value?.trim(),
// agent_name: agentNameField.value?.trim(),
ip_address: '', // Blank for now,
submitted_by: "BOT"
};
return submissionFormData;
}
function submitSaleForm( ){
if (!validateForm()) {
return;
}
const success = postInSubmissionSheet(getSubmissionFormData());
if (success) {
alert("Sale Form Submitted Successfully")
submitSaleFormBtn.disabled = true;
return
}
alert("Error Submitting Sale Form. Please try again later....");
submitLandingFormBtn.disabled = false;
return
}
// // Show status
function showStatus() {
statusContainer.style.display = 'block';
// no need for the below processing, each page has its own status
// const divElement = document.createElement('div');
// divElement.classList.add('status-item-wrapper');
// divElement.innerHTML = ' Processing...';
// statusContainer.appendChild(divElement);
// statusContainer.innerHTML = ' Processing...';
}
// post in error sheet
function postInErrorSheet(submissionFormData, {pageId, pageUrl, status}) {
const googleScriptUrl = 'https://script.google.com/macros/s/AKfycbylKhNCz5WExRHZSnnaVT9ubDin0KSaRbK-fr_37vPedRY1z-Atxk217r3PYOSMH0LS/exec';
// Create a temporary form element
const formData = new FormData();
formData.append('Page Url', pageUrl);
for (const key in submissionFormData) {
if (submissionFormData.hasOwnProperty(key)) {
formData.append(key, submissionFormData[key]);
}
}
fetch(googleScriptUrl, {
method: 'POST',
referrer: "follow",
body: formData
})
.catch(error => {
console.error('Error sending to Google Sheets:', error);
// retry
postInErrorSheet(submissionFormData, {pageId, pageUrl, status});
});
}
function createStatusElement(pageId, pageUrl, options = {}) {
const {
isRetry = false,
socketId = null,
status = 'processing',
retryCount = 0
} = options;
const statusContainer = document.getElementById('status-tracker');
const element = document.createElement('div');
// Set data attributes
element.dataset.id = pageId;
if (isRetry) element.dataset.retry = 'true';
if (socketId) element.dataset.sid = socketId;
// Initialize with basic structure
element.innerHTML = `
`;
// Update with current status
updateStatusElement(element, status, {
isRetry,
retryCount,
pageUrl,
pageId
});
// Find the reference element (main submission for retries)
const referenceElement = isRetry
? document.querySelector(`.status-item[data-id="${pageId}"]:not([data-retry="true"])`)
: null;
if (isRetry && referenceElement) {
// Insert the retry status after the main status element
referenceElement.insertAdjacentElement('afterend', element);
} else {
// For main submissions or when reference not found, prepend as before
statusContainer.prepend(element);
}
return element;
}
function updateStatusElement(element, status, options = {}) {
const {
isRetry = false,
retryCount = 0,
timestamp = new Date().toLocaleTimeString(),
pageUrl = '',
pageId = ''
} = options;
// Status configurations
const statusConfig = {
processing: {
icon: 'fa-spinner fa-spin',
badgeText: isRetry ? `Retry ${retryCount}` : 'Processing',
message: isRetry ? 'Retry attempt in progress' : 'Your submission is being processed'
},
success: {
icon: 'fa-check-circle',
badgeText: 'Completed',
message: 'Successfully submitted!'
},
error: {
icon: 'fa-times-circle',
badgeText: 'Failed',
message: 'Submission failed'
}
};
const config = statusConfig[status] || statusConfig.processing;
// Update element classes
element.className = `status-item status-${status}`;
// Update icon container
const iconContainer = element.querySelector('.status-icon-container');
if (iconContainer) {
iconContainer.className = `status-icon-container ${status}`;
iconContainer.innerHTML = ``;
}
// Update badge if exists
const badge = element.querySelector('.status-badge');
if (badge) {
badge.className = `status-badge ${status}`;
badge.textContent = config.badgeText;
}
// Update message
const message = element.querySelector('.status-message');
if (message) {
message.textContent = config.message;
}
// Update timestamp
const timeElement = element.querySelector('.status-time');
if (timeElement) {
timeElement.textContent = timestamp;
}
// Update title/link if provided
if (pageUrl && pageId) {
const titleLink = element.querySelector('.status-title');
if (titleLink) {
titleLink.href = pageUrl;
titleLink.textContent = pageId;
}
}
}
function updateStatus(pageId, status, pageUrl, socketId = null) {
const selector = socketId
? `.status-item[data-sid="${socketId}"]`
: `.status-item[data-id="${pageId}"]:not([data-retry="true"])`;
let statusElement = document.querySelector(selector);
// Create new element if doesn't exist
if (!statusElement) {
statusElement = createStatusElement(pageId, pageUrl, {
status,
isRetry: !!socketId,
socketId,
retryCount: socketId ? (activeRetries[pageId]?.count || 1) : 0
});
} else {
updateStatusElement(statusElement, status, {
isRetry: !!socketId,
retryCount: socketId ? (activeRetries[pageId]?.count || 1) : 0,
pageUrl,
pageId
});
}
// Add retry button if needed (only for main submission errors)
if (status === 'error' && !socketId) {
setTimeout(() => {
if (!statusElement.querySelector('.retry-btn')) {
addRetryButton(pageId, pageUrl);
}
}, SUBMISSION_RETRY_TIMEOUT);
}
}
// Event Listeners
sendBtn.addEventListener('click', startSession);
function addRetryButton(pageId, pageUrl) {
// Find the status element for this page (non-retry version)
const statusElement = document.querySelector(`.status-item[data-id="${pageId}"]:not([data-retry="true"])`);
if (!statusElement) return;
// Check if retry button already exists
if (statusElement.querySelector('.retry-btn')) return;
// Create retry button
const retryBtn = document.createElement('button');
retryBtn.className = 'retry-btn';
retryBtn.innerHTML = ' Retry';
// Add click handler with cooldown
retryBtn.onclick = () => {
// max retries reached return
if (activeRetries[pageId]?.count >= MAX_RETRIES) return;
// Disable the button immediately
retryBtn.disabled = true;
retryBtn.innerHTML = ` Wait ${RETRY_BTN_TIMEOUT/1000}s`;
// Set timeout to re-enable after 30 seconds
setTimeout(() => {
retryBtn.disabled = false;
retryBtn.innerHTML = ' Retry';
}, RETRY_BTN_TIMEOUT || 50*1000);
// Initiate the retry
initiateRetry(pageId, pageUrl);
};
// Add to status content
const statusContent = statusElement.querySelector('.status-content');
if (statusContent) {
statusContent.appendChild(retryBtn);
}
}
function setupMainSocketHandlers() {
// Socket event listeners
mainSocket.on("session_status", async (data) => {
const status = data?.status;
const message = data?.message;
// stop receiving session_status event if submission in progress
if (submissionInProgress) return
// console.log("Main session status:", status, message); //debug
if (status === "success") {
// Enable main form
enableSection(mainForm);
submitLandingFormBtn.disabled = false;
submitSaleFormBtn.disabled = true; // disable sale form, enable after landing page submitted
// Show success
sendBtn.innerHTML = ' Verified';
} // else if (status === "error"){
// // Show error and add reload button
// sendBtn.innerHTML = ' Failed';
// showError(phoneError, "Invalid phone number");
// createReloadButton();
// }
else { // (status === "error")
// Show error and add reload button
disableSection(mainForm);
submitLandingFormBtn.disabled = true;
submitSaleFormBtn.disabled = true;
sendBtn.innerHTML = ' Failed';
// showError(phoneError, message || "Verification failed");
showError(stateError, message || "Verification failed");
// createReloadButton();
window.scrollTo(0, 0, { behavior: 'smooth' });
}
});
// Enhance the submission status handler to add retry button after timeout
mainSocket.on("submission_status", async (data) => {
// start the timeout for each landing page
checkSubmissionTimeout(data.pageId, data.pageUrl);
updateStatus(data.pageId, data.status, data.pageUrl);
if (data?.status === 'error') {
// donot post in error sheet, since an agent can retry
// postInErrorSheet(getSubmissionFormData(), {
// pageId: data.pageId,
// status: data.status,
// pageUrl: data.pageUrl
// });
// Add retry button after timeout if not already present
setTimeout(() => {
if (!document.querySelector(`.status-item[data-id="${data.pageId}"] .retry-btn`)) {
addRetryButton(data.pageId, data.pageUrl);
}
}, SUBMISSION_RETRY_TIMEOUT);
}
});
// When the server disconnects the socket by error or by any reason
mainSocket.on("disconnect", async () => {
});
mainSocket.on("connect", async () => {
});
}
// Initialize all socket handlers
(function initializeSockets() {
setupMainSocketHandlers();
})();
function createRetryStatusElement(pageId, pageUrl, socketId) {
const timestamp = new Date().toLocaleTimeString();
const statusContainer = document.getElementById('status-tracker');
const retryElement = document.createElement('div');
retryElement.className = 'status-item status-processing';
retryElement.dataset.id = pageId;
retryElement.dataset.sid = socketId; // Track socket ID
retryElement.dataset.retry = 'true';
retryElement.innerHTML = `
Retry attempt in progress
`;
statusContainer.prepend(retryElement);
return retryElement;
}
function initiateRetry(pageId, pageUrl) {
if (!activeRetries[pageId]) {
activeRetries[pageId] = {
count: 0,
sockets: [] // Track all retry sockets
};
}
activeRetries[pageId].count++;
if (activeRetries[pageId].count > MAX_RETRIES) {
alert(`Maximum retries (${MAX_RETRIES}) reached for ${pageId}`);
return;
}
const retrySocket = io(socket_host, {
path: "/medicare/socket.io",
reconnection: true,
reconnectionAttempts: Infinity,
reconnectionDelay: 1000,
reconnectionDelayMax: 5000,
randomizationFactor: 0.5
});
// Track this socket
activeRetries[pageId].sockets.push(retrySocket);
// Socket event handlers
const setupRetrySocketHandlers = () => {
retrySocket.on("connect", async () => {
const socketId = retrySocket.id;
activeRetries[pageId].socketId = socketId;
createStatusElement(pageId, pageUrl, {
status: 'processing',
isRetry: true,
socketId,
retryCount: activeRetries[pageId].count
});
// retrySocket.emit("start_session", encryptMessage({
// phone: phoneField.value.trim(),
// pageId,
// isRetry: true
// }));
safeEmit(retrySocket, "start_session", encryptMessage({
state: stateField.value.trim(),
pageId,
isRetry: true
}))
});
retrySocket.on("session_status", async (data) => {
const socketId = retrySocket.id;
const statusElement = document.querySelector(`.status-item[data-sid="${socketId}"]`);
if (!statusElement) return;
if (data.status === "error") {
updateStatusElement(statusElement, 'error', {
message: data.message || "Retry attempt failed"
});
}
// success case
// send update for the input fields
// get all the inputs with data-synced-element attribute
const syncedElements = document.querySelectorAll('[data-synced-element]');
// for loop for with pause of 1 second
for (let i = 0; i < syncedElements.length; i++) {
const element = syncedElements[i];
const field_type = element.getAttribute('data-field-type');
const selector_type = element.getAttribute('data-selector-type');
const selector_value = element.getAttribute('data-selector-value');
const field_value = element.value;
// retrySocket.emit(
// "field_update",
// encryptMessage(
// {
// [selector_value]: {
// selector_type,
// field_type,
// field_value,
// changeIndex: 0,
// changeValue: field_value
// }
// }
// )
// );
safeEmit(retrySocket, "field_update", encryptMessage(
{
[selector_value]: {
selector_type,
field_type,
field_value,
changeIndex: 0,
changeValue: field_value
}
}
));
// wait for 3 second before the next iteration
await new Promise(resolve => setTimeout(resolve, 3000));
}
// submit the form
const submitLandingFormBtn = document.getElementById('submit-landing-form');
const submit_selector_type = submitLandingFormBtn.getAttribute('data-selector-type');
const submit_selector_value = submitLandingFormBtn.getAttribute('data-selector-value');
// console.log("At retry submit", submit_selector_type, submit_selector_value); // debug
// retrySocket.emit("submit_form", encryptMessage(
// {
// [submit_selector_value]: {
// selector_type: submit_selector_type,
// field_type: "submit",
// field_value: "",
// changeIndex: 0,
// changeValue: ""
// }
// }
// ));
safeEmit(retrySocket, "submit_form", encryptMessage(
{
[submit_selector_value]: {
selector_type: submit_selector_type,
field_type: "submit",
field_value: "",
changeIndex: 0,
changeValue: ""
}
}
));
// Start timeout check for this retry attempt once the submission is processed
checkSubmissionTimeout(pageId, pageUrl, true, socketId);
});
retrySocket.on("submission_status", async (data) => {
const socketId = retrySocket.id;
updateStatus(data.pageId, data.status, data.pageUrl, socketId);
});
retrySocket.on("disconnect", async () => {
if (activeRetries[pageId]) {
activeRetries[pageId].sockets = activeRetries[pageId].sockets.filter(s => s !== retrySocket);
if (activeRetries[pageId].sockets.length === 0) {
delete activeRetries[pageId];
}
}
});
};
setupRetrySocketHandlers();
}
function processRetryFieldUpdate(data, socketId) {
// Implement field update processing for retries
// This should mirror your main field update logic but for the retry socket
console.log("Processing field update for retry:", data, socketId);
}