Match each field below to a column from your file. Score and phone are optional.
0 leads shown
Name
Address
Service match
Signal date
Score
Edit copy per category — tokens: , , , ,
0 leads selected for export
`; /* ================= Dynamic PapaParse loader ================= */ function ensurePapa(onReady, onError){ if(window.Papa){ onReady(); return; } const existing = document.querySelector('script[data-leadscout-papa]'); if(existing){ existing.addEventListener('load', onReady); existing.addEventListener('error', ()=>onError(new Error('PapaParse failed to load'))); return; } const s = document.createElement('script'); s.src = 'https://cdnjs.cloudflare.com/ajax/libs/PapaParse/5.4.1/papaparse.min.js'; s.setAttribute('data-leadscout-papa','1'); s.onload = onReady; s.onerror = ()=>onError(new Error('PapaParse failed to load')); document.head.appendChild(s); } /* ================= Service catalog ================= */ const SERVICES = [ { key:'hvac', label:'HVAC / Furnace Repair', keywords:['hvac','furnace','heat pump','heating'], peak:[8,9,10], shoulder:[7,11], subject:'Keep the heat on this winter, ', body:'Hi ,\n\nWith temperatures dropping, now is the time to get your furnace inspected before the first cold snap. offers seasonal tune-ups for homes at and nearby.\n\nWant to grab a slot before it gets busy? \n\n— ' }, { key:'roofing', label:'Roofing Repair/Inspection', keywords:['roof'], peak:[8,9], shoulder:[7,10], subject:'Get your roof checked before winter, ', body:'Hi ,\n\nA quick roof inspection now can prevent expensive leaks once snow and ice set in. is scheduling fall inspections for homes near .\n\nBook a free look: \n\n— ' }, { key:'gutters', label:'Gutter Cleaning & Guards', keywords:['gutter'], peak:[9,10], shoulder:[8,11], subject:'Leaf season is here, — gutters covered?', body:'Hi ,\n\nClogged gutters cause ice dams and water damage fast once winter hits. can clear and guard your gutters at before the next storm.\n\n\n\n— ' }, { key:'landscaping', label:'Landscaping / Fall Cleanup', keywords:['landscap','lawn','mulch','leaf removal','yard cleanup','fall cleanup','aeration','overseed'], peak:[8,9], shoulder:[7,10], subject:'Get your yard winter-ready, ', body:'Hi ,\n\nFall cleanup, leaf removal, and a final mow now save you a mess once the snow flies. has openings for to wrap up the season right.\n\n\n\n— ' }, { key:'insulation', label:'Insulation & Weatherization', keywords:['insulation','weatheriz','energy audit','draft','air seal'], peak:[8,9,10], shoulder:[7,11], subject:'Stop heat loss before it costs you, ', body:'Hi ,\n\nDrafty windows and thin attic insulation quietly drive up winter heating bills. offers a quick weatherization assessment for homes like yours at .\n\n\n\n— ' }, { key:'chimney', label:'Chimney & Fireplace Service', keywords:['chimney','fireplace'], peak:[8,9,10], shoulder:[7], subject:'Ready your fireplace for the season, ', body:'Hi ,\n\nBefore the first fire of the season, a chimney inspection keeps things safe and running clean. is booking now for homes near .\n\n\n\n— ' }, { key:'snow', label:'Snow Removal / Plowing', keywords:['snow','plow'], peak:[9,10,11], shoulder:[8,0], subject:'Lock in your snow plan before the first storm, ', body:'Hi ,\n\nPlowing contracts fill up fast once the first snow hits. still has openings for homes at — lock in your season rate now.\n\n\n\n— ' }, { key:'generator', label:'Generator Installation', keywords:['generator'], peak:[8,9,10], shoulder:[7,11], subject:'Storm-proof your home this winter, ', body:'Hi ,\n\nWinter storms mean power outages. installs standby generators for homes like so you never lose heat or power.\n\n\n\n— ' }, { key:'waterheater', label:'Water Heater Service', keywords:['water heater','hot water'], peak:[8,9,10,11], shoulder:[7,0], subject:'Don\'t get caught with cold water, ', body:'Hi ,\n\nWater heaters fail more often in cold months. can inspect or replace yours at before it becomes an emergency.\n\n\n\n— ' }, { key:'driveway', label:'Driveway Sealing/Paving', keywords:['driveway','paving','asphalt','seal coat','sealcoat'], peak:[7,8,9], shoulder:[6,10], subject:'Last call to seal your driveway, ', body:'Hi ,\n\nOnce temperatures drop, sealing and paving season closes. has a few remaining slots for before the ground freezes.\n\n\n\n— ' }, { key:'tree', label:'Tree Trimming/Removal', keywords:['tree'], peak:[9,10], shoulder:[8,11], subject:'Trim storm-risk branches before winter, ', body:'Hi ,\n\nDead or overhanging branches are a real risk once ice and wind pick up. can assess the trees at now.\n\n\n\n— ' }, { key:'pest', label:'Pest Control (Rodent Exclusion)', keywords:['pest','rodent','exterminat','mice','mouse'], peak:[8,9,10], shoulder:[7,11], subject:'Seal up before rodents move in, ', body:'Hi ,\n\nAs it cools down, rodents look for a way indoors. offers fall exclusion service for homes near to seal entry points before they find one.\n\n\n\n— ' }, { key:'sump', label:'Sump Pump / Freeze Prep', keywords:['sump','pipe freeze','frozen pipe','plumbing'], peak:[9,10], shoulder:[8,11], subject:'Protect your pipes before the freeze, ', body:'Hi ,\n\nA quick sump pump and pipe check now can prevent a frozen-pipe disaster later. is scheduling freeze-prep visits for .\n\n\n\n— ' }, { key:'general', label:'General / Unmatched', keywords:[], peak:[], shoulder:[], subject:'A quick note for your home at , ', body:'Hi ,\n\n noticed you may be exploring home service options and wanted to reach out directly.\n\n\n\n— ' }, ]; const svcByKey = Object.fromEntries(SERVICES.map(s=>[s.key,s])); function matchService(text){ if(!text) return 'general'; const t = text.toLowerCase(); for(const s of SERVICES){ if(s.key==='general') continue; if(s.keywords.some(k=>t.includes(k))) return s.key; } return 'general'; } function seasonScore(key){ const month = new Date().getMonth(); const s = svcByKey[key]; if(!s) return 5; if(s.peak.includes(month)) return 40; if(s.shoulder.includes(month)) return 20; return 5; } function recencyScore(dateStr){ if(!dateStr) return 0; const d = new Date(dateStr); if(isNaN(d)) return 0; const days = (Date.now() - d.getTime()) / 86400000; if(days <= 7=""> shadow.getElementById(id); const csvInput = $('csvInput'); const csvBox = $('csvBox'); const csvText = $('csvText'); const mappingPanel = $('mappingPanel'); const mapGrid = $('mapGrid'); const dashboard = $('dashboard'); const templatesPanel = $('templatesPanel'); const tbody = $('leadRows'); const searchBox = $('searchBox'); const serviceFilter = $('serviceFilter'); const scoreFilter = $('scoreFilter'); const countStat = $('countStat'); const selectedStat = $('selectedStat'); const tmplList = $('tmplList'); const bizNameInput = $('bizName'); const bizLinkInput = $('bizLink'); /* ================= CSV upload ================= */ csvInput.addEventListener('change', (e)=>{ const file = e.target.files[0]; if(!file) return; csvBox.classList.add('has-file'); csvText.innerHTML = `${file.name} loading…`; ensurePapa(function(){ Papa.parse(file, { header:true, skipEmptyLines:true, complete: (res)=>{ rawRows = res.data; headers = res.meta.fields || []; csvText.innerHTML = `${file.name} loaded — map columns below`; buildMappingUI(); mappingPanel.style.display = 'block'; }, error: (err)=>{ csvText.innerHTML = `${file.name} — failed to parse: ${err.message}`; } }); }, function(err){ csvText.innerHTML = `Could not load the CSV parser (${err.message}). Check your connection and try again.`; }); }); const FIELD_DEFS = [ {key:'name', label:'Name', required:true, guesses:['name','full name','contact']}, {key:'address', label:'Address', required:true, guesses:['address','property address','street']}, {key:'email', label:'Email', required:true, guesses:['email','e-mail']}, {key:'service', label:'Service interest', required:true, guesses:['service','interest','category','intent','product']}, {key:'signalDate', label:'Signal date', required:false, guesses:['date','signal date','timestamp','last seen']}, {key:'score', label:'Pulse score (optional)', required:false, guesses:['score','intent score','rating']}, ]; function guessColumn(guesses){ for(const g of guesses){ const hit = headers.find(h=>h.toLowerCase().trim()===g); if(hit) return hit; } for(const g of guesses){ const hit = headers.find(h=>h.toLowerCase().includes(g)); if(hit) return hit; } return ''; } function buildMappingUI(){ mapGrid.innerHTML = ''; FIELD_DEFS.forEach(f=>{ const guess = guessColumn(f.guesses); const div = document.createElement('div'); div.className = 'map-item'; const opts = [''] .concat(headers.map(h=>``)); div.innerHTML = ` ${f.required ? 'required' : 'optional'}`; mapGrid.appendChild(div); }); } $('applyMapBtn').addEventListener('click', ()=>{ mapping = {}; mapGrid.querySelectorAll('select').forEach(sel=>{ mapping[sel.dataset.field] = sel.value; }); const missing = FIELD_DEFS.filter(f=>f.required && !mapping[f.key]); if(missing.length){ alert('Please map required fields: ' + missing.map(f=>f.label).join(', ')); return; } buildLeads(); mappingPanel.style.display = 'none'; dashboard.style.display = 'block'; templatesPanel.style.display = 'block'; buildTemplateEditors(); }); function buildLeads(){ leads = rawRows.map((row,i)=>{ const serviceRaw = mapping.service ? row[mapping.service] : ''; const svcKey = matchService(serviceRaw); const dateVal = mapping.signalDate ? row[mapping.signalDate] : ''; const providedScore = mapping.score ? parseFloat(row[mapping.score]) : NaN; let score = seasonScore(svcKey) + recencyScore(dateVal); if(!isNaN(providedScore)){ score += Math.max(0, Math.min(20, providedScore > 1 ? providedScore/5 : providedScore*20)); } score = Math.max(0, Math.min(100, Math.round(score))); return { id:'ld_'+i, name: mapping.name ? row[mapping.name] : '', address: mapping.address ? row[mapping.address] : '', email: mapping.email ? row[mapping.email] : '', serviceRaw, svcKey, signalDate: dateVal, score, status:'New' }; }); serviceFilter.innerHTML = '' + SERVICES.map(s=>``).join(''); renderTable(); } /* ================= Template editors ================= */ function buildTemplateEditors(){ const usedKeys = [...new Set(leads.map(l=>l.svcKey))]; tmplList.innerHTML = ''; usedKeys.forEach(key=>{ const s = svcByKey[key]; const count = leads.filter(l=>l.svcKey===key).length; const div = document.createElement('div'); div.className = 'tmpl-item'; div.innerHTML = `