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 = `
${pageId}
`; // 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 = `
${pageId} Retry ${activeRetries[pageId]?.count || 1} ${timestamp}
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); }