Β© 2026 IncentiveSwift A SwiftSoftware Company. All rights reserved. +a+'').join('') +''; } else if(t==='hotel_savings_card'){ const amounts=[100,200,300,500]; return ''; } else if(ct==='raffle'){ html+='' +'' +''; } else if(ct==='quiz'||ct==='trivia'){ html+='' +'' +''; } else if(ct==='social_contest'){ html+='' +''; } else if(ct==='referral'){ html+='' +'' +''; } else if(ct==='checkin'){ html+='' +'' +''; } else if(ct==='points'||ct==='purchase'){ html+='' +'' +''; } else if(ct==='photo_contest'||ct==='form'){ html+='' +''; } html+=''; return html; } function toggleAccordion(id){ const el=document.getElementById(id); if(el)el.classList.toggle('open'); } // ── Campaign Integration Linking ── let campaignIntegrationsCache={}; async function loadCampaignIntegrations(campaignId){ try{ if(!campaignId)return; const r=await api('/api/v1/campaigns/'+campaignId+'/integrations'); campaignIntegrationsCache[campaignId]=r||[]; return campaignIntegrationsCache[campaignId]; }catch(e){console.log('Error loading integrations:',e.message);return []} } function renderIntegrationLinkSection(containerId,campaignId,cfg){ const container=document.getElementById(containerId); if(!container)return; const linked=cfg?.linked_integrations||campaignIntegrationsCache[campaignId]||[]; const linkedIds=new Set(linked.map(l=>l.integration_id||l.id)); let html=''; container.innerHTML=html; } async function toggleCampaignIntegration(checkbox,campaignId,integrationId){ const linked=checkbox.checked; // Optimistic toggle checkbox.disabled=true; try{ if(linked){ await api('/api/v1/campaigns/'+campaignId+'/integrations',{ method:'POST', body:JSON.stringify({integration_id:integrationId,trigger_events:['on_win','on_entry'],enabled:true}) }); }else{ await api('/api/v1/campaigns/'+campaignId+'/integrations/'+integrationId,{method:'DELETE'}); } showToast(linked?'Integration linked βœ“':'Integration unlinked'); // Refresh await loadCampaignIntegrations(campaignId); // Re-render the section if it exists const el=document.getElementById('integration-link-section'); if(el){ const cam=campaigns.find(c=>c.id===campaignId||c.slug===campaignId); renderIntegrationLinkSection('integration-link-section',campaignId,cam?.config||{}); } }catch(e){ checkbox.checked=!linked; showToast('Error: '+e.message,'error'); } checkbox.disabled=false; } async function updateMbOffer(input,campaignId,integrationId){ const offerId=input.value.trim(); try{ await api('/api/v1/campaigns/'+campaignId+'/integrations',{ method:'POST', body:JSON.stringify({ integration_id:integrationId, provider_metadata:{offer_id:offerId||null}, trigger_events:['on_win','on_entry'], enabled:true }) }); showToast(offerId?'Offer ID saved βœ“':'Offer ID cleared'); }catch(e){showToast('Error: '+e.message,'error')} } async function saveCampaignEditor(btn){ btn.textContent='Saving...';btn.disabled=true; try{ const table=$('prize-table'); const rows=Array.from(table.querySelectorAll('tr')).slice(1); const prizes=rows.map((r,i)=>{ return { id: 'p'+(i+1), label: r.querySelector('[data-field=label]').value || 'Prize '+(i+1), color: r.querySelector('[data-field=color]').value, weight: parseInt(r.querySelector('[data-field=weight]').value)||0, prize_type: r.querySelector('[data-field=prize_type]').value, inventory: (function(){const v=r.querySelector('[data-field=inventory]').value;return v===''?null:parseInt(v)})(), marketing_boost: (function(){ const mbType=r.querySelector('[data-field=mb_type]')?.value; if(!mbType)return null; const mb={incentive_type:mbType,enabled:true}; const amtEl=document.getElementById('mb-amt-'+(parseInt(r.dataset.idx||i)||i)); if(amtEl){ const amtInput=amtEl.querySelector('select')||amtEl.querySelector('input'); if(amtInput){ if(mbType==='vacation_incentive')mb.destination=parseInt(amtInput.value); else if(amtInput.value)mb.amount=parseInt(amtInput.value); } } return mb; })() }; }); const totalWeight=prizes.reduce((s,p)=>s+p.weight,0); // Collect per-prize delivery configs const deliveryTable=$('delivery-section'); const deliveryRows=deliveryTable?Array.from(deliveryTable.querySelectorAll('tr')).slice(1):[]; const updatedPrizes=prizes.map((p,i)=>{ const delRow=deliveryRows[i]; if(delRow){ const methodEl=delRow.querySelector('[data-delivery="method"]'); const subjectEl=delRow.querySelector('[data-delivery="subject-or-url"]'); const bodyEl=delRow.querySelector('[data-delivery="body"]'); if(methodEl&&methodEl.value!=='none'){ p.delivery={ method: methodEl.value, subject: methodEl.value==='email'?(subjectEl?.value||'You won {{prize_label}}!'):undefined, body: methodEl.value==='email'?(bodyEl?.value||''):undefined, redirect_url: methodEl.value==='redirect'?(subjectEl?.value||''):undefined, }; } } return p; }); // Collect entry field toggles const entryFields={}; document.querySelectorAll('.entry-field-toggle').forEach(cb=>{ entryFields[cb.dataset.field]=cb.checked; }); // Collect custom fields const customFieldRows=document.querySelectorAll('.custom-field-row'); const customFields=Array.from(customFieldRows).map((row,idx)=>{ const label=row.querySelector('.cf-label')?.value||''; const ftype=row.querySelector('.cf-type')?.value||'text'; const required=row.querySelector('.cf-required')?.checked||false; const options=row.querySelector('.cf-options')?.value||''; return {field_key:'cf_'+(idx+1),field_label:label,field_type:ftype,required,options:options.split(',').map(s=>s.trim()).filter(Boolean),sort_order:idx}; }).filter(cf=>cf.field_label); const ct=campaign?.campaign_type||campaign?.type||'spin_wheel'; const config={ prize_pool: { prizes: updatedPrizes, total_weight: totalWeight, inventory_tracking: $('inv-tracking').checked, allow_when_exhausted: $('allow-exhausted').checked }, pity_timer: { enabled: $('pity-enabled').checked, threshold: parseInt($('pity-threshold').value)||5 }, max_spins_per_day: parseInt($('max-day').value)||0, max_spins_per_campaign: parseInt($('max-campaign').value)||0, entry_fields: entryFields, custom_fields: customFields, delivery: { on_win: { redirect: { url: $('win-url')?.value||'', text: $('win-text')?.value||'' }, autoresponder_fire: $('fire-autoresponder')?.checked||false }, on_lose: { redirect: { url: $('lose-url')?.value||'' } } } }; // Type-specific editor fields if($('ed-win-prob'))config.win_probability=parseInt($('ed-win-prob').value)||10; if($('ed-draw-date'))config.draw_date=$('ed-draw-date').value; if($('ed-max-entries'))config.max_entries_per_person=parseInt($('ed-max-entries').value)||0; if($('ed-auto-draw'))config.auto_draw=$('ed-auto-draw').checked; if($('ed-passing-score'))config.passing_score=parseInt($('ed-passing-score').value)||70; if($('ed-num-questions'))config.num_questions=parseInt($('ed-num-questions').value)||5; if($('ed-shuffle'))config.shuffle_questions=$('ed-shuffle').checked; if($('ed-platform'))config.platform=$('ed-platform').value; if($('ed-action'))config.required_action=$('ed-action').value; if($('ed-ref-points'))config.referral_points=parseInt($('ed-ref-points').value)||10; if($('ed-max-ref'))config.max_referrals=parseInt($('ed-max-ref').value)||0; if($('ed-reward-ref'))config.reward_referrer=$('ed-reward-ref').checked!==false; if($('ed-radius'))config.geo_radius=parseInt($('ed-radius').value)||100; if($('ed-location'))config.location_name=$('ed-location').value; if($('ed-checkin-limit'))config.checkins_per_day=parseInt($('ed-checkin-limit').value)||1; if($('ed-pts-action'))config.points_per_action=parseInt($('ed-pts-action').value)||1; if($('ed-pts-needed'))config.points_needed=parseInt($('ed-pts-needed').value)||100; if($('ed-auto-enroll'))config.auto_enroll=$('ed-auto-enroll').checked; if($('ed-max-sub'))config.max_submissions=parseInt($('ed-max-sub').value)||1; if($('ed-require-approval'))config.require_approval=$('ed-require-approval').checked; if(editingCampaignId){ const campaign=campaigns.find(c=>c.id===editingCampaignId); const slug=campaign?.slug||''; await api('/campaigns/'+editingCampaignId,{method:'PUT',body:JSON.stringify({config})}); // Save custom fields via API if(slug){ // Delete existing custom fields first (simple approach: clear and recreate) try{ const existing=await api('/campaigns/'+slug+'/custom-fields'); if(existing?.fields){ for(const f of existing.fields){ await api('/campaigns/'+slug+'/custom-fields/'+f.id,{method:'DELETE'}); } } }catch(ex){/* ok */} for(const cf of customFields){ try{ await api('/campaigns/'+slug+'/custom-fields',{method:'POST',body:JSON.stringify(cf)}); }catch(ex){/* skip errors */} } } showToast('Campaign config saved'); } else { showToast('Please create the campaign first, then edit its config','error'); } btn.closest('.modal-overlay')?.remove(); renderCampaigns(); }catch(e){ showToast('Error: '+e.message,'error'); btn.textContent='Save Campaign Config';btn.disabled=false; } } /* ===== LOYALTY ===== */ async function renderLoyalty(){ let rewards; try{rewards=await api('/loyalty/rewards')}catch(e){rewards=[]} if(!Array.isArray(rewards))rewards=rewards?.data||rewards?.rewards||[]; const mc=$('main-content'); mc.innerHTML='

⭐ Loyalty Program

' +'
' + '
'+(Array.isArray(rewards)?rewards.length:0)+'
Reward Claims
' + '
'+rewards.filter(r=>r.status==='pending').length+'
Pending Approval
' +'
' +'
' +'' +(Array.isArray(rewards)&&rewards.length?rewards.slice(0,50).map(r=>'').join(''):'') +'
MemberPointsRewardStatusActions
'+esc(r.member_name||r.member_id?.slice(0,8)||'--')+''+(r.points||0)+''+esc(r.reward_name||r.reward||'--')+''+esc(r.status||'pending')+''+(r.status==='pending'?' ':'--')+'
No rewards yet
'; } async function approveReward(id){ try{await api('/loyalty/rewards/'+id+'/approve',{method:'POST'});showToast('Approved');renderLoyalty();} catch(e){showToast(e.message,'error')} } async function denyReward(id){ try{await api('/loyalty/rewards/'+id+'/deny',{method:'POST'});showToast('Denied');renderLoyalty();} catch(e){showToast(e.message,'error')} } /* ===== ENTRIES ===== */ let entries=[],entPage=1; async function renderEntries(){ try{entries=await api('/entries')}catch(e){entries=[]} if(!Array.isArray(entries))entries=entries?.data||entries?.entries||[]; renderEntriesTable(); } function renderEntriesTable(){ const start=(entPage-1)*20; const page=entries.slice(start,start+20); const tp=Math.ceil(entries.length/20)||1; const mc=$('main-content'); mc.innerHTML='

πŸ“ Entries

' +'
' +page.map(e=>'').join('') +'
IDCampaignContactStatusDate
'+esc(e.id?.slice(0,8)||'--')+''+esc(e.campaign_name||e.campaign?.name||'--')+''+esc(e.contact_name||e.contact?.name||e.email||'--')+''+esc(e.status||'pending')+''+(e.created_at?new Date(e.created_at).toLocaleDateString():'--')+'
' +''; } /* ===== RAFFLES ===== */ let raffles=[],rafPage=1; async function renderRaffles(){ try{raffles=await api('/raffles')}catch(e){raffles=[]} if(!Array.isArray(raffles))raffles=raffles?.data||[]; renderRafflesTable(); } function renderRafflesTable(){ const start=(rafPage-1)*20; const page=raffles.slice(start,start+20); const tp=Math.ceil(raffles.length/20)||1; const mc=$('main-content'); mc.innerHTML='

🎲 Raffles / Giveaways

' +'
' +page.map(r=>'').join('') +'
NameSlugEntriesStatusActions
'+esc(r.name||'--')+''+esc(r.slug||'--')+''+(r.entry_count||0)+''+esc(r.status||'draft')+'
' +''; } async function drawRaffle(slug){ try{const r=await api('/raffles/'+slug+'/draw',{method:'POST'});showToast('Winner drawn: '+(r.winner||'see results'));renderRaffles();} catch(e){showToast(e.message,'error')} } /* ===== CONTACTS ===== */ let contacts=[],conPage=1,conSearch=''; async function renderContacts(){ try{contacts=await api('/contacts')}catch(e){contacts=[]} if(!Array.isArray(contacts))contacts=contacts?.data||[]; renderContactsTable(); } function renderContactsTable(){ const start=(conPage-1)*20; const filtered=contacts.filter(c=>!conSearch||(c.name||c.email||'').toLowerCase().includes(conSearch.toLowerCase())); const page=filtered.slice(start,start+20); const tp=Math.ceil(filtered.length/20)||1; const mc=$('main-content'); mc.innerHTML='

πŸ‘₯ Contacts

' +'
' +'
' +page.map(c=>'').join('') +'
NameEmailPhoneSource
'+esc(c.name||'--')+''+esc(c.email||'--')+''+esc(c.phone||'--')+''+esc(c.source||'--')+'
' +''; } /* ===== API KEYS ===== */ let apikeys=[],akPage=1; async function renderApiKeys(){ try{apikeys=await api('/api-keys')}catch(e){apikeys=[]} if(!Array.isArray(apikeys))apikeys=apikeys?.data||[]; renderApiKeysTable(); } function renderApiKeysTable(){ const start=(akPage-1)*20; const page=apikeys.slice(start,start+20); const tp=Math.ceil(apikeys.length/20)||1; const mc=$('main-content'); mc.innerHTML='
πŸ’‘ Connect to FunnelSwift: Create an API key below, then paste it into FunnelSwift under Settings β†’ Connect Apps β†’ IncentiveSwift to share your campaigns in the FunnelSwift mobile app.
' +'

πŸ”‘ API Keys

' +'
' +page.map(k=>'').join('') +'
NameKey PreviewActiveActions
'+esc(k.name||'--')+''+esc((k.key||k.api_key||'').slice(0,12)+'...')+''+(!k.revoked?'Active':'Revoked')+'
' +''; } async function createApiKey(){ const name=prompt('API Key name:'); if(!name)return; try{const r=await api('/api-keys',{method:'POST',body:JSON.stringify({name})});var fullKey=r.full_key||r.key;showToast('Key created!');alert('Your new API key (copy this now β€” it won\'t be shown again):\n\n'+fullKey+'\n\nPaste this in FunnelSwift β†’ Settings β†’ Connect Apps β†’ IncentiveSwift');renderApiKeys();} catch(e){showToast(e.message,'error')} } async function deleteApiKey(id){ if(!confirm('Revoke this API key?'))return; try{await api('/api-keys/'+id,{method:'DELETE'});renderApiKeys();showToast('Revoked');} catch(e){showToast(e.message,'error')} } /* ===== PORTFOLIO COMPANIES ===== */ let portfolioCos=[],pcPage=1,pcSearch='',pcSelected=[]; async function renderPortfolios(){ try{const r=await api('/portfolio-companies');portfolioCos=r.companies||[]}catch(e){portfolioCos=[]} if(!Array.isArray(portfolioCos))portfolioCos=portfolioCos?.data||[]; renderPortfoliosTable(); } function renderPortfoliosTable(){ const start=(pcPage-1)*20; const filtered=portfolioCos.filter(c=>!pcSearch||(c.name||c.email||c.slug||'').toLowerCase().includes(pcSearch.toLowerCase())); const page=filtered.slice(start,start+20); const tp=Math.ceil(filtered.length/20)||1; const allSelected=pcSelected.length===page.length&&page.length>0; const mc=$('main-content'); mc.innerHTML='

🏒 Portfolio Companies

' +'
' +'' +'
' +'
' +'
' +'' +'' +page.map(c=>{ const sel=pcSelected.includes(c.id); return '' +'' +'' +''; }).join('') +'
NameSlugEmailActions
'+esc(c.name||'--')+''+esc(c.slug||'--')+''+esc(c.email||'--')+'' +'
' +''; } function togglePCSelect(id,checked){ if(checked&&!pcSelected.includes(id))pcSelected.push(id); else if(!checked)pcSelected=pcSelected.filter(x=>x!==id); showPCBulkBar(); } function showPCBulkBar(){ const bar=$('pc-bulk-bar'); if(!bar)return; if(pcSelected.length===0){bar.style.display='none';return} bar.style.display='flex'; bar.innerHTML=''+pcSelected.length+' selected' +''; } async function bulkDeleteCompanies(){ if(!confirm('Delete '+pcSelected.length+' companies?'))return; let ok=0,fail=0; for(const id of pcSelected){ try{await api('/portfolio-companies/'+id,{method:'DELETE'});ok++} catch(e){fail++} } showToast('Deleted '+ok+' companies'+(fail?' ('+fail+' failed)':'')); pcSelected=[]; renderPortfolios(); } async function deleteCompany(id){ if(!confirm('Delete this portfolio company?'))return; try{ await api('/portfolio-companies/'+id,{method:'DELETE'}); showToast('Deleted'); renderPortfolios(); }catch(e){showToast(e.message,'error')} } function showCompanyForm(existing){ const overlay=document.createElement('div'); overlay.className='modal-overlay'; overlay.onclick=e=>{if(e.target===overlay)overlay.remove()}; const data=existing||{name:'',email:'',slug:'',settings:{}}; const mk=existing?.settings?.marketing_boost_api_key||''; overlay.innerHTML=''; document.body.appendChild(overlay); } async function saveCompany(btn,isEdit){ const form=btn.closest('.modal').querySelector('#company-form'); const fd=new FormData(form); const mk=fd.get('marketing_boost_api_key')||''; btn.textContent='Saving...';btn.disabled=true; try{ const payload={ name: fd.get('name'), email: fd.get('email'), slug: fd.get('slug'), settings: {} }; if(mk) payload.settings.marketing_boost_api_key=mk; if(isEdit){ await api('/portfolio-companies/'+portEditId,{method:'PUT',body:JSON.stringify(payload)}); showToast('Company updated'); } else { await api('/portfolio-companies',{method:'POST',body:JSON.stringify(payload)}); showToast('Company created'); } btn.closest('.modal-overlay').remove(); renderPortfolios(); }catch(e){showToast(e.message,'error');btn.textContent=isEdit?'Update':'Create';btn.disabled=false} } let portEditId=null; function editCompany(id){ const c=portfolioCos.find(x=>x.id===id); if(c){portEditId=id;showCompanyForm(c)} } /* ===== INTEGRATIONS ===== */ let integrations=[],intPage=1; async function renderIntegrations(){ try{integrations=await api('/integration-targets')}catch(e){integrations=[]} if(!Array.isArray(integrations))integrations=integrations?.data||[]; renderIntegrationsTable(); } function renderIntegrationsTable(){ const start=(intPage-1)*20; const page=integrations.slice(start,start+20); const tp=Math.ceil(integrations.length/20)||1; const mc=$('main-content'); mc.innerHTML='

πŸ”— Integrations Hub

' +'
' +page.map(i=>{const domains=(i.allowed_domains||[]).length?i.allowed_domains.join(', '):'any';return '' +'' +'' +'' +'';}).join('') +'
NameProviderWebhook URLAllowed DomainsDaily LimitStatusActions
'+esc(i.name||'--')+''+esc(i.provider||'--')+''+esc((i.webhook_url||i.url||'').slice(0,35)+'...')+''+domains+''+(i.daily_limit||1000)+'/day'+(i.is_active!==false?'Active':'Inactive')+' ' +'
' +'
' +'πŸ”’ Security: Webhook URLs validated against allowed domains. Each integration limited to 1000 calls/day. Blocked calls are logged.' +'
' +''; } function showIntegrationForm(existing){ const overlay=document.createElement('div'); overlay.className='modal-overlay'; overlay.onclick=e=>{if(e.target===overlay)overlay.remove()}; const isEdit=!!existing; const data=existing||{name:'',provider:'',webhook_url:'',api_key:'',events:['on_win'],allowed_domains:[],daily_limit:1000}; const providers=['mailgun','sendgrid','sendiio','letterman','nexweave','sam_gov','marketing_boost','webhook']; overlay.innerHTML=''; document.body.appendChild(overlay); } function toggleMbOfferField(provider){ const el=document.getElementById('mb-offer-field'); if(el)el.style.display=provider==='marketing_boost'?'':'none'; } async function saveIntegration(btn,isEdit){ const form=btn.closest('.modal').querySelector('#integration-form'); const fd=new FormData(form); const offerId = (fd.get('mb_offer_id')||'').trim(); const data={ name: fd.get('name'), provider: fd.get('provider'), webhook_url: fd.get('webhook_url'), api_key: fd.get('api_key'), events: Array.from(form.querySelectorAll('input[name=events]:checked')).map(e=>e.value), allowed_domains: (fd.get('allowed_domains')||'').split(',').map(s=>s.trim()).filter(Boolean), daily_limit: parseInt(fd.get('daily_limit'))||1000, provider_metadata: offerId ? { offer_id: offerId } : null }; btn.textContent='Saving...';btn.disabled=true; try{ await api('/integration-targets',{method:'POST',body:JSON.stringify(data)}); showToast('Integration '+(isEdit?'updated':'created')); btn.closest('.modal-overlay').remove(); renderIntegrations(); }catch(e){showToast(e.message,'error');btn.textContent='Save';btn.disabled=false} } function editIntegration(id){ const i=integrations.find(x=>x.id===id); if(i)showIntegrationForm(i); } async function deleteIntegration(id){ if(!confirm('Delete this integration?'))return; try{await api('/integration-targets/'+id,{method:'DELETE'});showToast('Deleted');renderIntegrations();} catch(e){showToast(e.message,'error')} } /* ===== PROVIDER KEYS ===== */ let providerKeys=[],pkPage=1; async function renderProviderKeys(){ try{const r=await api('/provider-keys');providerKeys=r.items||r.data||[]}catch(e){providerKeys=[]} if(!Array.isArray(providerKeys))providerKeys=[]; renderProviderKeysTable(); } function renderProviderKeysTable(){ const start=(pkPage-1)*20; const page=providerKeys.slice(start,start+20); const tp=Math.ceil(providerKeys.length/20)||1; const mc=$('main-content'); mc.innerHTML='
πŸ’‘ Provider Keys are third-party API keys that IncentiveSwift uses on your behalf. Add keys for Marketing Boost, OpenAI, and other providers so your campaigns can use them automatically.
' +'

πŸ” Provider Keys

' +'
' +page.map(p=>'' +'' +'' +'').join('') +'
ProviderAPI KeyBase URLStatusScopeActions
'+esc(p.provider_name||p.provider||'--')+''+esc(p.api_key_masked||'***')+''+esc(p.base_url||'--')+''+(p.is_active!==false?'Active':'Inactive')+''+esc(p.scope||'account')+' ' +'
' +''; } function showProviderKeyForm(existing){ const overlay=document.createElement('div'); overlay.className='modal-overlay'; overlay.onclick=e=>{if(e.target===overlay)overlay.remove()}; const data=existing||{provider:'',api_key:'',base_url:'',is_active:true,scope:'account'}; overlay.innerHTML=''; document.body.appendChild(overlay); } function fillPkBaseUrl(){ const sel=document.getElementById('pk-provider-select'); const url=document.getElementById('pk-base-url'); if(!sel||!url)return; const defaults={ marketing_boost: 'https://api.marketingboost.com/v1', openai: 'https://api.openai.com/v1', anthropic: 'https://api.anthropic.com/v1', stripe: 'https://api.stripe.com/v1', sendgrid: 'https://api.sendgrid.com/v3', mailgun: 'https://api.mailgun.net/v3', deepseek: 'https://api.deepseek.com/v1', coreswift: 'https://api.coreswiftcrm.com/v1', }; const provider=sel.value; if(!url.value&&defaults[provider])url.value=defaults[provider]; } async function saveProviderKey(btn,isEdit){ const form=btn.closest('.modal').querySelector('#provider-key-form'); const fd=new FormData(form); const provider=fd.get('provider'); if(!provider){showToast('Please select a provider','error');return;} const apiKey=fd.get('api_key'); if(!apiKey&&!isEdit){showToast('API key is required','error');return;} const data={ provider: provider, api_key: apiKey||undefined, base_url: fd.get('base_url')||null, is_active: !!fd.get('is_active'), scope: fd.get('scope')||'account' }; btn.textContent='Saving...';btn.disabled=true; try{ await api('/provider-keys',{method:'POST',body:JSON.stringify(data)}); showToast('Provider key '+(isEdit?'updated':'saved')); btn.closest('.modal-overlay').remove(); renderProviderKeys(); }catch(e){showToast(e.message,'error');btn.textContent=isEdit?'Update':'Add Key';btn.disabled=false} } function editProviderKey(id){ const p=providerKeys.find(x=>x.id===id); if(p)showProviderKeyForm(p); } async function deleteProviderKey(pname){ if(!confirm('Delete this provider key?'))return; try{await api('/provider-keys/'+pname,{method:'DELETE'});showToast('Deleted');renderProviderKeys();} catch(e){showToast(e.message,'error')} } function showIntegrationsCampaignPicker(integrationId,integrationName){ const overlay=document.createElement('div'); overlay.className='modal-overlay'; overlay.onclick=e=>{if(e.target===overlay)overlay.remove()}; overlay.innerHTML=''; document.body.appendChild(overlay); filterIntCampaigns(); } function filterIntCampaigns(){ const q=(document.getElementById('int-campaign-search')?.value||'').toLowerCase(); const list=document.getElementById('int-campaign-list'); if(!list)return; const filtered=campaigns.filter(c=>c.name?.toLowerCase().includes(q)||c.slug?.toLowerCase().includes(q)); list.innerHTML=filtered.map(c=>{ const isLinked=c.config?.linked_integrations?.some(l=>l.integration_id===integrationTargetId); return '
' +''+esc(c.name||'')+'' +''+(isLinked?'βœ… Linked':'πŸ”— Click to link')+'' +'
'; }).join(''); if(filtered.length===0)list.innerHTML='

No campaigns found

'; } let integrationTargetId=null; async function linkIntToCampaign(campaignId,integrationId){ integrationTargetId=integrationId; try{ await api('/api/v1/campaigns/'+campaignId+'/integrations',{ method:'POST', body:JSON.stringify({integration_id:integrationId,trigger_events:['on_win','on_entry'],enabled:true}) }); showToast('Integration linked βœ“'); filterIntCampaigns(); // refresh }catch(e){showToast('Error: '+e.message,'error')} } /* ===== PLANS ===== */ let plans=[],plPage=1,plSearch=''; async function renderPlans(){ try{const r=await api('/admin/plans');plans=r.plans||[]}catch(e){plans=[]} if(!Array.isArray(plans))plans=plans?.data||plans?.plans||[]; renderPlansTable(); } function renderPlansTable(){ const start=(plPage-1)*20; const filtered=plans.filter(p=>!plSearch||(p.name||'').toLowerCase().includes(plSearch.toLowerCase())); const page=filtered.slice(start,start+20); const tp=Math.ceil(filtered.length/20)||1; const mc=$('main-content'); mc.innerHTML='

πŸ“‹ Plans

' +'
' +'
' +page.map(p=>'').join('') +'
NamePriceFeaturesStatusActions
'+esc(p.name||'--')+''+(p.price_monthly?'$'+Number(p.price_monthly).toFixed(2):'--')+''+(Array.isArray(p.features)?p.features.length+' features':'--')+''+(p.is_active!==false?'Active':'Inactive')+'
' +''; } async function deletePlan(id){ if(!confirm('Are you sure you want to delete this plan? This cannot be undone.'))return; try{ await api('/admin/plans/'+id,{method:'DELETE'}); showToast('Plan deleted'); renderPlans(); }catch(e){showToast(e.message,'error')} } function showPlanForm(p){ const overlay=document.createElement('div'); overlay.className='modal-overlay'; overlay.onclick=e=>{if(e.target===overlay)overlay.remove()}; const edit=!!p.id; const f=p.features||{}; overlay.innerHTML=''; document.body.appendChild(overlay); } async function savePlanForm(btn,editId){ const form=btn.closest('.modal').querySelector('#plan-form'); const fd=new FormData(form); btn.textContent='Saving...';btn.disabled=true; try{ const features={ credits_monthly: parseInt(fd.get('credits_monthly'))||0, credits_overdraft: parseInt(fd.get('credits_overdraft'))||0, cost_spin: parseInt(fd.get('cost_spin'))||2, cost_chat: parseInt(fd.get('cost_chat'))||5, cost_sms: parseInt(fd.get('cost_sms'))||10, cost_quiz: parseInt(fd.get('cost_quiz'))||3, cost_raffle: parseInt(fd.get('cost_raffle'))||2, cost_email: parseInt(fd.get('cost_email'))||1, cost_checkin: parseInt(fd.get('cost_checkin'))||1, cost_photo: parseInt(fd.get('cost_photo'))||3, }; const payload={ name: fd.get('name'), slug: fd.get('slug')||undefined, description: fd.get('description')||undefined, price_monthly: parseFloat(fd.get('price_monthly'))||0, price_yearly: parseFloat(fd.get('price_yearly'))||0, features: features, sort_order: parseInt(fd.get('sort_order'))||0, }; if(editId){ await api('/admin/plans/'+editId,{method:'PUT',body:JSON.stringify(payload)}); showToast('Plan updated'); } else { await api('/admin/plans',{method:'POST',body:JSON.stringify(payload)}); showToast('Plan created'); } btn.closest('.modal-overlay').remove(); renderPlans(); }catch(e){showToast(e.message,'error');btn.textContent=editId?'Update':'Create';btn.disabled=false} } function editPlan(id){ const p=plans.find(x=>x.id===id); if(p)showPlanForm(p); } /* ===== QUIZ QUESTIONS MANAGER ===== */ let questionRows=[]; async function loadCampaignQuestions(){ if(!editingCampaignId)return; try{ const slug=campaigns.find(c=>c.id===editingCampaignId)?.slug; if(!slug)return; const res=await api('/campaigns/'+slug+'/questions'); questionRows=res.questions||[]; renderQuestionEditor(); }catch(e){ questionRows=[]; renderQuestionEditor(); } } function renderQuestionEditor(){ const container=$('questions-editor'); if(!container)return; if(questionRows.length===0){ container.innerHTML='

No questions yet. Add your first question below.

'; return; } let html='' +''; questionRows.forEach((q,i)=>{ html+='' +'' +'' +'' +'' +'' +'' +'' +''; }); html+='
KeyQuestion TextTypeOptionsCorrect AnswerCRM Field
'; container.innerHTML=html; } function addQuestionRow(){ questionRows.push({ id:null,question_key:'q'+(questionRows.length+1),question_text:'',question_type:'single', options:null,correct_answer:'',score_weight:1,crm_field:'',sort_order:questionRows.length }); renderQuestionEditor(); } async function deleteQuestion(idx){ const q=questionRows[idx]; if(q.id){ const slug=campaigns.find(c=>c.id===editingCampaignId)?.slug; if(slug)await api('/campaigns/'+slug+'/questions/'+q.id,{method:'DELETE'}); } questionRows.splice(idx,1); renderQuestionEditor(); } async function saveQuestions(){ const slug=campaigns.find(c=>c.id===editingCampaignId)?.slug; if(!slug)return; const editor=$('questions-editor'); if(!editor)return; // Collect from table rows const rows=editor.querySelectorAll('tr:not(:first-child)'); const updates=[]; rows.forEach((tr,i)=>{ const fields={ id:questionRows[i]?.id||null, question_key:tr.querySelector('.q-key')?.value||'q'+(i+1), question_text:tr.querySelector('.q-text')?.value||'', question_type:tr.querySelector('.q-type')?.value||'single', options:tr.querySelector('.q-options')?.value.split(',').map(s=>s.trim()).filter(s=>s)||null, correct_answer:tr.querySelector('.q-correct')?.value||'', crm_field:tr.querySelector('.q-crm')?.value||'', sort_order:i, }; updates.push(fields); }); // Save each question for(const f of updates){ const body={ question_key:f.question_key, question_text:f.question_text, question_type:f.question_type, sort_order:f.sort_order, correct_answer:f.correct_answer||null, score_weight:1, options:f.options?{values:f.options}:null, crm_field:f.crm_field||null, crm_field_type:f.crm_field?'custom_field':null, }; try{ if(f.id){ await api('/campaigns/'+slug+'/questions/'+f.id,{method:'PUT',body:JSON.stringify(body)}); }else if(f.question_text){ await api('/campaigns/'+slug+'/questions',{method:'POST',body:JSON.stringify(body)}); } }catch(e){console.error('Question save failed:',e);} } } // Hook into showCampaignEditor to load questions const origShowEditor=showCampaignEditor; showCampaignEditor=function(campaign){ origShowEditor(campaign); setTimeout(()=>loadCampaignQuestions(),100); }; // Hook into saveCampaignEditor to save questions const origSaveEditor=saveCampaignEditor; saveCampaignEditor=async function(btn){ await saveQuestions(); await origSaveEditor(btn); }; /* ===== IQS FUNNELS ===== */ let iqsFunnels=[],iqsPage=1,iqsSearch='',editingIqsFunnelId=null,editingIqsQns=[],editingIqsRules=[]; async function renderIqs(){ const mc=$('main-content'); mc.innerHTML='
Loading IQS Funnels...
'; try{ const r=await api('/iqs/funnels'); iqsFunnels=r.data||[]; }catch(e){iqsFunnels=[]} if(!Array.isArray(iqsFunnels))iqsFunnels=[]; renderIqsTable(); } function renderIqsTable(){ const start=(iqsPage-1)*20; const filtered=iqsFunnels.filter(f=>!iqsSearch||(f.name||'').toLowerCase().includes(iqsSearch.toLowerCase())||(f.slug||'').toLowerCase().includes(iqsSearch.toLowerCase())); const page=filtered.slice(start,start+20); const tp=Math.ceil(filtered.length/20)||1; const mc=$('main-content'); mc.innerHTML='

🧠 Intelligent Qualifying Surveys

' +'
' +'' +''+filtered.length+' funnels' +'
' +'
' +page.map(f=>{ const status=f.status||'draft'; const badgeCls=status==='active'?'active':status==='archived'?'inactive':'draft'; return '' +'' +'' +'' +'' +'' +'' +'' +''; }).join('') +'
NameTypeSlugStatusResponsesCreatedActions
'+esc(f.name||'--')+''+esc(f.funnel_type||'survey')+''+esc(f.slug||'--')+''+status+''+f.response_count+''+new Date(f.created_at).toLocaleDateString()+'
' +'' +'' +'' +'' +'' +'
' +'' +'
' +'πŸ’‘ IQS Funnels are intelligent qualifying surveys. Each funnel has a configurable score threshold. ' +'Responses above the threshold are automatically tagged as qualified β€” you can set answer scoring and answerβ†’tag mappings in each question\'s options.' +'
'; } function showIqsForm(existingFunnel){ const overlay=document.createElement('div'); overlay.className='modal-overlay'; overlay.onclick=e=>{if(e.target===overlay)overlay.remove()}; const isEdit=!!existingFunnel; const f=existingFunnel||{}; overlay.innerHTML=''; document.body.appendChild(overlay); } async function saveIqsFunnel(btn,id){ const form=btn.closest('.modal').querySelector('form'); const fd=new FormData(form); const config={ passing_score: parseInt(fd.get('passing_score'))||70, max_attempts: parseInt(fd.get('max_attempts'))||1, collect_email: !!fd.get('collect_email'), collect_name: !!fd.get('collect_name'), collect_phone: !!fd.get('collect_phone'), show_progress_bar: !!fd.get('show_progress'), allow_skip: !!fd.get('allow_skip'), display_mode: fd.get('display_mode')||'step_by_step', redirect_url: fd.get('redirect_url')||null, post_submit: { qualified_title: fd.get('qualified_title')||null, qualified_subtitle: fd.get('qualified_subtitle')||null, qualified_redirect_url: fd.get('qualified_redirect_url')||null, disqualified_title: fd.get('disqualified_title')||null, disqualified_subtitle: fd.get('disqualified_subtitle')||null, disqualified_redirect_url: fd.get('disqualified_redirect_url')||null } }; const theme={ preset: fd.get('theme_preset')||'dark_modern', accent_color: fd.get('accent_color')||'#8b5cf6', button_style: fd.get('button_style')||'rounded' }; const payload={ name: fd.get('name'), slug: fd.get('slug')||undefined, description: fd.get('description')||null, funnel_type: fd.get('funnel_type')||'survey', status: fd.get('status')||'draft', source_tag: fd.get('source_tag')||null, config: config, theme: theme }; btn.textContent='Saving...';btn.disabled=true; try{ if(id){ await api('/iqs/funnels/'+id,{method:'PUT',body:JSON.stringify(payload)}); showToast('Funnel updated'); } else { const res=await api('/iqs/funnels',{method:'POST',body:JSON.stringify(payload)}); showToast('Funnel created! Slug: '+esc(res.slug||'')); } btn.closest('.modal-overlay').remove(); renderIqs(); }catch(e){showToast(e.message,'error');btn.textContent=id?'Update':'Create';btn.disabled=false} } async function deleteIqsFunnel(id){ if(!confirm('Delete this IQS funnel and all its data (questions, rules, submissions)?'))return; try{await api('/iqs/funnels/'+id,{method:'DELETE'});showToast('Deleted');renderIqs()} catch(e){showToast(e.message,'error')} } function showIqsPlayPreview(slug){ window.open('https://app.incentiveswift.com/iqs/play/'+slug,'_blank'); } // ===== IQS FUNNEL EDITOR (Questions + Settings) ===== let iqsQns=[],iqsRules=[]; async function showIqsEditor(funnelId){ editingIqsFunnelId=funnelId; const mc=$('main-content'); mc.innerHTML='
Loading funnel editor...
'; try{ const [fRes,qRes]=await Promise.all([ api('/iqs/funnels/'+funnelId), api('/iqs/funnels/'+funnelId+'/questions') ]); const funnel=fRes.data; iqsQns=qRes.data||[]; if(!Array.isArray(iqsQns))iqsQns=[]; renderIqsEditor(funnel); }catch(e){ showToast('Failed to load funnel: '+e.message,'error'); renderIqs(); } } function renderIqsEditor(funnel){ const mc=$('main-content'); mc.innerHTML='

🧠 '+esc(funnel.name)+'

' +'
' +'' +'' +'' +'
' +'
' +'
' +'
' +'

πŸ“ Questions  ('+iqsQns.length+' total)

' +'' +'
' +'
' +'
' +'
'; renderIqsQuestionList(); } function renderIqsQuestionList(){ const container=$('iqs-question-list'); if(!container)return; if(!iqsQns||iqsQns.length===0){ container.innerHTML='

No questions yet. Click "+ Add Question" to build your survey.

'; return; } let html=''; iqsQns.forEach((q,i)=>{ const typeLabels={single_choice:'β˜‘οΈ Single',multiple_choice:'βœ… Multiple',text:'πŸ“ Text',numeric:'πŸ”’ Numeric',email:'πŸ“§ Email',phone:'πŸ“ž Phone',rating:'⭐ Rating',field_consent:'πŸ“‹ Consent'}; const typeLabel=typeLabels[q.question_type]||q.question_type; const opts=q.options; const optionPreview=Array.isArray(opts)&&opts.length>0 ? '
'+opts.map(o=>{ const label=o.label||o.value||''; const score=o.score?' +'+o.score+'pts':''; const tag=o.tag?' 🏷️'+o.tag:''; return ''+esc(label)+''+score+tag+''; }).join('')+'
' : ''; html+='
' +'
' +'
' +'
' +'β Ώ' +''+esc(q.question_text||'Untitled')+'' +''+esc(q.question_key)+'' +'
' +'
' +''+typeLabel+'' +'πŸ”’ Sort: '+(q.sort_order||i)+'' +''+(q.required?'πŸ”΄ Required':'βšͺ Optional')+'' +'
' +optionPreview +'
' +'
' +'' +'' +'
' +'
' +'
'; }); // Reorder buttons html+='
' +'' +'' +'
'; container.innerHTML=html; } function addIqsQuestion(){ const newQ={ question_key:'q'+(iqsQns.length+1), question_text:'', question_type:'single_choice', required:true, options:[{label:'Option 1',value:'opt1',score:10,tag:''},{label:'Option 2',value:'opt2',score:5,tag:''}], config:{}, sort_order:iqsQns.length }; iqsQns.push(newQ); editIqsQuestion(iqsQns.length-1); } function editIqsQuestion(idx){ const q=iqsQns[idx]; if(!q)return; const overlay=document.createElement('div'); overlay.className='modal-overlay'; overlay.onclick=e=>{if(e.target===overlay)overlay.remove()}; overlay.innerHTML=''; document.body.appendChild(overlay); // Populate options renderIqsOptionRows(q); toggleIqsQuestionTypeOptions(q.question_type); } function toggleIqsQuestionTypeOptions(val){ const section=$('iqs-options-section'); if(!section)return; const hasOptions=['single_choice','multiple_choice'].includes(val); section.style.display=hasOptions?'block':'none'; } function renderIqsOptionRows(q){ const container=$('iqs-option-rows'); if(!container)return; const opts=q&&Array.isArray(q.options)?q.options:[{label:'Option 1',value:'opt1',score:10,tag:''},{label:'Option 2',value:'opt2',score:5,tag:''}]; let html=''; opts.forEach((o,i)=>{ html+='
' +'' +'' +'' +'' +'' +'
'; }); container.innerHTML=html; } function addIqsOptionRow(){ const container=$('iqs-option-rows'); if(!container)return; const div=document.createElement('div'); div.className='iqs-option-row'; div.style='display:grid;grid-template-columns:1fr 120px 80px 120px 30px;gap:6px;align-items:center;margin-bottom:4px'; div.innerHTML='' +'' +'' +'' +''; container.appendChild(div); } function removeIqsOptionRow(btn){btn.closest('.iqs-option-row').remove()} async function saveIqsQuestion(btn,idx){ const q=iqsQns[idx]; if(!q)return; const form=btn.closest('.modal').querySelector('form'); const fd=new FormData(form); // Collect options from rows const container=$('iqs-option-rows'); let options=null; if(container){ const rows=container.querySelectorAll('.iqs-option-row'); options=Array.from(rows).map(row=>({ label: row.querySelector('.iqs-opt-label').value, value: row.querySelector('.iqs-opt-value').value, score: parseInt(row.querySelector('.iqs-opt-score').value)||0, tag: row.querySelector('.iqs-opt-tag').value||'' })).filter(o=>o.label||o.value); } const question_type=fd.get('question_type'); const updated={ question_key: fd.get('question_key')||'q'+(idx+1), question_text: fd.get('question_text'), question_type: question_type, required: !!fd.get('required'), options: ['single_choice','multiple_choice'].includes(question_type)?options:null, config: {}, sort_order: q.sort_order||idx }; btn.textContent='Saving...';btn.disabled=true; try{ if(q.id){ await api('/iqs/funnels/'+editingIqsFunnelId+'/questions/'+q.id,{method:'PUT',body:JSON.stringify(updated)}); } else { const res=await api('/iqs/funnels/'+editingIqsFunnelId+'/questions',{method:'POST',body:JSON.stringify(updated)}); q.id=res.data?.id||null; } Object.assign(q,updated); btn.closest('.modal-overlay').remove(); renderIqsQuestionList(); showToast('Question saved'); }catch(e){showToast(e.message,'error');btn.textContent='Save';btn.disabled=false} } async function deleteIqsQuestion(idx,qid){ if(!confirm('Delete this question?'))return; if(qid){ try{await api('/iqs/funnels/'+editingIqsFunnelId+'/questions/'+qid,{method:'DELETE'})} catch(e){showToast(e.message,'error');return} } iqsQns.splice(idx,1); renderIqsQuestionList(); } async function reorderIqsQuestions(){ const ids=iqsQns.map(q=>q.id).filter(Boolean); if(ids.length<2)return; try{ await api('/iqs/funnels/'+editingIqsFunnelId+'/questions/reorder',{method:'PUT',body:JSON.stringify({question_ids:ids})}); showToast('Order saved'); }catch(e){showToast(e.message,'error')} } function showIqsQuestionBank(){ const mc=$('main-content'); mc.innerHTML='

πŸ“‹ Question Bank

' +'
' +'
' +'

Common qualifying questions you can add to this funnel with one click.

' +'
' +'
'; const bankQuestions=[ {text:'What is your budget range?',type:'single_choice',key:'budget',opts:[{label:'Under $1K',value:'under_1k',score:5,tag:'budget_low'},{label:'$1K-$5K',value:'1k_5k',score:10,tag:'budget_mid'},{label:'$5K-$10K',value:'5k_10k',score:15,tag:'budget_high'},{label:'$10K+',value:'10k_plus',score:20,tag:'budget_premium'}]}, {text:'What is your decision timeline?',type:'single_choice',key:'timeline',opts:[{label:'ASAP (within a week)',value:'asap',score:20,tag:'timeline_urgent'},{label:'1-3 months',value:'1_3_months',score:15,tag:'timeline_soon'},{label:'3-6 months',value:'3_6_months',score:10,tag:'timeline_medium'},{label:'Just researching',value:'researching',score:5,tag:'timeline_long'}]}, {text:'Who is the decision maker?',type:'single_choice',key:'authority',opts:[{label:'I am the decision maker',value:'self',score:20,tag:'authority_self'},{label:'I need approval',value:'need_approval',score:10,tag:'authority_needs_ok'},{label:'Just gathering info',value:'info_only',score:5,tag:'authority_info'}]}, {text:'What is your biggest challenge right now?',type:'single_choice',key:'pain',opts:[{label:'Growing revenue',value:'revenue',score:10,tag:'pain_revenue'},{label:'Saving time',value:'time',score:10,tag:'pain_time'},{label:'Customer retention',value:'retention',score:10,tag:'pain_retention'},{label:'Team productivity',value:'productivity',score:10,tag:'pain_productivity'}]}, {text:'How did you hear about us?',type:'single_choice',key:'source',opts:[{label:'Google Search',value:'google',score:0,tag:'src_google'},{label:'Social Media',value:'social',score:0,tag:'src_social'},{label:'Referral',value:'referral',score:5,tag:'src_referral'},{label:'Email',value:'email',score:0,tag:'src_email'},{label:'Other',value:'other',score:0,tag:'src_other'}]}, {text:'Company size (employees)',type:'single_choice',key:'company_size',opts:[{label:'Just me (1)',value:'1',score:5,tag:'size_1'},{label:'2-10',value:'2_10',score:10,tag:'size_small'},{label:'11-50',value:'11_50',score:15,tag:'size_medium'},{label:'51-200',value:'51_200',score:20,tag:'size_large'},{label:'200+',value:'200_plus',score:25,tag:'size_enterprise'}]}, {text:'Your email address',type:'email',key:'email',opts:null}, {text:'Your phone number',type:'phone',key:'phone',opts:null}, ]; const grid=$('iqs-bank-grid'); if(grid){ grid.innerHTML=bankQuestions.map((bq,i)=>{ return '
' +'
'+esc(bq.text)+'
' +'
'+bq.type+(bq.opts?' Β· '+bq.opts.length+' options':'')+'
' +'
'; }).join(''); // Store bank questions globally for access window.__iqsBankQns=bankQuestions; } } function addIqsBankQuestion(idx){ const bank=window.__iqsBankQns||[]; const bq=bank[idx]; if(!bq)return; iqsQns.push({ question_key: bq.key||'q'+(iqsQns.length+1), question_text: bq.text, question_type: bq.type, required: true, options: bq.opts||null, config: {}, sort_order: iqsQns.length }); showToast('Added: '+bq.text); renderIqsQuestionList(); } // ===== IQS RULES ===== async function showIqsRules(funnelId){ editingIqsFunnelId=funnelId; const mc=$('main-content'); mc.innerHTML='
Loading rules...
'; try{ const [fRes,rRes]=await Promise.all([ api('/iqs/funnels/'+funnelId), api('/iqs/funnels/'+funnelId+'/rules') ]); const funnel=fRes.data; iqsRules=rRes.data||[]; if(!Array.isArray(iqsRules))iqsRules=[]; renderIqsRules(funnel); }catch(e){ showToast('Failed to load rules: '+e.message,'error'); renderIqs(); } } function renderIqsRules(funnel){ const mc=$('main-content'); mc.innerHTML='

βš™οΈ Rules: '+esc(funnel.name)+'

' +'
' +'' +'' +'
' +'
' +'πŸ’‘ Rules define what happens based on submission answers. Each rule has conditions (when certain answers match) and actions (set outcome, tag contact, redirect, etc.)' +'
' +'
'; const container=$('iqs-rules-list'); if(!container)return; if(!iqsRules||iqsRules.length===0){ container.innerHTML='

No rules yet. Rules let you define dynamic outcomes based on answers.

'; return; } container.innerHTML=iqsRules.map((r,i)=>{ const conds=Array.isArray(r.conditions)?r.conditions:[]; const acts=Array.isArray(r.actions)?r.actions:[]; return '
' +'
' +'
' +'
' +''+(r.is_active!==false?'Active':'Inactive')+'' +''+esc(r.rule_type||'always')+'' +'Priority: '+(r.priority||0)+'' +'
' +'
Conditions: ' +(conds.length?conds.map(c=>esc(JSON.stringify(c))).join(' AND '):'Always (no conditions)')+'
' +'
Actions: ' +(acts.length?acts.map(a=>esc(JSON.stringify(a))).join(', '):'No actions')+'
' +'
' +'
' +'' +'' +'
' +'
' +'
'; }).join(''); } function addIqsRule(){ iqsRules.push({ rule_type:'always', priority:iqsRules.length, conditions:[], actions:[], is_active:true }); editIqsRule(iqsRules.length-1); } function editIqsRule(idx){ const r=iqsRules[idx]; if(!r)return; const overlay=document.createElement('div'); overlay.className='modal-overlay'; overlay.onclick=e=>{if(e.target===overlay)overlay.remove()}; overlay.innerHTML=''; document.body.appendChild(overlay); } async function saveIqsRule(btn,idx){ const r=iqsRules[idx]; if(!r)return; const form=btn.closest('.modal').querySelector('form'); const fd=new FormData(form); let conditions,actions; try{conditions=JSON.parse(fd.get('conditions')||'[]')}catch(e){conditions=[]} try{actions=JSON.parse(fd.get('actions')||'[]')}catch(e){actions=[]} const payload={ rule_type: fd.get('rule_type')||'always', priority: parseInt(fd.get('priority'))||0, conditions: conditions, actions: actions, is_active: !!fd.get('is_active') }; btn.textContent='Saving...';btn.disabled=true; try{ if(r.id){ await api('/iqs/funnels/'+editingIqsFunnelId+'/rules/'+r.id,{method:'PUT',body:JSON.stringify(payload)}); } else { const res=await api('/iqs/funnels/'+editingIqsFunnelId+'/rules',{method:'POST',body:JSON.stringify(payload)}); r.id=res.data?.id||null; } Object.assign(r,payload); btn.closest('.modal-overlay').remove(); showIqsRules(editingIqsFunnelId); showToast('Rule saved'); }catch(e){showToast(e.message,'error');btn.textContent='Save';btn.disabled=false} } async function deleteIqsRule(idx,rid){ if(!confirm('Delete this rule?'))return; if(rid){ try{await api('/iqs/funnels/'+editingIqsFunnelId+'/rules/'+rid,{method:'DELETE'})} catch(e){showToast(e.message,'error');return} } iqsRules.splice(idx,1); showIqsRules(editingIqsFunnelId); } // ===== IQS SUBMISSIONS ===== async function showIqsSubmissions(funnelId){ editingIqsFunnelId=funnelId; const mc=$('main-content'); mc.innerHTML='
Loading submissions...
'; try{ const [fRes,sRes]=await Promise.all([ api('/iqs/funnels/'+funnelId), api('/iqs/funnels/'+funnelId+'/submissions') ]); const funnel=fRes.data; const subs=sRes.data||[]; renderIqsSubmissions(funnel,Array.isArray(subs)?subs:[]); }catch(e){ showToast('Failed: '+e.message,'error'); renderIqs(); } } function renderIqsSubmissions(funnel,subs){ const mc=$('main-content'); mc.innerHTML='

πŸ“‹ Responses: '+esc(funnel.name)+'

' +'
' +'' +'
' +'
' +'
'+subs.length+'
Total Responses
' +'
'+subs.filter(s=>s.outcome==='qualified').length+'
Qualified
' +'
'+subs.filter(s=>s.outcome==='disqualified').length+'
Disqualified
' +'
'+funnel.response_count+'
Total Count
' +'
' +'
' +subs.map(s=>{ const tags=Array.isArray(s.tags_applied)&&s.tags_applied.length?s.tags_applied.join(', '):'--'; const outcomeCls=s.outcome==='qualified'?'active':s.outcome==='disqualified'?'inactive':'draft'; return '' +'' +'' +'' +'' +''; }).join('') +'
DateScoreOutcomeTagsContact ID
'+new Date(s.created_at).toLocaleString()+''+s.total_score+''+(s.outcome||'--')+''+esc(tags)+''+(s.contact_id||'').slice(0,8)+'...
'; } /* ===== UTILITIES ===== */ function esc(s){if(!s)return'';const d=document.createElement('div');d.textContent=s;return d.innerHTML} function showToast(msg,type){ const el=document.createElement('div'); el.className=type==='error'?'error':'success'; el.textContent=msg; el.style.position='fixed';el.style.top='16px';el.style.right='16px';el.style.zIndex='200';el.style.maxWidth='400px'; document.body.appendChild(el); setTimeout(()=>el.remove(),3000); } render(); Β© 2026 IncentiveSwift A SwiftSoftware Company. All rights reserved. +a+'').join('') +''; } else if(t==='vacation_incentive'){ return ''; } return '--'; } function normalizeWeights(){ const table=$('prize-table'); const rows=Array.from(table.querySelectorAll('tr')).slice(1); // skip header let total=0; const weights=[]; rows.forEach((r,i)=>{ const inp=r.querySelector('[data-field=weight]'); const w=parseInt(inp.value)||0; weights[i]=w; total+=w; }); // Show normalized % next to each weight // We will not auto-adjust to avoid confusion; just show total if(total>0){ rows.forEach((r,i)=>{ const inp=r.querySelector('[data-field=weight]'); const pct=Math.round((weights[i]/total)*100); inp.style.setProperty('--after',"' ("+pct+"%)'"); }); } } function renderEditorTypeConfig(campaign){ const ct=campaign.campaign_type||campaign.type||'spin_wheel'; const cfg=campaign.config||{}; let html='

🎯 '+typeLabel(ct)+' Config

'; if(ct==='spin_wheel'){ html+=''; } else if(ct==='raffle'){ html+='' +'' +''; } else if(ct==='quiz'||ct==='trivia'){ html+='' +'' +''; } else if(ct==='social_contest'){ html+='' +''; } else if(ct==='referral'){ html+='' +'' +''; } else if(ct==='checkin'){ html+='' +'' +''; } else if(ct==='points'||ct==='purchase'){ html+='' +'' +''; } else if(ct==='photo_contest'||ct==='form'){ html+='' +''; } html+='
'; return html; } function toggleAccordion(id){ const el=document.getElementById(id); if(el)el.classList.toggle('open'); } // ── Campaign Integration Linking ── let campaignIntegrationsCache={}; async function loadCampaignIntegrations(campaignId){ try{ if(!campaignId)return; const r=await api('/api/v1/campaigns/'+campaignId+'/integrations'); campaignIntegrationsCache[campaignId]=r||[]; return campaignIntegrationsCache[campaignId]; }catch(e){console.log('Error loading integrations:',e.message);return []} } function renderIntegrationLinkSection(containerId,campaignId,cfg){ const container=document.getElementById(containerId); if(!container)return; const linked=cfg?.linked_integrations||campaignIntegrationsCache[campaignId]||[]; const linkedIds=new Set(linked.map(l=>l.integration_id||l.id)); let html=''; container.innerHTML=html; } async function toggleCampaignIntegration(checkbox,campaignId,integrationId){ const linked=checkbox.checked; // Optimistic toggle checkbox.disabled=true; try{ if(linked){ await api('/api/v1/campaigns/'+campaignId+'/integrations',{ method:'POST', body:JSON.stringify({integration_id:integrationId,trigger_events:['on_win','on_entry'],enabled:true}) }); }else{ await api('/api/v1/campaigns/'+campaignId+'/integrations/'+integrationId,{method:'DELETE'}); } showToast(linked?'Integration linked βœ“':'Integration unlinked'); // Refresh await loadCampaignIntegrations(campaignId); // Re-render the section if it exists const el=document.getElementById('integration-link-section'); if(el){ const cam=campaigns.find(c=>c.id===campaignId||c.slug===campaignId); renderIntegrationLinkSection('integration-link-section',campaignId,cam?.config||{}); } }catch(e){ checkbox.checked=!linked; showToast('Error: '+e.message,'error'); } checkbox.disabled=false; } async function updateMbOffer(input,campaignId,integrationId){ const offerId=input.value.trim(); try{ await api('/api/v1/campaigns/'+campaignId+'/integrations',{ method:'POST', body:JSON.stringify({ integration_id:integrationId, provider_metadata:{offer_id:offerId||null}, trigger_events:['on_win','on_entry'], enabled:true }) }); showToast(offerId?'Offer ID saved βœ“':'Offer ID cleared'); }catch(e){showToast('Error: '+e.message,'error')} } async function saveCampaignEditor(btn){ btn.textContent='Saving...';btn.disabled=true; try{ const table=$('prize-table'); const rows=Array.from(table.querySelectorAll('tr')).slice(1); const prizes=rows.map((r,i)=>{ return { id: 'p'+(i+1), label: r.querySelector('[data-field=label]').value || 'Prize '+(i+1), color: r.querySelector('[data-field=color]').value, weight: parseInt(r.querySelector('[data-field=weight]').value)||0, prize_type: r.querySelector('[data-field=prize_type]').value, inventory: (function(){const v=r.querySelector('[data-field=inventory]').value;return v===''?null:parseInt(v)})(), marketing_boost: (function(){ const mbType=r.querySelector('[data-field=mb_type]')?.value; if(!mbType)return null; const mb={incentive_type:mbType,enabled:true}; const amtEl=document.getElementById('mb-amt-'+(parseInt(r.dataset.idx||i)||i)); if(amtEl){ const amtInput=amtEl.querySelector('select')||amtEl.querySelector('input'); if(amtInput){ if(mbType==='vacation_incentive')mb.destination=parseInt(amtInput.value); else if(amtInput.value)mb.amount=parseInt(amtInput.value); } } return mb; })() }; }); const totalWeight=prizes.reduce((s,p)=>s+p.weight,0); // Collect per-prize delivery configs const deliveryTable=$('delivery-section'); const deliveryRows=deliveryTable?Array.from(deliveryTable.querySelectorAll('tr')).slice(1):[]; const updatedPrizes=prizes.map((p,i)=>{ const delRow=deliveryRows[i]; if(delRow){ const methodEl=delRow.querySelector('[data-delivery="method"]'); const subjectEl=delRow.querySelector('[data-delivery="subject-or-url"]'); const bodyEl=delRow.querySelector('[data-delivery="body"]'); if(methodEl&&methodEl.value!=='none'){ p.delivery={ method: methodEl.value, subject: methodEl.value==='email'?(subjectEl?.value||'You won {{prize_label}}!'):undefined, body: methodEl.value==='email'?(bodyEl?.value||''):undefined, redirect_url: methodEl.value==='redirect'?(subjectEl?.value||''):undefined, }; } } return p; }); // Collect entry field toggles const entryFields={}; document.querySelectorAll('.entry-field-toggle').forEach(cb=>{ entryFields[cb.dataset.field]=cb.checked; }); // Collect custom fields const customFieldRows=document.querySelectorAll('.custom-field-row'); const customFields=Array.from(customFieldRows).map((row,idx)=>{ const label=row.querySelector('.cf-label')?.value||''; const ftype=row.querySelector('.cf-type')?.value||'text'; const required=row.querySelector('.cf-required')?.checked||false; const options=row.querySelector('.cf-options')?.value||''; return {field_key:'cf_'+(idx+1),field_label:label,field_type:ftype,required,options:options.split(',').map(s=>s.trim()).filter(Boolean),sort_order:idx}; }).filter(cf=>cf.field_label); const ct=campaign?.campaign_type||campaign?.type||'spin_wheel'; const config={ prize_pool: { prizes: updatedPrizes, total_weight: totalWeight, inventory_tracking: $('inv-tracking').checked, allow_when_exhausted: $('allow-exhausted').checked }, pity_timer: { enabled: $('pity-enabled').checked, threshold: parseInt($('pity-threshold').value)||5 }, max_spins_per_day: parseInt($('max-day').value)||0, max_spins_per_campaign: parseInt($('max-campaign').value)||0, entry_fields: entryFields, custom_fields: customFields, delivery: { on_win: { redirect: { url: $('win-url')?.value||'', text: $('win-text')?.value||'' }, autoresponder_fire: $('fire-autoresponder')?.checked||false }, on_lose: { redirect: { url: $('lose-url')?.value||'' } } } }; // Type-specific editor fields if($('ed-win-prob'))config.win_probability=parseInt($('ed-win-prob').value)||10; if($('ed-draw-date'))config.draw_date=$('ed-draw-date').value; if($('ed-max-entries'))config.max_entries_per_person=parseInt($('ed-max-entries').value)||0; if($('ed-auto-draw'))config.auto_draw=$('ed-auto-draw').checked; if($('ed-passing-score'))config.passing_score=parseInt($('ed-passing-score').value)||70; if($('ed-num-questions'))config.num_questions=parseInt($('ed-num-questions').value)||5; if($('ed-shuffle'))config.shuffle_questions=$('ed-shuffle').checked; if($('ed-platform'))config.platform=$('ed-platform').value; if($('ed-action'))config.required_action=$('ed-action').value; if($('ed-ref-points'))config.referral_points=parseInt($('ed-ref-points').value)||10; if($('ed-max-ref'))config.max_referrals=parseInt($('ed-max-ref').value)||0; if($('ed-reward-ref'))config.reward_referrer=$('ed-reward-ref').checked!==false; if($('ed-radius'))config.geo_radius=parseInt($('ed-radius').value)||100; if($('ed-location'))config.location_name=$('ed-location').value; if($('ed-checkin-limit'))config.checkins_per_day=parseInt($('ed-checkin-limit').value)||1; if($('ed-pts-action'))config.points_per_action=parseInt($('ed-pts-action').value)||1; if($('ed-pts-needed'))config.points_needed=parseInt($('ed-pts-needed').value)||100; if($('ed-auto-enroll'))config.auto_enroll=$('ed-auto-enroll').checked; if($('ed-max-sub'))config.max_submissions=parseInt($('ed-max-sub').value)||1; if($('ed-require-approval'))config.require_approval=$('ed-require-approval').checked; if(editingCampaignId){ const campaign=campaigns.find(c=>c.id===editingCampaignId); const slug=campaign?.slug||''; await api('/campaigns/'+editingCampaignId,{method:'PUT',body:JSON.stringify({config})}); // Save custom fields via API if(slug){ // Delete existing custom fields first (simple approach: clear and recreate) try{ const existing=await api('/campaigns/'+slug+'/custom-fields'); if(existing?.fields){ for(const f of existing.fields){ await api('/campaigns/'+slug+'/custom-fields/'+f.id,{method:'DELETE'}); } } }catch(ex){/* ok */} for(const cf of customFields){ try{ await api('/campaigns/'+slug+'/custom-fields',{method:'POST',body:JSON.stringify(cf)}); }catch(ex){/* skip errors */} } } showToast('Campaign config saved'); } else { showToast('Please create the campaign first, then edit its config','error'); } btn.closest('.modal-overlay')?.remove(); renderCampaigns(); }catch(e){ showToast('Error: '+e.message,'error'); btn.textContent='Save Campaign Config';btn.disabled=false; } } /* ===== LOYALTY ===== */ async function renderLoyalty(){ let rewards; try{rewards=await api('/loyalty/rewards')}catch(e){rewards=[]} if(!Array.isArray(rewards))rewards=rewards?.data||rewards?.rewards||[]; const mc=$('main-content'); mc.innerHTML='

⭐ Loyalty Program

' +'
' + '
'+(Array.isArray(rewards)?rewards.length:0)+'
Reward Claims
' + '
'+rewards.filter(r=>r.status==='pending').length+'
Pending Approval
' +'
' +'
' +'' +(Array.isArray(rewards)&&rewards.length?rewards.slice(0,50).map(r=>'').join(''):'') +'
MemberPointsRewardStatusActions
'+esc(r.member_name||r.member_id?.slice(0,8)||'--')+''+(r.points||0)+''+esc(r.reward_name||r.reward||'--')+''+esc(r.status||'pending')+''+(r.status==='pending'?' ':'--')+'
No rewards yet
'; } async function approveReward(id){ try{await api('/loyalty/rewards/'+id+'/approve',{method:'POST'});showToast('Approved');renderLoyalty();} catch(e){showToast(e.message,'error')} } async function denyReward(id){ try{await api('/loyalty/rewards/'+id+'/deny',{method:'POST'});showToast('Denied');renderLoyalty();} catch(e){showToast(e.message,'error')} } /* ===== ENTRIES ===== */ let entries=[],entPage=1; async function renderEntries(){ try{entries=await api('/entries')}catch(e){entries=[]} if(!Array.isArray(entries))entries=entries?.data||entries?.entries||[]; renderEntriesTable(); } function renderEntriesTable(){ const start=(entPage-1)*20; const page=entries.slice(start,start+20); const tp=Math.ceil(entries.length/20)||1; const mc=$('main-content'); mc.innerHTML='

πŸ“ Entries

' +'
' +page.map(e=>'').join('') +'
IDCampaignContactStatusDate
'+esc(e.id?.slice(0,8)||'--')+''+esc(e.campaign_name||e.campaign?.name||'--')+''+esc(e.contact_name||e.contact?.name||e.email||'--')+''+esc(e.status||'pending')+''+(e.created_at?new Date(e.created_at).toLocaleDateString():'--')+'
' +''; } /* ===== RAFFLES ===== */ let raffles=[],rafPage=1; async function renderRaffles(){ try{raffles=await api('/raffles')}catch(e){raffles=[]} if(!Array.isArray(raffles))raffles=raffles?.data||[]; renderRafflesTable(); } function renderRafflesTable(){ const start=(rafPage-1)*20; const page=raffles.slice(start,start+20); const tp=Math.ceil(raffles.length/20)||1; const mc=$('main-content'); mc.innerHTML='

🎲 Raffles / Giveaways

' +'
' +page.map(r=>'').join('') +'
NameSlugEntriesStatusActions
'+esc(r.name||'--')+''+esc(r.slug||'--')+''+(r.entry_count||0)+''+esc(r.status||'draft')+'
' +''; } async function drawRaffle(slug){ try{const r=await api('/raffles/'+slug+'/draw',{method:'POST'});showToast('Winner drawn: '+(r.winner||'see results'));renderRaffles();} catch(e){showToast(e.message,'error')} } /* ===== CONTACTS ===== */ let contacts=[],conPage=1,conSearch=''; async function renderContacts(){ try{contacts=await api('/contacts')}catch(e){contacts=[]} if(!Array.isArray(contacts))contacts=contacts?.data||[]; renderContactsTable(); } function renderContactsTable(){ const start=(conPage-1)*20; const filtered=contacts.filter(c=>!conSearch||(c.name||c.email||'').toLowerCase().includes(conSearch.toLowerCase())); const page=filtered.slice(start,start+20); const tp=Math.ceil(filtered.length/20)||1; const mc=$('main-content'); mc.innerHTML='

πŸ‘₯ Contacts

' +'
' +'
' +page.map(c=>'').join('') +'
NameEmailPhoneSource
'+esc(c.name||'--')+''+esc(c.email||'--')+''+esc(c.phone||'--')+''+esc(c.source||'--')+'
' +''; } /* ===== API KEYS ===== */ let apikeys=[],akPage=1; async function renderApiKeys(){ try{apikeys=await api('/api-keys')}catch(e){apikeys=[]} if(!Array.isArray(apikeys))apikeys=apikeys?.data||[]; renderApiKeysTable(); } function renderApiKeysTable(){ const start=(akPage-1)*20; const page=apikeys.slice(start,start+20); const tp=Math.ceil(apikeys.length/20)||1; const mc=$('main-content'); mc.innerHTML='
πŸ’‘ Connect to FunnelSwift: Create an API key below, then paste it into FunnelSwift under Settings β†’ Connect Apps β†’ IncentiveSwift to share your campaigns in the FunnelSwift mobile app.
' +'

πŸ”‘ API Keys

' +'
' +page.map(k=>'').join('') +'
NameKey PreviewActiveActions
'+esc(k.name||'--')+''+esc((k.key||k.api_key||'').slice(0,12)+'...')+''+(!k.revoked?'Active':'Revoked')+'
' +''; } async function createApiKey(){ const name=prompt('API Key name:'); if(!name)return; try{const r=await api('/api-keys',{method:'POST',body:JSON.stringify({name})});var fullKey=r.full_key||r.key;showToast('Key created!');alert('Your new API key (copy this now β€” it won\'t be shown again):\n\n'+fullKey+'\n\nPaste this in FunnelSwift β†’ Settings β†’ Connect Apps β†’ IncentiveSwift');renderApiKeys();} catch(e){showToast(e.message,'error')} } async function deleteApiKey(id){ if(!confirm('Revoke this API key?'))return; try{await api('/api-keys/'+id,{method:'DELETE'});renderApiKeys();showToast('Revoked');} catch(e){showToast(e.message,'error')} } /* ===== PORTFOLIO COMPANIES ===== */ let portfolioCos=[],pcPage=1,pcSearch='',pcSelected=[]; async function renderPortfolios(){ try{const r=await api('/portfolio-companies');portfolioCos=r.companies||[]}catch(e){portfolioCos=[]} if(!Array.isArray(portfolioCos))portfolioCos=portfolioCos?.data||[]; renderPortfoliosTable(); } function renderPortfoliosTable(){ const start=(pcPage-1)*20; const filtered=portfolioCos.filter(c=>!pcSearch||(c.name||c.email||c.slug||'').toLowerCase().includes(pcSearch.toLowerCase())); const page=filtered.slice(start,start+20); const tp=Math.ceil(filtered.length/20)||1; const allSelected=pcSelected.length===page.length&&page.length>0; const mc=$('main-content'); mc.innerHTML='

🏒 Portfolio Companies

' +'
' +'' +'
' +'
' +'
' +'' +'' +page.map(c=>{ const sel=pcSelected.includes(c.id); return '' +'' +'' +''; }).join('') +'
NameSlugEmailActions
'+esc(c.name||'--')+''+esc(c.slug||'--')+''+esc(c.email||'--')+'' +'
' +''; } function togglePCSelect(id,checked){ if(checked&&!pcSelected.includes(id))pcSelected.push(id); else if(!checked)pcSelected=pcSelected.filter(x=>x!==id); showPCBulkBar(); } function showPCBulkBar(){ const bar=$('pc-bulk-bar'); if(!bar)return; if(pcSelected.length===0){bar.style.display='none';return} bar.style.display='flex'; bar.innerHTML=''+pcSelected.length+' selected' +''; } async function bulkDeleteCompanies(){ if(!confirm('Delete '+pcSelected.length+' companies?'))return; let ok=0,fail=0; for(const id of pcSelected){ try{await api('/portfolio-companies/'+id,{method:'DELETE'});ok++} catch(e){fail++} } showToast('Deleted '+ok+' companies'+(fail?' ('+fail+' failed)':'')); pcSelected=[]; renderPortfolios(); } async function deleteCompany(id){ if(!confirm('Delete this portfolio company?'))return; try{ await api('/portfolio-companies/'+id,{method:'DELETE'}); showToast('Deleted'); renderPortfolios(); }catch(e){showToast(e.message,'error')} } function showCompanyForm(existing){ const overlay=document.createElement('div'); overlay.className='modal-overlay'; overlay.onclick=e=>{if(e.target===overlay)overlay.remove()}; const data=existing||{name:'',email:'',slug:'',settings:{}}; const mk=existing?.settings?.marketing_boost_api_key||''; overlay.innerHTML=''; document.body.appendChild(overlay); } async function saveCompany(btn,isEdit){ const form=btn.closest('.modal').querySelector('#company-form'); const fd=new FormData(form); const mk=fd.get('marketing_boost_api_key')||''; btn.textContent='Saving...';btn.disabled=true; try{ const payload={ name: fd.get('name'), email: fd.get('email'), slug: fd.get('slug'), settings: {} }; if(mk) payload.settings.marketing_boost_api_key=mk; if(isEdit){ await api('/portfolio-companies/'+portEditId,{method:'PUT',body:JSON.stringify(payload)}); showToast('Company updated'); } else { await api('/portfolio-companies',{method:'POST',body:JSON.stringify(payload)}); showToast('Company created'); } btn.closest('.modal-overlay').remove(); renderPortfolios(); }catch(e){showToast(e.message,'error');btn.textContent=isEdit?'Update':'Create';btn.disabled=false} } let portEditId=null; function editCompany(id){ const c=portfolioCos.find(x=>x.id===id); if(c){portEditId=id;showCompanyForm(c)} } /* ===== INTEGRATIONS ===== */ let integrations=[],intPage=1; async function renderIntegrations(){ try{integrations=await api('/integration-targets')}catch(e){integrations=[]} if(!Array.isArray(integrations))integrations=integrations?.data||[]; renderIntegrationsTable(); } function renderIntegrationsTable(){ const start=(intPage-1)*20; const page=integrations.slice(start,start+20); const tp=Math.ceil(integrations.length/20)||1; const mc=$('main-content'); mc.innerHTML='

πŸ”— Integrations Hub

' +'
' +page.map(i=>{const domains=(i.allowed_domains||[]).length?i.allowed_domains.join(', '):'any';return '' +'' +'' +'' +'';}).join('') +'
NameProviderWebhook URLAllowed DomainsDaily LimitStatusActions
'+esc(i.name||'--')+''+esc(i.provider||'--')+''+esc((i.webhook_url||i.url||'').slice(0,35)+'...')+''+domains+''+(i.daily_limit||1000)+'/day'+(i.is_active!==false?'Active':'Inactive')+' ' +'
' +'
' +'πŸ”’ Security: Webhook URLs validated against allowed domains. Each integration limited to 1000 calls/day. Blocked calls are logged.' +'
' +''; } function showIntegrationForm(existing){ const overlay=document.createElement('div'); overlay.className='modal-overlay'; overlay.onclick=e=>{if(e.target===overlay)overlay.remove()}; const isEdit=!!existing; const data=existing||{name:'',provider:'',webhook_url:'',api_key:'',events:['on_win'],allowed_domains:[],daily_limit:1000}; const providers=['mailgun','sendgrid','sendiio','letterman','nexweave','sam_gov','marketing_boost','webhook']; overlay.innerHTML=''; document.body.appendChild(overlay); } function toggleMbOfferField(provider){ const el=document.getElementById('mb-offer-field'); if(el)el.style.display=provider==='marketing_boost'?'':'none'; } async function saveIntegration(btn,isEdit){ const form=btn.closest('.modal').querySelector('#integration-form'); const fd=new FormData(form); const offerId = (fd.get('mb_offer_id')||'').trim(); const data={ name: fd.get('name'), provider: fd.get('provider'), webhook_url: fd.get('webhook_url'), api_key: fd.get('api_key'), events: Array.from(form.querySelectorAll('input[name=events]:checked')).map(e=>e.value), allowed_domains: (fd.get('allowed_domains')||'').split(',').map(s=>s.trim()).filter(Boolean), daily_limit: parseInt(fd.get('daily_limit'))||1000, provider_metadata: offerId ? { offer_id: offerId } : null }; btn.textContent='Saving...';btn.disabled=true; try{ await api('/integration-targets',{method:'POST',body:JSON.stringify(data)}); showToast('Integration '+(isEdit?'updated':'created')); btn.closest('.modal-overlay').remove(); renderIntegrations(); }catch(e){showToast(e.message,'error');btn.textContent='Save';btn.disabled=false} } function editIntegration(id){ const i=integrations.find(x=>x.id===id); if(i)showIntegrationForm(i); } async function deleteIntegration(id){ if(!confirm('Delete this integration?'))return; try{await api('/integration-targets/'+id,{method:'DELETE'});showToast('Deleted');renderIntegrations();} catch(e){showToast(e.message,'error')} } /* ===== PROVIDER KEYS ===== */ let providerKeys=[],pkPage=1; async function renderProviderKeys(){ try{const r=await api('/provider-keys');providerKeys=r.items||r.data||[]}catch(e){providerKeys=[]} if(!Array.isArray(providerKeys))providerKeys=[]; renderProviderKeysTable(); } function renderProviderKeysTable(){ const start=(pkPage-1)*20; const page=providerKeys.slice(start,start+20); const tp=Math.ceil(providerKeys.length/20)||1; const mc=$('main-content'); mc.innerHTML='
πŸ’‘ Provider Keys are third-party API keys that IncentiveSwift uses on your behalf. Add keys for Marketing Boost, OpenAI, and other providers so your campaigns can use them automatically.
' +'

πŸ” Provider Keys

' +'
' +page.map(p=>'' +'' +'' +'').join('') +'
ProviderAPI KeyBase URLStatusScopeActions
'+esc(p.provider_name||p.provider||'--')+''+esc(p.api_key_masked||'***')+''+esc(p.base_url||'--')+''+(p.is_active!==false?'Active':'Inactive')+''+esc(p.scope||'account')+' ' +'
' +''; } function showProviderKeyForm(existing){ const overlay=document.createElement('div'); overlay.className='modal-overlay'; overlay.onclick=e=>{if(e.target===overlay)overlay.remove()}; const data=existing||{provider:'',api_key:'',base_url:'',is_active:true,scope:'account'}; overlay.innerHTML=''; document.body.appendChild(overlay); } function fillPkBaseUrl(){ const sel=document.getElementById('pk-provider-select'); const url=document.getElementById('pk-base-url'); if(!sel||!url)return; const defaults={ marketing_boost: 'https://api.marketingboost.com/v1', openai: 'https://api.openai.com/v1', anthropic: 'https://api.anthropic.com/v1', stripe: 'https://api.stripe.com/v1', sendgrid: 'https://api.sendgrid.com/v3', mailgun: 'https://api.mailgun.net/v3', deepseek: 'https://api.deepseek.com/v1', coreswift: 'https://api.coreswiftcrm.com/v1', }; const provider=sel.value; if(!url.value&&defaults[provider])url.value=defaults[provider]; } async function saveProviderKey(btn,isEdit){ const form=btn.closest('.modal').querySelector('#provider-key-form'); const fd=new FormData(form); const provider=fd.get('provider'); if(!provider){showToast('Please select a provider','error');return;} const apiKey=fd.get('api_key'); if(!apiKey&&!isEdit){showToast('API key is required','error');return;} const data={ provider: provider, api_key: apiKey||undefined, base_url: fd.get('base_url')||null, is_active: !!fd.get('is_active'), scope: fd.get('scope')||'account' }; btn.textContent='Saving...';btn.disabled=true; try{ await api('/provider-keys',{method:'POST',body:JSON.stringify(data)}); showToast('Provider key '+(isEdit?'updated':'saved')); btn.closest('.modal-overlay').remove(); renderProviderKeys(); }catch(e){showToast(e.message,'error');btn.textContent=isEdit?'Update':'Add Key';btn.disabled=false} } function editProviderKey(id){ const p=providerKeys.find(x=>x.id===id); if(p)showProviderKeyForm(p); } async function deleteProviderKey(pname){ if(!confirm('Delete this provider key?'))return; try{await api('/provider-keys/'+pname,{method:'DELETE'});showToast('Deleted');renderProviderKeys();} catch(e){showToast(e.message,'error')} } function showIntegrationsCampaignPicker(integrationId,integrationName){ const overlay=document.createElement('div'); overlay.className='modal-overlay'; overlay.onclick=e=>{if(e.target===overlay)overlay.remove()}; overlay.innerHTML=''; document.body.appendChild(overlay); filterIntCampaigns(); } function filterIntCampaigns(){ const q=(document.getElementById('int-campaign-search')?.value||'').toLowerCase(); const list=document.getElementById('int-campaign-list'); if(!list)return; const filtered=campaigns.filter(c=>c.name?.toLowerCase().includes(q)||c.slug?.toLowerCase().includes(q)); list.innerHTML=filtered.map(c=>{ const isLinked=c.config?.linked_integrations?.some(l=>l.integration_id===integrationTargetId); return '
' +''+esc(c.name||'')+'' +''+(isLinked?'βœ… Linked':'πŸ”— Click to link')+'' +'
'; }).join(''); if(filtered.length===0)list.innerHTML='

No campaigns found

'; } let integrationTargetId=null; async function linkIntToCampaign(campaignId,integrationId){ integrationTargetId=integrationId; try{ await api('/api/v1/campaigns/'+campaignId+'/integrations',{ method:'POST', body:JSON.stringify({integration_id:integrationId,trigger_events:['on_win','on_entry'],enabled:true}) }); showToast('Integration linked βœ“'); filterIntCampaigns(); // refresh }catch(e){showToast('Error: '+e.message,'error')} } /* ===== PLANS ===== */ let plans=[],plPage=1,plSearch=''; async function renderPlans(){ try{const r=await api('/admin/plans');plans=r.plans||[]}catch(e){plans=[]} if(!Array.isArray(plans))plans=plans?.data||plans?.plans||[]; renderPlansTable(); } function renderPlansTable(){ const start=(plPage-1)*20; const filtered=plans.filter(p=>!plSearch||(p.name||'').toLowerCase().includes(plSearch.toLowerCase())); const page=filtered.slice(start,start+20); const tp=Math.ceil(filtered.length/20)||1; const mc=$('main-content'); mc.innerHTML='

πŸ“‹ Plans

' +'
' +'
' +page.map(p=>'').join('') +'
NamePriceFeaturesStatusActions
'+esc(p.name||'--')+''+(p.price_monthly?'$'+Number(p.price_monthly).toFixed(2):'--')+''+(Array.isArray(p.features)?p.features.length+' features':'--')+''+(p.is_active!==false?'Active':'Inactive')+'
' +''; } async function deletePlan(id){ if(!confirm('Are you sure you want to delete this plan? This cannot be undone.'))return; try{ await api('/admin/plans/'+id,{method:'DELETE'}); showToast('Plan deleted'); renderPlans(); }catch(e){showToast(e.message,'error')} } function showPlanForm(p){ const overlay=document.createElement('div'); overlay.className='modal-overlay'; overlay.onclick=e=>{if(e.target===overlay)overlay.remove()}; const edit=!!p.id; const f=p.features||{}; overlay.innerHTML=''; document.body.appendChild(overlay); } async function savePlanForm(btn,editId){ const form=btn.closest('.modal').querySelector('#plan-form'); const fd=new FormData(form); btn.textContent='Saving...';btn.disabled=true; try{ const features={ credits_monthly: parseInt(fd.get('credits_monthly'))||0, credits_overdraft: parseInt(fd.get('credits_overdraft'))||0, cost_spin: parseInt(fd.get('cost_spin'))||2, cost_chat: parseInt(fd.get('cost_chat'))||5, cost_sms: parseInt(fd.get('cost_sms'))||10, cost_quiz: parseInt(fd.get('cost_quiz'))||3, cost_raffle: parseInt(fd.get('cost_raffle'))||2, cost_email: parseInt(fd.get('cost_email'))||1, cost_checkin: parseInt(fd.get('cost_checkin'))||1, cost_photo: parseInt(fd.get('cost_photo'))||3, }; const payload={ name: fd.get('name'), slug: fd.get('slug')||undefined, description: fd.get('description')||undefined, price_monthly: parseFloat(fd.get('price_monthly'))||0, price_yearly: parseFloat(fd.get('price_yearly'))||0, features: features, sort_order: parseInt(fd.get('sort_order'))||0, }; if(editId){ await api('/admin/plans/'+editId,{method:'PUT',body:JSON.stringify(payload)}); showToast('Plan updated'); } else { await api('/admin/plans',{method:'POST',body:JSON.stringify(payload)}); showToast('Plan created'); } btn.closest('.modal-overlay').remove(); renderPlans(); }catch(e){showToast(e.message,'error');btn.textContent=editId?'Update':'Create';btn.disabled=false} } function editPlan(id){ const p=plans.find(x=>x.id===id); if(p)showPlanForm(p); } /* ===== QUIZ QUESTIONS MANAGER ===== */ let questionRows=[]; async function loadCampaignQuestions(){ if(!editingCampaignId)return; try{ const slug=campaigns.find(c=>c.id===editingCampaignId)?.slug; if(!slug)return; const res=await api('/campaigns/'+slug+'/questions'); questionRows=res.questions||[]; renderQuestionEditor(); }catch(e){ questionRows=[]; renderQuestionEditor(); } } function renderQuestionEditor(){ const container=$('questions-editor'); if(!container)return; if(questionRows.length===0){ container.innerHTML='

No questions yet. Add your first question below.

'; return; } let html='' +''; questionRows.forEach((q,i)=>{ html+='' +'' +'' +'' +'' +'' +'' +'' +''; }); html+='
KeyQuestion TextTypeOptionsCorrect AnswerCRM Field
'; container.innerHTML=html; } function addQuestionRow(){ questionRows.push({ id:null,question_key:'q'+(questionRows.length+1),question_text:'',question_type:'single', options:null,correct_answer:'',score_weight:1,crm_field:'',sort_order:questionRows.length }); renderQuestionEditor(); } async function deleteQuestion(idx){ const q=questionRows[idx]; if(q.id){ const slug=campaigns.find(c=>c.id===editingCampaignId)?.slug; if(slug)await api('/campaigns/'+slug+'/questions/'+q.id,{method:'DELETE'}); } questionRows.splice(idx,1); renderQuestionEditor(); } async function saveQuestions(){ const slug=campaigns.find(c=>c.id===editingCampaignId)?.slug; if(!slug)return; const editor=$('questions-editor'); if(!editor)return; // Collect from table rows const rows=editor.querySelectorAll('tr:not(:first-child)'); const updates=[]; rows.forEach((tr,i)=>{ const fields={ id:questionRows[i]?.id||null, question_key:tr.querySelector('.q-key')?.value||'q'+(i+1), question_text:tr.querySelector('.q-text')?.value||'', question_type:tr.querySelector('.q-type')?.value||'single', options:tr.querySelector('.q-options')?.value.split(',').map(s=>s.trim()).filter(s=>s)||null, correct_answer:tr.querySelector('.q-correct')?.value||'', crm_field:tr.querySelector('.q-crm')?.value||'', sort_order:i, }; updates.push(fields); }); // Save each question for(const f of updates){ const body={ question_key:f.question_key, question_text:f.question_text, question_type:f.question_type, sort_order:f.sort_order, correct_answer:f.correct_answer||null, score_weight:1, options:f.options?{values:f.options}:null, crm_field:f.crm_field||null, crm_field_type:f.crm_field?'custom_field':null, }; try{ if(f.id){ await api('/campaigns/'+slug+'/questions/'+f.id,{method:'PUT',body:JSON.stringify(body)}); }else if(f.question_text){ await api('/campaigns/'+slug+'/questions',{method:'POST',body:JSON.stringify(body)}); } }catch(e){console.error('Question save failed:',e);} } } // Hook into showCampaignEditor to load questions const origShowEditor=showCampaignEditor; showCampaignEditor=function(campaign){ origShowEditor(campaign); setTimeout(()=>loadCampaignQuestions(),100); }; // Hook into saveCampaignEditor to save questions const origSaveEditor=saveCampaignEditor; saveCampaignEditor=async function(btn){ await saveQuestions(); await origSaveEditor(btn); }; /* ===== IQS FUNNELS ===== */ let iqsFunnels=[],iqsPage=1,iqsSearch='',editingIqsFunnelId=null,editingIqsQns=[],editingIqsRules=[]; async function renderIqs(){ const mc=$('main-content'); mc.innerHTML='
Loading IQS Funnels...
'; try{ const r=await api('/iqs/funnels'); iqsFunnels=r.data||[]; }catch(e){iqsFunnels=[]} if(!Array.isArray(iqsFunnels))iqsFunnels=[]; renderIqsTable(); } function renderIqsTable(){ const start=(iqsPage-1)*20; const filtered=iqsFunnels.filter(f=>!iqsSearch||(f.name||'').toLowerCase().includes(iqsSearch.toLowerCase())||(f.slug||'').toLowerCase().includes(iqsSearch.toLowerCase())); const page=filtered.slice(start,start+20); const tp=Math.ceil(filtered.length/20)||1; const mc=$('main-content'); mc.innerHTML='

🧠 Intelligent Qualifying Surveys

' +'
' +'' +''+filtered.length+' funnels' +'
' +'
' +page.map(f=>{ const status=f.status||'draft'; const badgeCls=status==='active'?'active':status==='archived'?'inactive':'draft'; return '' +'' +'' +'' +'' +'' +'' +'' +''; }).join('') +'
NameTypeSlugStatusResponsesCreatedActions
'+esc(f.name||'--')+''+esc(f.funnel_type||'survey')+''+esc(f.slug||'--')+''+status+''+f.response_count+''+new Date(f.created_at).toLocaleDateString()+'
' +'' +'' +'' +'' +'' +'
' +'' +'
' +'πŸ’‘ IQS Funnels are intelligent qualifying surveys. Each funnel has a configurable score threshold. ' +'Responses above the threshold are automatically tagged as qualified β€” you can set answer scoring and answerβ†’tag mappings in each question\'s options.' +'
'; } function showIqsForm(existingFunnel){ const overlay=document.createElement('div'); overlay.className='modal-overlay'; overlay.onclick=e=>{if(e.target===overlay)overlay.remove()}; const isEdit=!!existingFunnel; const f=existingFunnel||{}; overlay.innerHTML=''; document.body.appendChild(overlay); } async function saveIqsFunnel(btn,id){ const form=btn.closest('.modal').querySelector('form'); const fd=new FormData(form); const config={ passing_score: parseInt(fd.get('passing_score'))||70, max_attempts: parseInt(fd.get('max_attempts'))||1, collect_email: !!fd.get('collect_email'), collect_name: !!fd.get('collect_name'), collect_phone: !!fd.get('collect_phone'), show_progress_bar: !!fd.get('show_progress'), allow_skip: !!fd.get('allow_skip'), display_mode: fd.get('display_mode')||'step_by_step', redirect_url: fd.get('redirect_url')||null, post_submit: { qualified_title: fd.get('qualified_title')||null, qualified_subtitle: fd.get('qualified_subtitle')||null, qualified_redirect_url: fd.get('qualified_redirect_url')||null, disqualified_title: fd.get('disqualified_title')||null, disqualified_subtitle: fd.get('disqualified_subtitle')||null, disqualified_redirect_url: fd.get('disqualified_redirect_url')||null } }; const theme={ preset: fd.get('theme_preset')||'dark_modern', accent_color: fd.get('accent_color')||'#8b5cf6', button_style: fd.get('button_style')||'rounded' }; const payload={ name: fd.get('name'), slug: fd.get('slug')||undefined, description: fd.get('description')||null, funnel_type: fd.get('funnel_type')||'survey', status: fd.get('status')||'draft', source_tag: fd.get('source_tag')||null, config: config, theme: theme }; btn.textContent='Saving...';btn.disabled=true; try{ if(id){ await api('/iqs/funnels/'+id,{method:'PUT',body:JSON.stringify(payload)}); showToast('Funnel updated'); } else { const res=await api('/iqs/funnels',{method:'POST',body:JSON.stringify(payload)}); showToast('Funnel created! Slug: '+esc(res.slug||'')); } btn.closest('.modal-overlay').remove(); renderIqs(); }catch(e){showToast(e.message,'error');btn.textContent=id?'Update':'Create';btn.disabled=false} } async function deleteIqsFunnel(id){ if(!confirm('Delete this IQS funnel and all its data (questions, rules, submissions)?'))return; try{await api('/iqs/funnels/'+id,{method:'DELETE'});showToast('Deleted');renderIqs()} catch(e){showToast(e.message,'error')} } function showIqsPlayPreview(slug){ window.open('https://app.incentiveswift.com/iqs/play/'+slug,'_blank'); } // ===== IQS FUNNEL EDITOR (Questions + Settings) ===== let iqsQns=[],iqsRules=[]; async function showIqsEditor(funnelId){ editingIqsFunnelId=funnelId; const mc=$('main-content'); mc.innerHTML='
Loading funnel editor...
'; try{ const [fRes,qRes]=await Promise.all([ api('/iqs/funnels/'+funnelId), api('/iqs/funnels/'+funnelId+'/questions') ]); const funnel=fRes.data; iqsQns=qRes.data||[]; if(!Array.isArray(iqsQns))iqsQns=[]; renderIqsEditor(funnel); }catch(e){ showToast('Failed to load funnel: '+e.message,'error'); renderIqs(); } } function renderIqsEditor(funnel){ const mc=$('main-content'); mc.innerHTML='

🧠 '+esc(funnel.name)+'

' +'
' +'' +'' +'' +'
' +'
' +'
' +'
' +'

πŸ“ Questions  ('+iqsQns.length+' total)

' +'' +'
' +'
' +'
' +'
'; renderIqsQuestionList(); } function renderIqsQuestionList(){ const container=$('iqs-question-list'); if(!container)return; if(!iqsQns||iqsQns.length===0){ container.innerHTML='

No questions yet. Click "+ Add Question" to build your survey.

'; return; } let html=''; iqsQns.forEach((q,i)=>{ const typeLabels={single_choice:'β˜‘οΈ Single',multiple_choice:'βœ… Multiple',text:'πŸ“ Text',numeric:'πŸ”’ Numeric',email:'πŸ“§ Email',phone:'πŸ“ž Phone',rating:'⭐ Rating',field_consent:'πŸ“‹ Consent'}; const typeLabel=typeLabels[q.question_type]||q.question_type; const opts=q.options; const optionPreview=Array.isArray(opts)&&opts.length>0 ? '
'+opts.map(o=>{ const label=o.label||o.value||''; const score=o.score?' +'+o.score+'pts':''; const tag=o.tag?' 🏷️'+o.tag:''; return ''+esc(label)+''+score+tag+''; }).join('')+'
' : ''; html+='
' +'
' +'
' +'
' +'β Ώ' +''+esc(q.question_text||'Untitled')+'' +''+esc(q.question_key)+'' +'
' +'
' +''+typeLabel+'' +'πŸ”’ Sort: '+(q.sort_order||i)+'' +''+(q.required?'πŸ”΄ Required':'βšͺ Optional')+'' +'
' +optionPreview +'
' +'
' +'' +'' +'
' +'
' +'
'; }); // Reorder buttons html+='
' +'' +'' +'
'; container.innerHTML=html; } function addIqsQuestion(){ const newQ={ question_key:'q'+(iqsQns.length+1), question_text:'', question_type:'single_choice', required:true, options:[{label:'Option 1',value:'opt1',score:10,tag:''},{label:'Option 2',value:'opt2',score:5,tag:''}], config:{}, sort_order:iqsQns.length }; iqsQns.push(newQ); editIqsQuestion(iqsQns.length-1); } function editIqsQuestion(idx){ const q=iqsQns[idx]; if(!q)return; const overlay=document.createElement('div'); overlay.className='modal-overlay'; overlay.onclick=e=>{if(e.target===overlay)overlay.remove()}; overlay.innerHTML=''; document.body.appendChild(overlay); // Populate options renderIqsOptionRows(q); toggleIqsQuestionTypeOptions(q.question_type); } function toggleIqsQuestionTypeOptions(val){ const section=$('iqs-options-section'); if(!section)return; const hasOptions=['single_choice','multiple_choice'].includes(val); section.style.display=hasOptions?'block':'none'; } function renderIqsOptionRows(q){ const container=$('iqs-option-rows'); if(!container)return; const opts=q&&Array.isArray(q.options)?q.options:[{label:'Option 1',value:'opt1',score:10,tag:''},{label:'Option 2',value:'opt2',score:5,tag:''}]; let html=''; opts.forEach((o,i)=>{ html+='
' +'' +'' +'' +'' +'' +'
'; }); container.innerHTML=html; } function addIqsOptionRow(){ const container=$('iqs-option-rows'); if(!container)return; const div=document.createElement('div'); div.className='iqs-option-row'; div.style='display:grid;grid-template-columns:1fr 120px 80px 120px 30px;gap:6px;align-items:center;margin-bottom:4px'; div.innerHTML='' +'' +'' +'' +''; container.appendChild(div); } function removeIqsOptionRow(btn){btn.closest('.iqs-option-row').remove()} async function saveIqsQuestion(btn,idx){ const q=iqsQns[idx]; if(!q)return; const form=btn.closest('.modal').querySelector('form'); const fd=new FormData(form); // Collect options from rows const container=$('iqs-option-rows'); let options=null; if(container){ const rows=container.querySelectorAll('.iqs-option-row'); options=Array.from(rows).map(row=>({ label: row.querySelector('.iqs-opt-label').value, value: row.querySelector('.iqs-opt-value').value, score: parseInt(row.querySelector('.iqs-opt-score').value)||0, tag: row.querySelector('.iqs-opt-tag').value||'' })).filter(o=>o.label||o.value); } const question_type=fd.get('question_type'); const updated={ question_key: fd.get('question_key')||'q'+(idx+1), question_text: fd.get('question_text'), question_type: question_type, required: !!fd.get('required'), options: ['single_choice','multiple_choice'].includes(question_type)?options:null, config: {}, sort_order: q.sort_order||idx }; btn.textContent='Saving...';btn.disabled=true; try{ if(q.id){ await api('/iqs/funnels/'+editingIqsFunnelId+'/questions/'+q.id,{method:'PUT',body:JSON.stringify(updated)}); } else { const res=await api('/iqs/funnels/'+editingIqsFunnelId+'/questions',{method:'POST',body:JSON.stringify(updated)}); q.id=res.data?.id||null; } Object.assign(q,updated); btn.closest('.modal-overlay').remove(); renderIqsQuestionList(); showToast('Question saved'); }catch(e){showToast(e.message,'error');btn.textContent='Save';btn.disabled=false} } async function deleteIqsQuestion(idx,qid){ if(!confirm('Delete this question?'))return; if(qid){ try{await api('/iqs/funnels/'+editingIqsFunnelId+'/questions/'+qid,{method:'DELETE'})} catch(e){showToast(e.message,'error');return} } iqsQns.splice(idx,1); renderIqsQuestionList(); } async function reorderIqsQuestions(){ const ids=iqsQns.map(q=>q.id).filter(Boolean); if(ids.length<2)return; try{ await api('/iqs/funnels/'+editingIqsFunnelId+'/questions/reorder',{method:'PUT',body:JSON.stringify({question_ids:ids})}); showToast('Order saved'); }catch(e){showToast(e.message,'error')} } function showIqsQuestionBank(){ const mc=$('main-content'); mc.innerHTML='

πŸ“‹ Question Bank

' +'
' +'
' +'

Common qualifying questions you can add to this funnel with one click.

' +'
' +'
'; const bankQuestions=[ {text:'What is your budget range?',type:'single_choice',key:'budget',opts:[{label:'Under $1K',value:'under_1k',score:5,tag:'budget_low'},{label:'$1K-$5K',value:'1k_5k',score:10,tag:'budget_mid'},{label:'$5K-$10K',value:'5k_10k',score:15,tag:'budget_high'},{label:'$10K+',value:'10k_plus',score:20,tag:'budget_premium'}]}, {text:'What is your decision timeline?',type:'single_choice',key:'timeline',opts:[{label:'ASAP (within a week)',value:'asap',score:20,tag:'timeline_urgent'},{label:'1-3 months',value:'1_3_months',score:15,tag:'timeline_soon'},{label:'3-6 months',value:'3_6_months',score:10,tag:'timeline_medium'},{label:'Just researching',value:'researching',score:5,tag:'timeline_long'}]}, {text:'Who is the decision maker?',type:'single_choice',key:'authority',opts:[{label:'I am the decision maker',value:'self',score:20,tag:'authority_self'},{label:'I need approval',value:'need_approval',score:10,tag:'authority_needs_ok'},{label:'Just gathering info',value:'info_only',score:5,tag:'authority_info'}]}, {text:'What is your biggest challenge right now?',type:'single_choice',key:'pain',opts:[{label:'Growing revenue',value:'revenue',score:10,tag:'pain_revenue'},{label:'Saving time',value:'time',score:10,tag:'pain_time'},{label:'Customer retention',value:'retention',score:10,tag:'pain_retention'},{label:'Team productivity',value:'productivity',score:10,tag:'pain_productivity'}]}, {text:'How did you hear about us?',type:'single_choice',key:'source',opts:[{label:'Google Search',value:'google',score:0,tag:'src_google'},{label:'Social Media',value:'social',score:0,tag:'src_social'},{label:'Referral',value:'referral',score:5,tag:'src_referral'},{label:'Email',value:'email',score:0,tag:'src_email'},{label:'Other',value:'other',score:0,tag:'src_other'}]}, {text:'Company size (employees)',type:'single_choice',key:'company_size',opts:[{label:'Just me (1)',value:'1',score:5,tag:'size_1'},{label:'2-10',value:'2_10',score:10,tag:'size_small'},{label:'11-50',value:'11_50',score:15,tag:'size_medium'},{label:'51-200',value:'51_200',score:20,tag:'size_large'},{label:'200+',value:'200_plus',score:25,tag:'size_enterprise'}]}, {text:'Your email address',type:'email',key:'email',opts:null}, {text:'Your phone number',type:'phone',key:'phone',opts:null}, ]; const grid=$('iqs-bank-grid'); if(grid){ grid.innerHTML=bankQuestions.map((bq,i)=>{ return '
' +'
'+esc(bq.text)+'
' +'
'+bq.type+(bq.opts?' Β· '+bq.opts.length+' options':'')+'
' +'
'; }).join(''); // Store bank questions globally for access window.__iqsBankQns=bankQuestions; } } function addIqsBankQuestion(idx){ const bank=window.__iqsBankQns||[]; const bq=bank[idx]; if(!bq)return; iqsQns.push({ question_key: bq.key||'q'+(iqsQns.length+1), question_text: bq.text, question_type: bq.type, required: true, options: bq.opts||null, config: {}, sort_order: iqsQns.length }); showToast('Added: '+bq.text); renderIqsQuestionList(); } // ===== IQS RULES ===== async function showIqsRules(funnelId){ editingIqsFunnelId=funnelId; const mc=$('main-content'); mc.innerHTML='
Loading rules...
'; try{ const [fRes,rRes]=await Promise.all([ api('/iqs/funnels/'+funnelId), api('/iqs/funnels/'+funnelId+'/rules') ]); const funnel=fRes.data; iqsRules=rRes.data||[]; if(!Array.isArray(iqsRules))iqsRules=[]; renderIqsRules(funnel); }catch(e){ showToast('Failed to load rules: '+e.message,'error'); renderIqs(); } } function renderIqsRules(funnel){ const mc=$('main-content'); mc.innerHTML='

βš™οΈ Rules: '+esc(funnel.name)+'

' +'
' +'' +'' +'
' +'
' +'πŸ’‘ Rules define what happens based on submission answers. Each rule has conditions (when certain answers match) and actions (set outcome, tag contact, redirect, etc.)' +'
' +'
'; const container=$('iqs-rules-list'); if(!container)return; if(!iqsRules||iqsRules.length===0){ container.innerHTML='

No rules yet. Rules let you define dynamic outcomes based on answers.

'; return; } container.innerHTML=iqsRules.map((r,i)=>{ const conds=Array.isArray(r.conditions)?r.conditions:[]; const acts=Array.isArray(r.actions)?r.actions:[]; return '
' +'
' +'
' +'
' +''+(r.is_active!==false?'Active':'Inactive')+'' +''+esc(r.rule_type||'always')+'' +'Priority: '+(r.priority||0)+'' +'
' +'
Conditions: ' +(conds.length?conds.map(c=>esc(JSON.stringify(c))).join(' AND '):'Always (no conditions)')+'
' +'
Actions: ' +(acts.length?acts.map(a=>esc(JSON.stringify(a))).join(', '):'No actions')+'
' +'
' +'
' +'' +'' +'
' +'
' +'
'; }).join(''); } function addIqsRule(){ iqsRules.push({ rule_type:'always', priority:iqsRules.length, conditions:[], actions:[], is_active:true }); editIqsRule(iqsRules.length-1); } function editIqsRule(idx){ const r=iqsRules[idx]; if(!r)return; const overlay=document.createElement('div'); overlay.className='modal-overlay'; overlay.onclick=e=>{if(e.target===overlay)overlay.remove()}; overlay.innerHTML=''; document.body.appendChild(overlay); } async function saveIqsRule(btn,idx){ const r=iqsRules[idx]; if(!r)return; const form=btn.closest('.modal').querySelector('form'); const fd=new FormData(form); let conditions,actions; try{conditions=JSON.parse(fd.get('conditions')||'[]')}catch(e){conditions=[]} try{actions=JSON.parse(fd.get('actions')||'[]')}catch(e){actions=[]} const payload={ rule_type: fd.get('rule_type')||'always', priority: parseInt(fd.get('priority'))||0, conditions: conditions, actions: actions, is_active: !!fd.get('is_active') }; btn.textContent='Saving...';btn.disabled=true; try{ if(r.id){ await api('/iqs/funnels/'+editingIqsFunnelId+'/rules/'+r.id,{method:'PUT',body:JSON.stringify(payload)}); } else { const res=await api('/iqs/funnels/'+editingIqsFunnelId+'/rules',{method:'POST',body:JSON.stringify(payload)}); r.id=res.data?.id||null; } Object.assign(r,payload); btn.closest('.modal-overlay').remove(); showIqsRules(editingIqsFunnelId); showToast('Rule saved'); }catch(e){showToast(e.message,'error');btn.textContent='Save';btn.disabled=false} } async function deleteIqsRule(idx,rid){ if(!confirm('Delete this rule?'))return; if(rid){ try{await api('/iqs/funnels/'+editingIqsFunnelId+'/rules/'+rid,{method:'DELETE'})} catch(e){showToast(e.message,'error');return} } iqsRules.splice(idx,1); showIqsRules(editingIqsFunnelId); } // ===== IQS SUBMISSIONS ===== async function showIqsSubmissions(funnelId){ editingIqsFunnelId=funnelId; const mc=$('main-content'); mc.innerHTML='
Loading submissions...
'; try{ const [fRes,sRes]=await Promise.all([ api('/iqs/funnels/'+funnelId), api('/iqs/funnels/'+funnelId+'/submissions') ]); const funnel=fRes.data; const subs=sRes.data||[]; renderIqsSubmissions(funnel,Array.isArray(subs)?subs:[]); }catch(e){ showToast('Failed: '+e.message,'error'); renderIqs(); } } function renderIqsSubmissions(funnel,subs){ const mc=$('main-content'); mc.innerHTML='

πŸ“‹ Responses: '+esc(funnel.name)+'

' +'
' +'' +'
' +'
' +'
'+subs.length+'
Total Responses
' +'
'+subs.filter(s=>s.outcome==='qualified').length+'
Qualified
' +'
'+subs.filter(s=>s.outcome==='disqualified').length+'
Disqualified
' +'
'+funnel.response_count+'
Total Count
' +'
' +'
' +subs.map(s=>{ const tags=Array.isArray(s.tags_applied)&&s.tags_applied.length?s.tags_applied.join(', '):'--'; const outcomeCls=s.outcome==='qualified'?'active':s.outcome==='disqualified'?'inactive':'draft'; return '' +'' +'' +'' +'' +''; }).join('') +'
DateScoreOutcomeTagsContact ID
'+new Date(s.created_at).toLocaleString()+''+s.total_score+''+(s.outcome||'--')+''+esc(tags)+''+(s.contact_id||'').slice(0,8)+'...
'; } /* ===== UTILITIES ===== */ function esc(s){if(!s)return'';const d=document.createElement('div');d.textContent=s;return d.innerHTML} function showToast(msg,type){ const el=document.createElement('div'); el.className=type==='error'?'error':'success'; el.textContent=msg; el.style.position='fixed';el.style.top='16px';el.style.right='16px';el.style.zIndex='200';el.style.maxWidth='400px'; document.body.appendChild(el); setTimeout(()=>el.remove(),3000); } render(); Β© 2026 IncentiveSwift A SwiftSoftware Company. All rights reserved.