/** * Institution Lookup Widget * Provides UI for searching institutions and populating HubSpot forms * Adapted for Securly website with jQuery support */ class InstitutionLookupWidget { constructor(config = {}) { this.api = config.api; // Initialize Sets for tracking processed institutions and emails this._processedInstitutions = new Set(); this._processedPersonnelEmails = new Set(); this.hubspotFormId = config.hubspotFormId; this.portalId = config.portalId; this.fieldMapping = config.fieldMapping || {}; this.selectedInstitution = null; this.$form = null; // Will store jQuery form reference this.formContext = null; // Will store HubSpot form context // Initialize Sets for tracking processed institutions and emails to prevent duplicate API calls this._processedInstitutions = new Set(); this._processedPersonnelEmails = new Set(); // PRIORITY 3: State name to code mapping (for API calls that require 2-letter codes) // Maps full state names to 2-letter codes as required by Agile-Ed API this.stateNameToCode = { 'alabama': 'AL', 'alaska': 'AK', 'arizona': 'AZ', 'arkansas': 'AR', 'california': 'CA', 'colorado': 'CO', 'connecticut': 'CT', 'delaware': 'DE', 'florida': 'FL', 'georgia': 'GA', 'hawaii': 'HI', 'idaho': 'ID', 'illinois': 'IL', 'indiana': 'IN', 'iowa': 'IA', 'kansas': 'KS', 'kentucky': 'KY', 'louisiana': 'LA', 'maine': 'ME', 'maryland': 'MD', 'massachusetts': 'MA', 'michigan': 'MI', 'minnesota': 'MN', 'mississippi': 'MS', 'missouri': 'MO', 'montana': 'MT', 'nebraska': 'NE', 'nevada': 'NV', 'new hampshire': 'NH', 'new jersey': 'NJ', 'new mexico': 'NM', 'new york': 'NY', 'north carolina': 'NC', 'north dakota': 'ND', 'ohio': 'OH', 'oklahoma': 'OK', 'oregon': 'OR', 'pennsylvania': 'PA', 'rhode island': 'RI', 'south carolina': 'SC', 'south dakota': 'SD', 'tennessee': 'TN', 'texas': 'TX', 'utah': 'UT', 'vermont': 'VT', 'virginia': 'VA', 'washington': 'WA', 'west virginia': 'WV', 'wisconsin': 'WI', 'wyoming': 'WY', 'district of columbia': 'DC', 'washington dc': 'DC', 'dc': 'DC' }; this.init(); } /** * Convert full state name to 2-letter code (for API calls) * @param {string} stateName - Full state name (e.g., "Pennsylvania", "New York") * @returns {string} 2-letter state code (e.g., "PA", "NY") or original value if not found */ getStateCode(stateName) { if (!stateName || typeof stateName !== 'string') { return stateName; } // If already a 2-letter code, return as-is if (stateName.length === 2 && /^[A-Z]{2}$/i.test(stateName)) { return stateName.toUpperCase(); } // Try to find in mapping (case-insensitive) const normalized = stateName.toLowerCase().trim(); const code = this.stateNameToCode[normalized]; if (code) { return code; } return stateName; } /** * Initialize event listeners */ init() { const self = this; // Use jQuery if available, otherwise vanilla JS if (typeof $ !== 'undefined') { $(document).ready(function() { $('#institution-search-btn').on('click', function() { self.handleSearch(); }); $('#institution-search-input').on('keypress', function(e) { if (e.key === 'Enter') { e.preventDefault(); self.handleSearch(); } }); }); } else { const searchBtn = document.getElementById('institution-search-btn'); if (searchBtn) { searchBtn.addEventListener('click', () => this.handleSearch()); } const searchInput = document.getElementById('institution-search-input'); if (searchInput) { searchInput.addEventListener('keypress', (e) => { if (e.key === 'Enter') { this.handleSearch(); } }); } } } /** * Set the HubSpot form jQuery reference * @param {jQuery} $form - jQuery form element */ setForm($form) { this.$form = $form; } /** * Handle search button click */ async handleSearch() { const searchType = document.getElementById('institution-search-type')?.value || 'zip'; let searchValue = document.getElementById('institution-search-input')?.value.trim(); const institutionType = document.querySelector('input[name="institution-type"]:checked')?.value || 'K12'; if (!searchValue) { this.showError('Please enter a search value'); return; } // Extract domain from email if search type is domain but user entered email if (searchType === 'domain' && searchValue.includes('@')) { const emailParts = searchValue.split('@'); if (emailParts.length === 2) { searchValue = emailParts[1]; } } // Extract domain from email if search type is email (use full email) // But if they want to search by domain, extract it if (searchType === 'email' && !searchValue.includes('@')) { this.showError('Please enter a valid email address (e.g., teacher@philasd.org)'); return; } this.showLoading(true); this.hideError(); this.hideResults(); try { let results; switch (searchType) { case 'zip': results = await this.api.searchByZip(institutionType, searchValue); break; case 'email': results = await this.api.searchByEmail(searchValue); break; case 'domain': results = await this.api.searchByDomain(searchValue); break; default: throw new Error('Invalid search type'); } this.displayResults(results); } catch (error) { // Provide more helpful error messages let errorMessage = `Search failed: ${error.message}`; if (error.message.includes('Failed to fetch') || error.message.includes('CSP') || error.message.includes('Content Security Policy')) { errorMessage = 'Network error: Unable to reach Agile-Ed API. This may be a Content Security Policy (CSP) issue. Please contact your administrator to add https://lookupapi.agile-ed.com to the CSP connect-src directive.'; } else if (error.message.includes('Network error')) { errorMessage = error.message; // Use the detailed message from API } this.showError(errorMessage); } finally { this.showLoading(false); } } /** * Display search results * @param {Object|Array} results - API response */ displayResults(results) { const resultsContainer = document.getElementById('institution-results-list'); if (!resultsContainer) return; resultsContainer.innerHTML = ''; // Handle different response formats let institutions = []; if (Array.isArray(results)) { institutions = results; } else if (results.data && Array.isArray(results.data)) { institutions = results.data; } else if (results) { institutions = [results]; } if (institutions.length === 0) { resultsContainer.innerHTML = '

No institutions found. Please try a different search.

'; document.getElementById('institution-search-results').style.display = 'block'; return; } // Log the first result to help debug field names if (institutions.length > 0) { } institutions.forEach((institution, index) => { const div = document.createElement('div'); div.className = 'institution-result'; div.dataset.index = index; div.style.cssText = 'padding: 15px; margin: 10px 0; border: 1px solid #ddd; border-radius: 5px; cursor: pointer; transition: all 0.2s; background: #fff;'; // Try multiple possible field names for institution name // Check all possible variations and nested objects let name = institution.name || institution.institutionName || institution.buildingName || institution.building_name || institution.schoolName || institution.school_name || institution.districtName || institution.district_name || institution.title || institution.displayName || institution.display_name || institution.BuildingName || institution.Building_Name || institution.InstitutionName || institution.Institution_Name || (institution.building && institution.building.name) || (institution.district && institution.district.name) || (institution.Building && institution.Building.name) || (institution.District && institution.District.name); // If still not found, try to find any field that might contain a name if (!name || name === '') { // Look for any field containing "name" (case insensitive) for (const key in institution) { if (key.toLowerCase().includes('name') && institution[key] && typeof institution[key] === 'string') { name = institution[key]; break; } } } // Final fallback if (!name || name === '') { name = 'Unknown Institution'; } // Try multiple possible field names for address const address = institution.address || institution.streetAddress || institution.street_address || institution.street || institution.street1 || (institution.building && institution.building.address) || ''; const city = institution.city || (institution.building && institution.building.city) || ''; const state = institution.state || institution.stateCode || institution.state_code || (institution.building && institution.building.state) || ''; const zip = institution.zip || institution.zipCode || institution.zip_code || institution.postalCode || institution.postal_code || (institution.building && institution.building.zip) || ''; div.innerHTML = ` ${name} ${address ? `${address}` : ''} ${city || state || zip ? `${city}${city && state ? ', ' : ''}${state} ${zip}` : ''} `; div.addEventListener('click', () => this.selectInstitution(institution, index)); div.addEventListener('mouseenter', () => { if (this.selectedInstitution?.index !== index) { div.style.backgroundColor = '#f5f5f5'; div.style.borderColor = '#007bff'; } }); div.addEventListener('mouseleave', () => { if (this.selectedInstitution?.index !== index) { div.style.backgroundColor = '#fff'; div.style.borderColor = '#ddd'; } }); resultsContainer.appendChild(div); }); document.getElementById('institution-search-results').style.display = 'block'; } /** * Handle institution selection * @param {Object} institution - Selected institution data * @param {number} index - Index in results array */ selectInstitution(institution, index) { this.selectedInstitution = { ...institution, index }; // populateHubSpotForm is now async, but we don't need to await it here // (it will handle its own async operations) this.populateHubSpotForm(institution).catch(error => { }); this.highlightSelected(index); } /** * Populate HubSpot form fields * @param {Object} institution - Institution data */ async populateHubSpotForm(institution) { if (!this.$form || !this.$form.length) { // FIX #2: Form not ready yet, store for later in namespaced global if (typeof window !== 'undefined') { if (!window.SecurlyForm) { window.SecurlyForm = {}; } window.SecurlyForm.pendingInstitutionData = institution; } return; } // Check if state is missing - check all possible locations and naming variations // IMPORTANT: API uses "mailingState" field name! const hasState = institution.mailingState || // API field name institution.state || institution.State || institution.stateCode || institution.StateCode || institution.state_code || (institution.building && (institution.building.state || institution.building.State || institution.building.mailingState)) || (institution.district && (institution.district.state || institution.district.State || institution.district.mailingState)) || (institution.parent && (institution.parent.state || institution.parent.State || institution.parent.mailingState)) || (institution.Parent && (institution.Parent.state || institution.Parent.State || institution.Parent.mailingState)) || (institution.District && (institution.District.state || institution.District.State || institution.District.mailingState)); // OPTIMIZATION: Populate form immediately with first response, then enhance with full details in parallel // This makes the form feel much faster - user sees results immediately let finalInstitution = institution; // Check if personnel data (including title) is already in the institution object // This happens when Personnel API was called BEFORE populateHubSpotForm (e.g., in email search flow) const hasPersonnelData = institution.personnelTitle || institution.personnelFirstName || institution.personnelLastName; if (hasPersonnelData) { } // Get email from the original search context if available let emailForNameExtraction = null; // FIX #2: Try to get email from namespaced global variable if (typeof window !== 'undefined' && window.SecurlyForm && window.SecurlyForm.lastSearchedEmail) { emailForNameExtraction = window.SecurlyForm.lastSearchedEmail; } else { // FALLBACK: Try to get email directly from the form field if (this.$form && this.$form.length) { const emailField = this.$form.find('input[name="email"], input[type="email"], [name="email"]').first(); if (emailField.length) { const emailValue = emailField.val(); if (emailValue && emailValue.includes('@')) { emailForNameExtraction = emailValue; // Also store it for future use if (typeof window !== 'undefined') { if (!window.SecurlyForm) { window.SecurlyForm = {}; } window.SecurlyForm.lastSearchedEmail = emailValue; } } } } } // Also check if email is in the institution object (unlikely but possible) if (!emailForNameExtraction && institution.email) { emailForNameExtraction = institution.email; } // OPTIMIZATION: Start form population immediately (don't wait for additional API calls) const fields = this.mapInstitutionToFormFields(institution, emailForNameExtraction); // FIX: Prevent duplicate API calls - track which institutions we've already fetched if (!this._processedInstitutions) { this._processedInstitutions = new Set(); } // Determine institution type - check multiple possible fields // IMPORTANT: buildingType values ("DIST", "BLDG") are NOT valid for getByUid API calls // The API expects: "K12", "ECC", "HE", or "ALL" // So we need to map buildingType to a valid type let institutionType = institution.type || institution.institutionType; // If we have buildingType but no type, map it to K12 (most common case) if (!institutionType && institution.buildingType) { // buildingType "DIST" or "BLDG" should map to "K12" for API calls if (institution.buildingType === 'DIST' || institution.buildingType === 'BLDG') { institutionType = 'K12'; } else { // For other buildingType values, default to K12 institutionType = 'K12'; } } // Default to K12 if still not determined if (!institutionType) { institutionType = 'K12'; } const institutionKey = `${institution.instUid || institution.uid || 'unknown'}_${institutionType}`; const alreadyProcessed = this._processedInstitutions.has(institutionKey); // Make parallel API calls for additional data (non-blocking) // 1. getByUid - for full institution details (requires type and instUid) // 2. getPersonnelByEmail - for title/job title (only needs email) // 3. getBuildingInDistrict - for district student count (requires instUid) // Call 2: Get personnel data (title/job title) from email // This call is INDEPENDENT - it only needs the email, not institution type // IMPORTANT: Personnel/PersonnelByEmail/{email} is a SEPARATE API call from Institution API // The Institution API does NOT return personnel data like Title // We MUST call the Personnel API separately to get the title // Try to get email from multiple sources const emailForPersonnel = emailForNameExtraction || (window.SecurlyForm && window.SecurlyForm.lastSearchedEmail) || ''; // Only call Personnel API if personnel data is NOT already in institution object if (!hasPersonnelData && emailForPersonnel && emailForPersonnel.includes('@')) { // Prevent duplicate Personnel API calls for the same email if (this._processedPersonnelEmails.has(emailForPersonnel)) { // Don't return - form population should continue } else { this._processedPersonnelEmails.add(emailForPersonnel); // Call Personnel API - this is separate from the Institution API // This call does NOT depend on institution type or instUid this.api.getPersonnelByEmail(emailForPersonnel) .then((personnelData) => { if (personnelData) { // Handle array response (in case API returns array) let actualData = personnelData; if (Array.isArray(personnelData)) { if (personnelData.length > 0) { actualData = personnelData[0]; } else { return; } } // Extract firstName and lastName from Personnel API (more accurate than email parsing) const personnelFirstName = actualData.firstName || actualData.FirstName || actualData.first_name || ''; const personnelLastName = actualData.lastName || actualData.LastName || actualData.last_name || ''; // Extract title field - API documentation confirms field name is "title" // According to API docs: Personnel/PersonnelByEmail/{email} returns { title: "Superintendent", ... } const title = actualData.title || actualData.Title || actualData.jobTitle || actualData.job_title || actualData.position || actualData.Position || actualData.JobTitle || actualData.role || actualData.Role || actualData.designation || actualData.Designation || ''; if (!title) { } // Store title in institution object for use in mapInstitutionToFormFields // IMPORTANT: Title ONLY comes from Personnel API, which is ONLY called when email is found if (title) { // Store in institution object so it's available for form mapping institution.personnelTitle = title; // Try multiple methods to update the field const updateJobTitle = () => { // Method 1: Direct jQuery selector let jobtitleField = this.$form.find('input[name="jobtitle"], textarea[name="jobtitle"], select[name="jobtitle"], [name="jobtitle"]'); if (jobtitleField.length) { const currentValue = jobtitleField.val(); if (!currentValue || currentValue === '' || currentValue === 'HIDDEN' || currentValue.toLowerCase() === 'hidden') { jobtitleField.val(title); jobtitleField.trigger('change'); jobtitleField.trigger('input'); if (jobtitleField[0]) { jobtitleField[0].dispatchEvent(new Event('change', { bubbles: true })); jobtitleField[0].dispatchEvent(new Event('input', { bubbles: true })); } return true; } else { return true; } } // Method 2: Use updateFieldValue helper this.updateFieldValue('jobtitle', title); // Check if it worked jobtitleField = this.$form.find('input[name="jobtitle"], textarea[name="jobtitle"], select[name="jobtitle"], [name="jobtitle"]'); if (jobtitleField.length && jobtitleField.val() === title) { return true; } // Method 3: Try HubSpot Forms API if available if (typeof window !== 'undefined' && window.hbspt && window.hbspt.forms) { try { // Get the form instance const formElement = this.$form[0]; if (formElement) { // Try to find the form context const formId = formElement.getAttribute('data-form-id') || formElement.closest('[data-form-id]')?.getAttribute('data-form-id'); if (formId) { // HubSpot forms API might be available const formInstance = window.hbspt.forms.get(formId); if (formInstance) { formInstance.setFieldValue('jobtitle', title); return true; } } } } catch (e) { } } // Method 4: Try all possible variations const variations = ['jobtitle', 'job_title', 'jobTitle', 'JobTitle', 'title', 'Title', 'job-title']; for (const variation of variations) { const field = this.$form.find(`[name="${variation}"], [id="${variation}"], [data-name="${variation}"]`); if (field.length) { const currentValue = field.val(); if (!currentValue || currentValue === '' || currentValue === 'HIDDEN') { field.val(title); field.trigger('change'); field.trigger('input'); if (field[0]) { field[0].dispatchEvent(new Event('change', { bubbles: true })); field[0].dispatchEvent(new Event('input', { bubbles: true })); } return true; } } } return false; }; // Wait a bit for form to be ready, then try all methods setTimeout(() => { updateJobTitle(); }, 500); } else { } } else { } }) .catch((error) => { }); } } else if (!hasPersonnelData) { } // Call 1 & 3: Get full institution details and district student count // These require instUid and type if (institution.instUid && institutionType && !alreadyProcessed) { // Mark as processed to prevent duplicate calls this._processedInstitutions.add(institutionKey); // Call 1: Get full institution details this.api.getByUid(institutionType, institution.instUid) .then((fullDetails) => { if (fullDetails) { this.enhanceFormWithFullDetails(fullDetails); } else { } }) .catch((error) => { this._processedInstitutions.delete(institutionKey); }); // Call 3: Get building in district (for district student count) // Building/K12/BldgInDist/{UID} returns districtStudents field // This works for both buildings and districts - always get district student count if (institution.instUid && (institutionType === 'K12' || institution.buildingType === 'BLDG' || institution.buildingType === 'DIST')) { this.api.getBuildingInDistrict(institution.instUid) .then((buildingData) => { if (buildingData) { // Handle array response (Building/K12/BldgInDist can return array) let finalData = buildingData; let buildingStudentsValue = null; let districtStudentsValue = null; if (Array.isArray(buildingData) && buildingData.length > 0) { // Find matching building by instUid const matchingBuilding = buildingData.find(b => (b.instUid && b.instUid.toString() === institution.instUid.toString()) || (b.uid && b.uid.toString() === institution.instUid.toString()) ); // Find district data (first item or item with parentUid === instUid) const districtData = buildingData.find(b => b.parentUid && b.parentUid.toString() === institution.instUid.toString() ) || buildingData[0]; // Combine: prioritize matching building, then district data finalData = matchingBuilding || districtData || buildingData[0]; // IMPORTANT: Search through ALL items in array to find buildingStudents and districtStudents // They might be in different items (e.g., districtStudents in district item, buildingStudents in building item) for (const item of buildingData) { // Get districtStudents from any item that has it (prioritize non-null values) if (item.districtStudents !== null && item.districtStudents !== undefined) { if (districtStudentsValue === null || districtStudentsValue === undefined) { districtStudentsValue = item.districtStudents; } } // Get buildingStudents from any item that has it (prioritize non-null values) if (item.buildingStudents !== null && item.buildingStudents !== undefined) { if (buildingStudentsValue === null || buildingStudentsValue === undefined) { buildingStudentsValue = item.buildingStudents; } } } } // Merge ALL fields from buildingData into institution object // This ensures address, city, zip, phone, and other fields are available Object.assign(institution, finalData); // districtStudents field contains the district's student count // Use value from array search if found, otherwise use finalData const districtStudentCount = districtStudentsValue !== null ? districtStudentsValue : finalData.districtStudents || finalData.district_students || finalData.districtStudentCount; // buildingStudents field contains the building's student count // Use value from array search if found, otherwise use finalData const buildingStudentCount = buildingStudentsValue !== null ? buildingStudentsValue : finalData.buildingStudents || finalData.building_students || finalData.buildingStudentCount; if (districtStudentCount !== null && districtStudentCount !== undefined) { // Convert to string to ensure proper field population const districtCountValue = String(districtStudentCount); // Update student count fields using field mapping keys this.updateFieldValue('student_count', districtCountValue); this.updateFieldValue('securly_student_count', districtCountValue); } else { } // Always update buildingStudents if we have a value (including 0, which is valid) if (buildingStudentCount !== null && buildingStudentCount !== undefined) { // Convert to string to ensure proper field population const buildingCountValue = String(buildingStudentCount); // Update building student count field this.updateFieldValue('building_students', buildingCountValue); // Debug log to verify value is being set if (DEBUG_MODE && window.console) { console.log('Building students count populated:', { buildingStudentCount: buildingStudentCount, buildingCountValue: buildingCountValue, fieldMapping: this.fieldMapping.building_students }); } } else { // Debug log if value is missing if (DEBUG_MODE && window.console) { console.warn('Building students count not found in API response:', { finalData: finalData, buildingStudentsValue: buildingStudentsValue, isArray: Array.isArray(buildingData), arrayLength: Array.isArray(buildingData) ? buildingData.length : 0 }); } } // Update phone field if available from Building API const phoneValue = finalData.phone || finalData.phoneNumber || finalData.phone_number || finalData.telephone || (finalData.building && (finalData.building.phone || finalData.building.phoneNumber)); if (phoneValue !== null && phoneValue !== undefined && phoneValue !== '') { // Convert to string to ensure proper field population const phoneStringValue = String(phoneValue); // Update phone field using field mapping key this.updateFieldValue('phone', phoneStringValue); } // Update file_type_text (Type of School: "Public" or "Private") if available from Building API const fileTypeTextValue = finalData.fileTypeText || finalData.file_type_text || finalData.FileTypeText || (finalData.building && finalData.building.fileTypeText); if (fileTypeTextValue !== null && fileTypeTextValue !== undefined && fileTypeTextValue !== '') { // Convert to string to ensure proper field population const fileTypeTextStringValue = String(fileTypeTextValue); // Update file_type_text field using field mapping key (maps to connectlink_type_of_school) this.updateFieldValue('file_type_text', fileTypeTextStringValue); // Also update institution_type (alternative mapping) this.updateFieldValue('institution_type', fileTypeTextStringValue); } // Update building_type (Building/District) if available from Building API // Note: buildingType comes as "BLDG" or "DIST" from API, needs conversion const buildingTypeValue = finalData.buildingType || finalData.building_type || finalData.BuildingType; if (buildingTypeValue !== null && buildingTypeValue !== undefined && buildingTypeValue !== '') { // Convert "BLDG" -> "Building", "DIST" -> "District" let buildingTypeString = String(buildingTypeValue); if (buildingTypeString.toUpperCase() === 'BLDG') { buildingTypeString = 'Building'; } else if (buildingTypeString.toUpperCase() === 'DIST') { buildingTypeString = 'District'; } // Update building_type field using field mapping key (maps to connectlink_institution_type) this.updateFieldValue('building_type', buildingTypeString); } if (!districtStudentCount) { // Method 2: Alternative approach - use DistInState to find district, then BldgInDist // As per API provider: "If you use Building/K12/DistInState/{state} call to obtain the UID, // then the Building/K12/BldgInDist/{UID} call can return the additional details." const stateCode = this.getStateCode(institution.mailingState || institution.state || institution.stateCode || institution.State); if (stateCode && institution.institutionNameProper) { this.api.getDistrictsInState(stateCode) .then((districts) => { if (districts && Array.isArray(districts) && districts.length > 0) { // Find matching district by name or UID const matchingDistrict = districts.find(dist => dist.instUid === institution.instUid || dist.uid === institution.instUid || (dist.institutionNameProper && institution.institutionNameProper && dist.institutionNameProper.toLowerCase() === institution.institutionNameProper.toLowerCase()) ); if (matchingDistrict && matchingDistrict.instUid) { return this.api.getBuildingInDistrict(matchingDistrict.instUid); } else { return null; } } else { return null; } }) .then((districtData) => { if (districtData) { const districtStudentCount = districtData.districtStudents || districtData.district_students || districtData.districtStudentCount; const buildingStudentCount = districtData.buildingStudents || districtData.building_students || districtData.buildingStudentCount; if (districtStudentCount) { this.updateFieldValue('student_count', districtStudentCount); this.updateFieldValue('securly_student_count', districtStudentCount); } if (buildingStudentCount) { this.updateFieldValue('building_students', buildingStudentCount); } // Update phone field if available from district data const districtPhoneValue = districtData.phone || districtData.phoneNumber || districtData.phone_number || districtData.telephone; if (districtPhoneValue !== null && districtPhoneValue !== undefined && districtPhoneValue !== '') { this.updateFieldValue('phone', String(districtPhoneValue)); } // Update file_type_text if available from district data const districtFileTypeText = districtData.fileTypeText || districtData.file_type_text || districtData.FileTypeText; if (districtFileTypeText !== null && districtFileTypeText !== undefined && districtFileTypeText !== '') { const fileTypeTextString = String(districtFileTypeText); this.updateFieldValue('file_type_text', fileTypeTextString); this.updateFieldValue('institution_type', fileTypeTextString); } // Update building_type if available from district data const districtBuildingType = districtData.buildingType || districtData.building_type || districtData.BuildingType; if (districtBuildingType !== null && districtBuildingType !== undefined && districtBuildingType !== '') { let buildingTypeString = String(districtBuildingType); if (buildingTypeString.toUpperCase() === 'BLDG') { buildingTypeString = 'Building'; } else if (buildingTypeString.toUpperCase() === 'DIST') { buildingTypeString = 'District'; } this.updateFieldValue('building_type', buildingTypeString); } if (!districtStudentCount) { } } }) .catch((error) => { }); } else { } } } }) .catch((error) => { // Fallback: Try alternative method if direct call fails // As per API provider: "If you use Building/K12/DistInState/{state} call to obtain the UID, // then the Building/K12/BldgInDist/{UID} call can return the additional details." const stateCode = this.getStateCode(institution.mailingState || institution.state || institution.stateCode || institution.State); if (stateCode && institution.institutionNameProper) { this.api.getDistrictsInState(stateCode) .then((districts) => { if (districts && Array.isArray(districts) && districts.length > 0) { const matchingDistrict = districts.find(dist => dist.instUid === institution.instUid || dist.uid === institution.instUid || (dist.institutionNameProper && institution.institutionNameProper && dist.institutionNameProper.toLowerCase() === institution.institutionNameProper.toLowerCase()) ); if (matchingDistrict && matchingDistrict.instUid) { return this.api.getBuildingInDistrict(matchingDistrict.instUid); } return null; } return null; }) .then((districtData) => { if (districtData) { const districtStudentCount = districtData.districtStudents || districtData.district_students || districtData.districtStudentCount; const buildingStudentCount = districtData.buildingStudents || districtData.building_students || districtData.buildingStudentCount; if (districtStudentCount) { this.updateFieldValue('student_count', districtStudentCount); this.updateFieldValue('securly_student_count', districtStudentCount); } if (buildingStudentCount) { this.updateFieldValue('building_students', buildingStudentCount); } // Update phone field if available from district data const districtPhoneValue = districtData.phone || districtData.phoneNumber || districtData.phone_number || districtData.telephone; if (districtPhoneValue !== null && districtPhoneValue !== undefined && districtPhoneValue !== '') { this.updateFieldValue('phone', String(districtPhoneValue)); } // Update file_type_text if available from district data const districtFileTypeText = districtData.fileTypeText || districtData.file_type_text || districtData.FileTypeText; if (districtFileTypeText !== null && districtFileTypeText !== undefined && districtFileTypeText !== '') { const fileTypeTextString = String(districtFileTypeText); this.updateFieldValue('file_type_text', fileTypeTextString); this.updateFieldValue('institution_type', fileTypeTextString); } // Update building_type if available from district data const districtBuildingType = districtData.buildingType || districtData.building_type || districtData.BuildingType; if (districtBuildingType !== null && districtBuildingType !== undefined && districtBuildingType !== '') { let buildingTypeString = String(districtBuildingType); if (buildingTypeString.toUpperCase() === 'BLDG') { buildingTypeString = 'Building'; } else if (buildingTypeString.toUpperCase() === 'DIST') { buildingTypeString = 'District'; } this.updateFieldValue('building_type', buildingTypeString); } } }) .catch((error) => { }); } }); } } else if (alreadyProcessed) { } // Debug: Check if country and state are in fields const countryFieldName = this.fieldMapping.country; const stateFieldName = this.fieldMapping.state; // Method 1: Try using HubSpot Forms API (recommended for iframe forms) // HubSpot provides a way to set field values using their API try { // Try to use HubSpot's form context from the callback if (this.formContext && this.formContext.data && this.formContext.data.fields) { const formFields = this.formContext.data.fields; // Try to set values using HubSpot's form context Object.keys(fields).forEach((hubspotFieldName) => { const value = fields[hubspotFieldName]; if (!value) return; // Find the field in HubSpot's field array const field = formFields.find(f => f && (f.name === hubspotFieldName || f.name === hubspotFieldName.toLowerCase() || f.name === hubspotFieldName.toUpperCase()) ); if (field) { try { field.value = value; } catch (e) { } } }); } // Also try form element methods if (window.hbspt && this.$form && this.$form.length) { const formElement = this.$form[0]; // Try to find the form's data object if (formElement && formElement.hsFormInstance) { const formInstance = formElement.hsFormInstance; // Try to set values using HubSpot's API Object.keys(fields).forEach((hubspotFieldName) => { const value = fields[hubspotFieldName]; if (value && formInstance.setFieldValue) { try { formInstance.setFieldValue(hubspotFieldName, value); } catch (e) { } } }); } } } catch (e) { } // Method 2: Direct DOM manipulation (works if form is in same origin or accessible) // IMPORTANT: Populate country FIRST, then wait for state field to appear, then populate state // Note: countryFieldName and stateFieldName are already defined above (lines 300-301) // Get values from mapped fields - check both the mapped key and the original key let countryValue = fields[countryFieldName]; let stateValue = fields[stateFieldName]; // If values are undefined, they weren't in the mapped object - this shouldn't happen but let's handle it if (countryValue === undefined && countryFieldName) { // Try to get it from defaultMapping directly const defaultMapping = this.mapInstitutionToFormFields(institution); countryValue = defaultMapping[countryFieldName] || ''; } if (stateValue === undefined && stateFieldName) { // Try to get it from defaultMapping directly const defaultMapping = this.mapInstitutionToFormFields(institution); stateValue = defaultMapping[stateFieldName] || ''; } // Helper function to populate a single field const populateSingleField = (hubspotFieldName, value, waitForDependent = false) => { return new Promise((resolve) => { if (!value) { resolve(false); return; } // Try multiple selectors and field name variations const selectors = [ // Standard field name formats `input[name="${hubspotFieldName}"]`, `input[name="${hubspotFieldName.toLowerCase()}"]`, `input[name="${hubspotFieldName.toUpperCase()}"]`, // Field ID formats `input[id="${hubspotFieldName}"]`, `input[id="${hubspotFieldName.toLowerCase()}"]`, // Data attribute formats (HubSpot sometimes uses these) `input[data-name="${hubspotFieldName}"]`, `input[data-field="${hubspotFieldName}"]`, // Other input types `textarea[name="${hubspotFieldName}"]`, `select[name="${hubspotFieldName}"]`, // Direct ID `#${hubspotFieldName}`, `#${hubspotFieldName.toLowerCase()}`, // Generic name attribute `[name="${hubspotFieldName}"]`, // Try common variations for school/district name hubspotFieldName === 'company' ? [ 'input[name*="school"]', 'input[name*="School"]', 'input[name*="district"]', 'input[name*="District"]', 'input[name*="institution"]', 'input[name*="Institution"]', 'input[placeholder*="school"]', 'input[placeholder*="School"]', 'input[placeholder*="district"]', 'input[placeholder*="District"]' ] : [], // Try common variations for country (hubspotFieldName === 'country' || hubspotFieldName === 'country__new_') ? [ 'select[name="country"]', 'select[name="country__new_"]', 'select[name*="country"]', 'select[name*="Country"]' ] : [], // Try common variations for state (conditional field names based on country) (hubspotFieldName === 'state' || hubspotFieldName === 'states' || hubspotFieldName === 'us_states') ? [ 'select[name="states"]', // US states field 'select[name="ca_provinces"]', // Canadian provinces field 'select[name="uk_regions"]', // UK regions field 'select[name="us_states"]', // Alternative US states field name 'select[name*="state"]', 'select[name*="State"]', 'select[name*="province"]', 'select[name*="Province"]', 'select[name*="region"]', 'select[name*="Region"]' ] : [] ].flat(); let fieldFound = false; for (const selector of selectors) { try { const field = this.$form.find(selector); if (field.length) { // Set the value const oldValue = field.val(); // Special handling for select/dropdown fields if (field.is('select')) { // For dropdowns, try multiple matching strategies let optionFound = false; // State code to full name mapping (for state fields) let stateCodeToName = {}; let fullStateName = null; if (hubspotFieldName === 'state' || hubspotFieldName === 'us_states') { stateCodeToName = { 'al': 'alabama', 'ak': 'alaska', 'az': 'arizona', 'ar': 'arkansas', 'ca': 'california', 'co': 'colorado', 'ct': 'connecticut', 'de': 'delaware', 'fl': 'florida', 'ga': 'georgia', 'hi': 'hawaii', 'id': 'idaho', 'il': 'illinois', 'in': 'indiana', 'ia': 'iowa', 'ks': 'kansas', 'ky': 'kentucky', 'la': 'louisiana', 'me': 'maine', 'md': 'maryland', 'ma': 'massachusetts', 'mi': 'michigan', 'mn': 'minnesota', 'ms': 'mississippi', 'mo': 'missouri', 'mt': 'montana', 'ne': 'nebraska', 'nv': 'nevada', 'nh': 'new hampshire', 'nj': 'new jersey', 'nm': 'new mexico', 'ny': 'new york', 'nc': 'north carolina', 'nd': 'north dakota', 'oh': 'ohio', 'ok': 'oklahoma', 'or': 'oregon', 'pa': 'pennsylvania', 'ri': 'rhode island', 'sc': 'south carolina', 'sd': 'south dakota', 'tn': 'tennessee', 'tx': 'texas', 'ut': 'utah', 'vt': 'vermont', 'va': 'virginia', 'wa': 'washington', 'wv': 'west virginia', 'wi': 'wisconsin', 'wy': 'wyoming', 'dc': 'district of columbia' }; fullStateName = stateCodeToName[value.toLowerCase()]; } // Strategy 1: Try exact value match (for both code and full name if state) const valuesToTry = [value]; if (fullStateName) { valuesToTry.push(fullStateName); valuesToTry.push(fullStateName.charAt(0).toUpperCase() + fullStateName.slice(1)); // Capitalized } for (const tryValue of valuesToTry) { if (field.find(`option[value="${tryValue}"]`).length) { field.val(tryValue); optionFound = true; break; } } if (!optionFound) { // Strategy 2: Try case-insensitive value match (for both code and full name if state) field.find('option').each(function() { const optionValue = $(this).attr('value'); if (optionValue) { const optionValueLower = optionValue.toLowerCase(); for (const tryValue of valuesToTry) { if (optionValueLower === tryValue.toLowerCase()) { field.val(optionValue); optionFound = true; return false; // break } } } }); } // Strategy 3: Try matching by option text (for country/state fields) if (!optionFound && (hubspotFieldName === 'country' || hubspotFieldName === 'country__new_' || hubspotFieldName === 'state' || hubspotFieldName === 'states' || hubspotFieldName === 'us_states')) { const valueLower = value.toLowerCase(); // State code to full name mapping const stateCodeToName = { 'al': 'alabama', 'ak': 'alaska', 'az': 'arizona', 'ar': 'arkansas', 'ca': 'california', 'co': 'colorado', 'ct': 'connecticut', 'de': 'delaware', 'fl': 'florida', 'ga': 'georgia', 'hi': 'hawaii', 'id': 'idaho', 'il': 'illinois', 'in': 'indiana', 'ia': 'iowa', 'ks': 'kansas', 'ky': 'kentucky', 'la': 'louisiana', 'me': 'maine', 'md': 'maryland', 'ma': 'massachusetts', 'mi': 'michigan', 'mn': 'minnesota', 'ms': 'mississippi', 'mo': 'missouri', 'mt': 'montana', 'ne': 'nebraska', 'nv': 'nevada', 'nh': 'new hampshire', 'nj': 'new jersey', 'nm': 'new mexico', 'ny': 'new york', 'nc': 'north carolina', 'nd': 'north dakota', 'oh': 'ohio', 'ok': 'oklahoma', 'or': 'oregon', 'pa': 'pennsylvania', 'ri': 'rhode island', 'sc': 'south carolina', 'sd': 'south dakota', 'tn': 'tennessee', 'tx': 'texas', 'ut': 'utah', 'vt': 'vermont', 'va': 'virginia', 'wa': 'washington', 'wv': 'west virginia', 'wi': 'wisconsin', 'wy': 'wyoming', 'dc': 'district of columbia' }; // If value is a state code, also try the full state name const fullStateName = stateCodeToName[valueLower]; const searchTerms = [valueLower]; if (fullStateName) { searchTerms.push(fullStateName); } field.find('option').each(function() { const optionText = $(this).text().trim().toLowerCase(); const optionValue = $(this).attr('value'); // Try each search term for (const searchTerm of searchTerms) { // Try exact text match if (optionText === searchTerm) { field.val(optionValue); optionFound = true; return false; // break } // Try partial match (e.g., "United States" matches "united states") if (optionText.includes(searchTerm) || searchTerm.includes(optionText)) { field.val(optionValue); optionFound = true; return false; // break } // Try matching option value (in case value is the state code) if (optionValue && optionValue.toLowerCase() === searchTerm) { field.val(optionValue); optionFound = true; return false; // break } } // Special handling for "United States" - try common variations if (value === 'United States') { const usVariations = ['united states', 'us', 'usa', 'united states of america']; if (usVariations.includes(optionText) || optionText.includes('united states')) { field.val(optionValue); optionFound = true; return false; // break } } }); } if (!optionFound) { const availableOptions = field.find('option').map(function() { return { value: $(this).attr('value'), text: $(this).text().trim() }; }).get(); // For state fields, try one more time with all variations if (hubspotFieldName === 'state' || hubspotFieldName === 'us_states') { const stateCodeToName = { 'pa': 'pennsylvania', 'ny': 'new york', 'ca': 'california', 'tx': 'texas', 'fl': 'florida', 'il': 'illinois', 'oh': 'ohio', 'ga': 'georgia', 'nc': 'north carolina', 'mi': 'michigan' }; const fullName = stateCodeToName[value.toLowerCase()]; if (fullName) { // Try all possible variations const variations = [ value, // "PA" value.toUpperCase(), // "PA" value.toLowerCase(), // "pa" fullName, // "pennsylvania" fullName.charAt(0).toUpperCase() + fullName.slice(1), // "Pennsylvania" fullName.toUpperCase() // "PENNSYLVANIA" ]; for (const variation of variations) { // Try as value const optionByValue = field.find(`option[value="${variation}"]`); if (optionByValue.length) { field.val(variation); optionFound = true; break; } // Try as text (case-insensitive) field.find('option').each(function() { const optText = $(this).text().trim().toLowerCase(); const optValue = $(this).attr('value'); if (optText === variation.toLowerCase() || optText.includes(variation.toLowerCase()) || variation.toLowerCase().includes(optText)) { field.val(optValue); optionFound = true; return false; // break } }); if (optionFound) break; } if (!optionFound) { } } } } else { // For country field specifically, add extra delay to allow HubSpot to show dependent fields if (hubspotFieldName === 'country' || hubspotFieldName === 'country__new_' || hubspotFieldName.includes('country')) { // Longer delay for country to ensure state field appears setTimeout(() => { field.trigger('change'); if (field[0]) { field[0].dispatchEvent(new Event('change', { bubbles: true, cancelable: true })); } // If we need to wait for dependent fields, resolve after shorter delay (optimized) if (waitForDependent) { setTimeout(() => { resolve(true); }, 100); // OPTIMIZED: Reduced from 200ms to 100ms for faster response } else { resolve(true); } }, 50); // OPTIMIZED: Reduced from 100ms to 50ms for faster response // Mark as found and skip the general dropdown handling below for country fieldFound = true; return; // Exit early, resolve will be called in setTimeout } } } else { // For input/textarea fields, set value directly field.val(value); } // For dropdowns, wait a bit after setting value to allow HubSpot to process if (field.is('select')) { // Skip if this is country field (already handled above) if (hubspotFieldName !== 'country' && hubspotFieldName !== 'country__new_' && !hubspotFieldName.includes('country')) { // Small delay for HubSpot to process dependent fields setTimeout(() => { field.trigger('change'); field.trigger('input'); // Also trigger native change event if (field[0]) { const nativeField = field[0]; nativeField.dispatchEvent(new Event('change', { bubbles: true, cancelable: true })); } }, 50); } } else { // For input fields, trigger immediately field.trigger('change'); field.trigger('input'); field.trigger('blur'); field.trigger('focus').trigger('blur'); } // Also try native events (but skip if country field already handled) if (field[0] && (hubspotFieldName !== 'country__new_' && !hubspotFieldName.includes('country'))) { const nativeField = field[0]; nativeField.dispatchEvent(new Event('input', { bubbles: true })); nativeField.dispatchEvent(new Event('change', { bubbles: true })); nativeField.dispatchEvent(new Event('blur', { bubbles: true })); } // Verify the value was set const newValue = field.val(); if (newValue === value || newValue === String(value) || (field.is('select') && newValue)) { } else { } fieldFound = true; if (!waitForDependent) { resolve(true); } break; } } catch (e) { } } if (!fieldFound) { this.$form.find('input, textarea, select').map(function() { const $field = $(this); const fieldType = $field.prop('tagName').toLowerCase(); const isSelect = fieldType === 'select'; return { name: $field.attr('name'), id: $field.attr('id'), type: $field.attr('type') || fieldType, placeholder: $field.attr('placeholder'), 'data-name': $field.attr('data-name'), label: $field.closest('.hs-form-field').find('label').first().text().trim(), currentValue: $field.val() || '(empty)', options: isSelect ? $field.find('option').map(function() { return { value: $(this).attr('value'), text: $(this).text().trim() }; }).get().slice(0, 5) : 'N/A' // Show first 5 options for selects }; }).get(); resolve(false); } }); }; // Populate fields sequentially: country first, then state, then others (async () => { // Step 1: Populate country first (if field name exists, try even if value is empty) if (countryFieldName) { if (countryValue) { const countryResult = await populateSingleField(countryFieldName, countryValue, true); // Wait for dependent fields // Step 2: Wait for state field to appear (HubSpot dependent field) // Determine the correct state field name based on country if (stateFieldName && stateValue) { // Determine which state field to use based on country // HubSpot shows different fields: "us_states" (US), "ca_provinces" (Canada), "uk_regions" (UK) let actualStateFieldName = 'us_states'; // Default to US states (FIXED: was 'states', now 'us_states') const countryLower = (countryValue || '').toLowerCase(); if (countryLower.includes('canada')) { actualStateFieldName = 'ca_provinces'; } else if (countryLower.includes('united kingdom') || countryLower.includes('uk')) { actualStateFieldName = 'uk_regions'; } else if (countryLower.includes('united states') || countryLower.includes('us') || countryLower.includes('usa')) { actualStateFieldName = 'us_states'; // FIXED: was 'states', now 'us_states' } // Wait for state field to appear in DOM (polling) let stateFieldAppeared = false; const maxWaitTime = 1500; // OPTIMIZED: Reduced from 2 seconds to 1.5 seconds const pollInterval = 30; // OPTIMIZED: Reduced from 50ms to 30ms for faster detection const startTime = Date.now(); while (!stateFieldAppeared && (Date.now() - startTime) < maxWaitTime) { // Try to find state field with various selectors (prioritize the correct one for the country) const stateSelectors = [ `select[name="${actualStateFieldName}"]`, // Try the correct field first `select[name="states"]`, // US states `select[name="ca_provinces"]`, // Canadian provinces `select[name="uk_regions"]`, // UK regions `select[name*="state"]`, `select[name*="State"]`, `select[name*="province"]`, `select[name*="Province"]`, `select[name*="region"]`, `select[name*="Region"]` ]; for (const selector of stateSelectors) { const $stateField = this.$form.find(selector); if ($stateField.length && $stateField.is(':visible')) { stateFieldAppeared = true; break; } } if (!stateFieldAppeared) { await new Promise(resolve => setTimeout(resolve, pollInterval)); } } if (stateFieldAppeared) { // OPTIMIZED: Minimal delay to ensure HubSpot has finished rendering await new Promise(resolve => setTimeout(resolve, 50)); // Reduced from 100ms to 50ms // Use the actual state field name determined from country const stateResult = await populateSingleField(actualStateFieldName, stateValue, false); } else { // Try with the determined field name const stateResult = await populateSingleField(actualStateFieldName, stateValue, false); } } else if (stateFieldName && !stateValue) { } } else { } } else { } // Step 3: Populate all other fields const otherFields = Object.keys(fields).filter(name => name !== countryFieldName && name !== stateFieldName ); for (const hubspotFieldName of otherFields) { const value = fields[hubspotFieldName]; if (value) { await populateSingleField(hubspotFieldName, value, false); } } // Check what was actually populated const checkField = (fieldName) => { const field = this.$form.find(`[name="${fieldName}"]`).first(); if (field.length) { const value = field.val(); return value && value !== '' && value !== 'HIDDEN' ? value : '(empty or HIDDEN)'; } return '(field not found)'; }; })(); } /** * Update a single field value in the form * @param {string} fieldKey - Field mapping key (e.g., 'jobtitle', 'student_count') OR direct HubSpot field name * @param {*} value - Value to set */ updateFieldValue(fieldKey, value) { // Skip if form is not available if (!this.$form || !this.$form.length) { return; } // Skip only if value is null or undefined (allow 0, false, empty string) // Empty string is allowed because we might want to clear a field // 0 and false are valid values for numeric/boolean fields if (value === null || value === undefined) { return; } // Check if fieldKey is a mapping key or direct HubSpot field name const hubspotFieldName = this.fieldMapping[fieldKey] || fieldKey; // PROTECTED FIELDS: These should ONLY be populated from API Call 1 (Personnel API) // API Call 2 (Building API) should NEVER overwrite these fields const protectedFields = ['jobtitle', 'firstname', 'lastname', 'role']; const isProtectedField = protectedFields.includes(fieldKey.toLowerCase()) || protectedFields.some(pf => hubspotFieldName.toLowerCase().includes(pf)); if (isProtectedField) { } // Try multiple selectors to find the field const selectors = [ `input[name="${hubspotFieldName}"]`, `input[id="${hubspotFieldName}"]`, `input[data-name="${hubspotFieldName}"]`, `textarea[name="${hubspotFieldName}"]`, `select[name="${hubspotFieldName}"]`, `[name="${hubspotFieldName}"]` ]; let field = null; for (const selector of selectors) { field = this.$form.find(selector); if (field.length) { break; } } if (field && field.length) { const currentValue = field.val(); // PROTECTION: For protected fields (Role, First name, Last name), only update if empty or HIDDEN // This ensures API Call 2 doesn't overwrite values from API Call 1 (Personnel API) if (isProtectedField && currentValue && currentValue !== '' && currentValue !== 'HIDDEN' && currentValue.toLowerCase() !== 'hidden') { return; // Don't overwrite protected fields that already have values } // Update if field is empty, has placeholder value, or is "HIDDEN" // Also update if current value is "HIDDEN" (case-insensitive check) // Always update student count fields even if they have values (they should be updated with latest API data) const isStudentCountField = fieldKey === 'student_count' || fieldKey === 'building_students' || hubspotFieldName.includes('student_count'); const shouldUpdate = isStudentCountField || // Always update student count fields !currentValue || currentValue === '' || currentValue === 'HIDDEN' || currentValue.toLowerCase() === 'hidden' || (currentValue && currentValue.trim() === 'HIDDEN'); if (shouldUpdate) { // Convert value to string for consistency const stringValue = String(value); field.val(stringValue); field.trigger('change'); field.trigger('input'); // Also trigger native events if (field[0]) { field[0].dispatchEvent(new Event('change', { bubbles: true })); field[0].dispatchEvent(new Event('input', { bubbles: true })); } } else { } } else { // Log available fields for debugging const availableFields = this.$form.find('input, select, textarea').map(function() { return { name: $(this).attr('name'), id: $(this).attr('id'), 'data-name': $(this).attr('data-name'), currentValue: $(this).val() }; }).get(); } } /** * Enhance form with additional data from full details API call * This updates fields that weren't available in the initial response * @param {Object} fullDetails - Full institution details from getByUid */ async enhanceFormWithFullDetails(fullDetails) { if (!this.$form || !this.$form.length) { return; } // Only update fields that are missing or need enhancement const emailForEnhancement = (window.SecurlyForm && window.SecurlyForm.lastSearchedEmail) || null; const fields = this.mapInstitutionToFormFields(fullDetails, emailForEnhancement); // Update hidden fields that might have better data now const hiddenFields = ['student_count', 'institution_id']; for (const key of hiddenFields) { if (this.fieldMapping[key] && fields[key]) { const hubspotFieldName = this.fieldMapping[key]; const value = fields[key]; if (value) { // Find and update the field const field = this.$form.find(`input[name="${hubspotFieldName}"], select[name="${hubspotFieldName}"]`); if (field.length) { const currentValue = field.val(); if (!currentValue || currentValue === '') { // Only update if field is empty (don't overwrite user input) field.val(value); field.trigger('change'); } } } } } } /** * Map institution data to HubSpot form field names * @param {Object} institution - Institution data * @returns {Object} Mapped fields */ mapInstitutionToFormFields(institution, email = null) { // Helper function to extract first and last name from email // Only extracts if it looks like a real name, not generic words const extractNamesFromEmail = (emailAddr) => { if (!emailAddr || typeof emailAddr !== 'string') { return { firstname: '', lastname: '' }; } // Extract the part before @ const localPart = emailAddr.split('@')[0].toLowerCase().trim(); if (!localPart) { return { firstname: '', lastname: '' }; } // List of common generic/non-name words that should NOT be extracted as names const genericWords = [ 'teacher', 'admin', 'administrator', 'info', 'contact', 'support', 'help', 'sales', 'marketing', 'noreply', 'no-reply', 'donotreply', 'test', 'demo', 'webmaster', 'postmaster', 'hostmaster', 'abuse', 'security', 'privacy', 'legal', 'billing', 'accounts', 'hr', 'humanresources', 'it', 'tech', 'service', 'services', 'customer', 'customerservice', 'helpdesk', 'ticket', 'newsletter', 'notifications', 'alerts', 'system', 'automated', 'auto', 'mail', 'email', 'user', 'users', 'account', 'accounts', 'staff', 'faculty', 'student', 'students', 'parent', 'parents', 'guardian' ]; // Common patterns: // - firstname.lastname@domain.com // - firstname_lastname@domain.com // - firstnamelastname@domain.com // - f.lastname@domain.com // - firstname-lastname@domain.com let firstname = ''; let lastname = ''; // Pattern 1: firstname.lastname or firstname_lastname or firstname-lastname if (localPart.includes('.') || localPart.includes('_') || localPart.includes('-')) { const separators = ['.', '_', '-']; for (const sep of separators) { if (localPart.includes(sep)) { const parts = localPart.split(sep); if (parts.length >= 2) { const potentialFirst = parts[0]; const potentialLast = parts.slice(1).join(sep); // Validate: BOTH parts must NOT be generic words // This ensures we only extract real names, not generic email addresses const isFirstGeneric = genericWords.includes(potentialFirst); const isLastGeneric = genericWords.includes(potentialLast); // Only extract if BOTH parts are NOT generic words // AND both parts have reasonable length (at least 2 characters) if (!isFirstGeneric && !isLastGeneric && potentialFirst.length >= 2 && potentialLast.length >= 2) { firstname = potentialFirst; lastname = potentialLast; break; } } } } } // Pattern 2: firstnamelastname (no separator) - try to split intelligently // Only if it doesn't match a generic word else if (localPart.length > 3 && !genericWords.includes(localPart)) { // Simple heuristic: assume first name is 3-6 chars, rest is last name // This is a best guess and won't work perfectly for all cases const midPoint = Math.min(6, Math.floor(localPart.length / 2)); const potentialFirst = localPart.substring(0, midPoint); const potentialLast = localPart.substring(midPoint); // Validate: don't extract if either part is a generic word if (!genericWords.includes(potentialFirst) && !genericWords.includes(potentialLast) && potentialFirst.length >= 2 && potentialLast.length >= 2) { firstname = potentialFirst; lastname = potentialLast; } } // Only return names if we successfully extracted valid names // (not generic words) if (!firstname || !lastname || genericWords.includes(firstname.toLowerCase()) || genericWords.includes(lastname.toLowerCase())) { return { firstname: '', lastname: '' }; } // Capitalize first letter of each name const capitalize = (str) => { if (!str) return ''; return str.charAt(0).toUpperCase() + str.slice(1); }; return { firstname: capitalize(firstname), lastname: capitalize(lastname) }; }; // PRIORITY 1: Use personnel data from Personnel API (ONLY when email is found) // These fields are added when Personnel API is called (which only happens when email is available) // IMPORTANT: These fields should NEVER be overwritten by API Call 2 (Building API) // API Call 2 should only populate OTHER fields, not Role, First name, or Last name const personnelFirstName = institution.personnelFirstName || institution.firstName || ''; const personnelLastName = institution.personnelLastName || institution.lastName || ''; // Title ONLY comes from Personnel API - do NOT use institution.title (that's institution name, not job title) // Personnel API is ONLY called when email is found, so for autocomplete (no email), this will be empty const personnelTitle = institution.personnelTitle || ''; // ONLY use personnel data from API - do NOT extract from email text // First Name and Last Name should ONLY be populated if: // 1. Email is entered // 2. API returns First Name and Last Name // If API doesn't return names, leave fields empty (do NOT extract from email) const names = (personnelFirstName && personnelLastName) ? { firstname: personnelFirstName, lastname: personnelLastName } : { firstname: '', lastname: '' }; // Use personnel title ONLY if available (from Personnel API when email was found) // For autocomplete scenario (no email), this will be empty - jobtitle field should remain empty/HIDDEN // IMPORTANT: Building API should NEVER populate jobtitle - it only comes from Personnel API const jobTitle = personnelTitle || ''; if (jobTitle) { } else { } // Helper functions to extract field values with multiple fallbacks const getName = () => { return institution.institutionNameProper || // Agile-Ed API format institution.name || institution.institutionName || institution.buildingName || institution.building_name || institution.schoolName || institution.school_name || institution.districtName || institution.district_name || institution.title || institution.displayName || institution.display_name || institution.parentNameProper || // Fallback to parent name (institution?.building?.name) || (institution?.district?.name) || ''; }; const getAddress = () => { return institution?.mailingAddr1Proper || // API field name institution?.mailingAddr1 || institution?.address || institution?.streetAddress || institution?.street_address || institution?.street || institution?.street1 || (institution?.building?.address) || (institution?.building?.mailingAddr1Proper) || ''; }; // Helper to determine country from API response or state const getCountry = () => { // First, check if country is directly in the API response const countryFromAPI = institution.country || institution.Country || institution.countryName || (institution.building && institution.building.country) || (institution.building && institution.building.Country) || ''; if (countryFromAPI) { // Normalize common country name variations const countryLower = countryFromAPI.toLowerCase(); if (countryLower.includes('united states') || countryLower === 'us' || countryLower === 'usa') { return 'United States'; } return countryFromAPI; // Return as-is if it's a valid country name } // Fallback: Determine country from state code // Try to get state from multiple locations // IMPORTANT: API uses "mailingState" field name! const state = institution.mailingState || // API field name institution.state || institution.stateCode || institution.state_code || (institution.building && (institution.building.state || institution.building.mailingState)) || (institution.district && (institution.district.state || institution.district.mailingState)) || (institution.parent && (institution.parent.state || institution.parent.mailingState)) || ''; // Only try to determine country if we have a state value if (!state || state.trim() === '') { // No state available - this is normal for some institutions return ''; } // US states (2-letter codes) const usStates = ['AL','AK','AZ','AR','CA','CO','CT','DE','FL','GA','HI','ID','IL','IN','IA','KS','KY','LA','ME','MD','MA','MI','MN','MS','MO','MT','NE','NV','NH','NJ','NM','NY','NC','ND','OH','OK','OR','PA','RI','SC','SD','TN','TX','UT','VT','VA','WA','WV','WI','WY','DC']; if (usStates.includes(state.toUpperCase().trim())) { return 'United States'; } // Canadian provinces (2-letter codes) const caProvinces = ['AB','BC','MB','NB','NL','NS','NT','NU','ON','PE','QC','SK','YT']; if (caProvinces.includes(state.toUpperCase().trim())) { return 'Canada'; } // State exists but doesn't match known US/Canadian codes // This could be a full state name, international code, or invalid data return ''; }; // Debug: Log what state-related fields exist in the API response // Extract state with detailed logging - check all possible locations and naming variations // IMPORTANT: API uses "mailingState" field name! const extractedState = institution.mailingState || // API field name institution.state || institution.State || institution.stateCode || institution.StateCode || institution.state_code || institution.state_Code || (institution.building && (institution.building.state || institution.building.State || institution.building.mailingState)) || (institution.district && (institution.district.state || institution.district.State || institution.district.mailingState)) || (institution.parent && (institution.parent.state || institution.parent.State || institution.parent.mailingState)) || (institution.Parent && (institution.Parent.state || institution.Parent.State || institution.Parent.mailingState)) || (institution.District && (institution.District.state || institution.District.State || institution.District.mailingState)) || ''; // Helper to convert buildingType to purchase level dropdown value const getPurchaseLevel = () => { const buildingType = institution.buildingType || institution.building_type || institution.type || (institution.building && institution.building.buildingType) || ''; // Map API buildingType values to dropdown options // API returns "BLDG" for buildings, "DIST" for districts if (buildingType === 'BLDG' || buildingType === 'Building' || buildingType === 'BUILDING' || buildingType === 'bldg') { return 'Building'; } else if (buildingType === 'DIST' || buildingType === 'District' || buildingType === 'DISTRICT' || buildingType === 'dist') { return 'District'; } // If no match, return empty (user will need to select manually) return ''; }; // Default mapping (customize based on your HubSpot form fields) // FIX #5: Use optional chaining for safety const defaultMapping = { institution_name: getName(), address: getAddress(), city: institution?.mailingCityProper || // API field name institution?.mailingCity || institution?.city || (institution?.building?.city) || (institution?.building?.mailingCityProper) || '', state: extractedState, zip: institution?.mailingZip || // API field name institution?.mailingZipCode || institution?.zip || institution?.zipCode || institution?.zip_code || institution?.postalCode || institution?.postal_code || (institution?.building?.zip) || (institution?.building?.mailingZip) || '', country: getCountry(), // Auto-detect country from state (getCountry checks institution.state which should match extractedState) company: getName(), // Often HubSpot uses 'company' for school name firstname: names.firstname, // Extracted from email or Personnel API lastname: names.lastname, // Extracted from email or Personnel API jobtitle: jobTitle, // From Personnel API (personnelTitle) - ONLY populated when email is found and Personnel API returns title // Optional fields - only include if mapped in fieldMapping institution_id: institution.instUid || institution.uid || institution.id || institution.buildingId || institution.building_id || '', // purchase_level removed - no longer populating district_or_building field // getPurchaseLevel() logic kept for building_type -> connectlink_institution_type // PRIORITY 2: Extract district student count (enrollment data) // FIX #5: Use optional chaining for safety (with fallback for older browsers) student_count: institution?.districtStudents || institution?.district_students || institution?.districtStudentCount || institution?.studentCount || institution?.student_count || institution?.enrollment || institution?.totalStudents || (institution?.district && (institution.district.studentCount || institution.district.enrollment || institution.district.student_count || institution.district.districtStudents)) || (institution?.parent && (institution.parent.studentCount || institution.parent.enrollment || institution.parent.student_count || institution.parent.districtStudents)) || '', // PRIORITY 3: Extract building student count (from BldgInDist API) building_students: institution?.buildingStudents || institution?.building_students || institution?.buildingStudentCount || '', // Phone number (if available from API) phone: institution?.phone || institution?.phoneNumber || institution?.phone_number || institution?.telephone || (institution?.building && (institution.building.phone || institution.building.phoneNumber)) || '', // Securly student count (same as student_count, but mapped to different field) securly_student_count: institution?.studentCount || institution?.student_count || institution?.enrollment || institution?.totalStudents || (institution?.district && (institution.district.studentCount || institution.district.enrollment || institution.district.student_count)) || (institution?.parent && (institution.parent.studentCount || institution.parent.enrollment || institution.parent.student_count)) || '', // Building type (converted from buildingType: "BLDG" -> "Building", "DIST" -> "District") building_type: getPurchaseLevel(), // Reuse the same conversion logic // File type text from API (e.g., "Public", "Private") - maps to Type of School file_type_text: institution?.fileTypeText || institution?.file_type_text || institution?.FileTypeText || (institution?.building && institution.building.fileTypeText) || '', // Institution type (same as fileTypeText) institution_type: institution?.fileTypeText || institution?.file_type_text || institution?.FileTypeText || (institution?.building && institution.building.fileTypeText) || '' }; // Use custom mapping if provided // Only map fields that are explicitly in fieldMapping to avoid warnings const mapped = {}; // IMPORTANT: Populate country FIRST so dependent fields (state) appear // Then populate other fields // Include hidden fields in fieldOrder so they're populated even though not visible // Include jobtitle early so it gets populated from Personnel API data // purchase_level removed from fieldOrder - no longer populating district_or_building field const fieldOrder = ['country', 'state', 'company', 'institution_name', 'city', 'zip', 'address', 'firstname', 'lastname', 'jobtitle', 'student_count', 'building_students', 'institution_id']; // First, add country if it exists and is mapped (ALWAYS include it, even if empty) if (this.fieldMapping.country) { const countryVal = defaultMapping.country || ''; mapped[this.fieldMapping.country] = countryVal; } else { } // Then add other fields in order fieldOrder.forEach((key) => { if (key === 'country') return; // Already added if (this.fieldMapping[key]) { // Include field even if value is empty (for state, we want to try to populate it) const fieldVal = defaultMapping[key] || ''; mapped[this.fieldMapping[key]] = fieldVal; if (key === 'state') { } else if (key === 'jobtitle') { if (fieldVal) { } else { } } else if (key === 'firstname' || key === 'lastname') { } } else if ((key === 'company' || key === 'institution_name') && defaultMapping[key]) { // Always include company/institution_name even if not explicitly mapped (common field) const hubspotFieldName = this.fieldMapping[key] || key; mapped[hubspotFieldName] = defaultMapping[key]; } else if ((key === 'firstname' || key === 'lastname' || key === 'jobtitle') && defaultMapping[key]) { // Always include name and jobtitle fields if they have values (from Personnel API) const hubspotFieldName = this.fieldMapping[key] || key; mapped[hubspotFieldName] = defaultMapping[key]; } }); // Add any remaining fields that are mapped (but skip if already added) Object.keys(defaultMapping).forEach((key) => { if (this.fieldMapping[key] && !mapped[this.fieldMapping[key]]) { // Include even if empty - we want to try to populate it mapped[this.fieldMapping[key]] = defaultMapping[key] || ''; } }); // Final check: ensure country and state are in mapped object (even if empty) if (this.fieldMapping.country && !(this.fieldMapping.country in mapped)) { mapped[this.fieldMapping.country] = defaultMapping.country || ''; } if (this.fieldMapping.state && !(this.fieldMapping.state in mapped)) { mapped[this.fieldMapping.state] = defaultMapping.state || ''; } // Debug: Verify country and state are in mapped object return mapped; } /** * Highlight selected institution in results * @param {number} index - Index of selected institution */ highlightSelected(index) { document.querySelectorAll('.institution-result').forEach((el, i) => { if (i === index) { el.style.backgroundColor = '#e3f2fd'; el.style.borderColor = '#007bff'; el.style.borderWidth = '2px'; } else { el.style.backgroundColor = '#fff'; el.style.borderColor = '#ddd'; el.style.borderWidth = '1px'; } }); } /** * Show loading indicator * @param {boolean} show - Show or hide */ showLoading(show) { const loadingEl = document.getElementById('institution-loading'); if (loadingEl) { loadingEl.style.display = show ? 'block' : 'none'; } } /** * Show error message * @param {string} message - Error message */ showError(message) { const errorEl = document.getElementById('institution-error-message'); if (errorEl) { errorEl.textContent = message; errorEl.style.display = 'block'; } } /** * Hide error message */ hideError() { const errorEl = document.getElementById('institution-error-message'); if (errorEl) { errorEl.style.display = 'none'; } } /** * Hide results container */ hideResults() { const resultsEl = document.getElementById('institution-search-results'); if (resultsEl) { resultsEl.style.display = 'none'; } } }