CoreSwiftCRM

Sign In

Access your unified CRM workspace

Don't have an account? Sign Up

+Number(o.value).toLocaleString()+'':'') +(o.probability?'
๐ŸŽฏ '+o.probability+'%
':'') +(o.expected_close_date?'
๐Ÿ“… '+new Date(o.expected_close_date).toLocaleDateString()+'
':'') +'').join(''); }catch(e){} } let dragDealId=null; function onDragStart(e,dealId){dragDealId=dealId;e.dataTransfer.effectAllowed='move';e.target.classList.add('dragging')} function onDragOver(e){e.preventDefault();e.dataTransfer.dropEffect='move';e.currentTarget.classList.add('drag-over')} function onDragLeave(e){e.currentTarget.classList.remove('drag-over')} async function onDrop(e,stageId){ e.preventDefault();e.currentTarget.classList.remove('drag-over'); if(!dragDealId)return; const dealId=dragDealId;dragDealId=null; document.querySelectorAll('.kanban-card.dragging').forEach(el=>el.remove()); try{ await api('/pipelines/'+selectedPipelineId+'/stages/'+stageId+'/move/'+dealId,{method:'PATCH',body:'{}'}); showToast('Deal moved โœ“'); document.querySelectorAll('.kanban-col').forEach(col=>loadStageOpps(col.dataset.stageId)); }catch(e){showToast('Move failed: '+e.message,'error');document.querySelectorAll('.kanban-col').forEach(col=>loadStageOpps(col.dataset.stageId))} } function showPipelineForm(existing){ const overlay=document.createElement('div');overlay.className='modal-overlay'; overlay.onclick=e=>{if(e.target===overlay)overlay.remove()}; const d=existing||{}; overlay.innerHTML=''; document.body.appendChild(overlay); } async function savePipeline(btn,isEdit){ const form=btn.closest('.modal').querySelector('#pipeline-form'); const fd=new FormData(form); btn.textContent='Saving...';btn.disabled=true; try{ if(isEdit){await api('/pipelines/'+selectedPipelineId,{method:'PATCH',body:JSON.stringify({name:fd.get('name'),description:fd.get('description')})})} else{await api('/pipelines',{method:'POST',body:JSON.stringify({name:fd.get('name'),description:fd.get('description')})})} showToast(isEdit?'Pipeline updated โœ“':'Pipeline created โœ“'); btn.closest('.modal-overlay').remove();renderPipelines(); }catch(e){showToast(e.message,'error')} btn.textContent=isEdit?'Update':'Create';btn.disabled=false; } function showStageForm(existing){ const overlay=document.createElement('div');overlay.className='modal-overlay'; overlay.onclick=e=>{if(e.target===overlay)overlay.remove()}; const d=existing||{}; overlay.innerHTML=''; document.body.appendChild(overlay); } function editStage(id){ showStageForm(true,id) } async function saveStage(btn,isEdit,stageId){ const form=btn.closest('.modal').querySelector('#stage-form'); const fd=new FormData(form); btn.textContent='Saving...';btn.disabled=true; try{ const data={name:fd.get('name'),color:fd.get('color'),probability:parseInt(fd.get('probability'))||null}; if(isEdit){ data.is_won_stage=!!fd.get('is_won_stage');data.is_lost_stage=!!fd.get('is_lost_stage'); await api('/pipelines/'+selectedPipelineId+'/stages/'+stageId,{method:'PATCH',body:JSON.stringify(data)}); }else{await api('/pipelines/'+selectedPipelineId+'/stages',{method:'POST',body:JSON.stringify(data)})} showToast(isEdit?'Stage updated โœ“':'Stage created โœ“'); btn.closest('.modal-overlay').remove();renderPipelines(); }catch(e){showToast(e.message,'error')} btn.textContent=isEdit?'Update':'Create';btn.disabled=false; } async function deleteStage(stageId){ if(!confirm('Delete this stage?'))return; try{await api('/pipelines/'+selectedPipelineId+'/stages/'+stageId,{method:'DELETE'});showToast('Stage deleted');renderPipelines()} catch(e){showToast(e.message,'error')} } async function showDealForm(stageId,dealId){ let existing={}; if(dealId){try{const r=await api('/pipelines/'+selectedPipelineId+'/opportunities/'+dealId);existing=r}catch(e){}} const overlay=document.createElement('div');overlay.className='modal-overlay'; overlay.onclick=e=>{if(e.target===overlay)overlay.remove()}; const d=existing||{}; const stageInfo=pipelineCache[selectedPipelineId]?.stages?.find(s=>s.id===stageId); let contacts=[];try{contacts=await api('/contacts?limit=500')}catch(e){} if(!Array.isArray(contacts))contacts=contacts?.data||contacts?.items||[]; overlay.innerHTML=''; document.body.appendChild(overlay); } async function saveDeal(btn,stageId,dealId){ const form=btn.closest('.modal').querySelector('#deal-form'); const fd=new FormData(form); btn.textContent='Saving...';btn.disabled=true; try{ let contactId=fd.get('contact_id')||null; if(contactId==='_new'){ const nc=await api('/contacts',{method:'POST',body:JSON.stringify({ first_name:fd.get('new_first')||'',last_name:fd.get('new_last')||'', email:fd.get('new_email')||null,phone:fd.get('new_phone')||null })}); contactId=nc.id||(nc.data&&nc.data.id)||null; } const data={name:fd.get('name'),contact_id:contactId,value:parseFloat(fd.get('value'))||null,probability:parseInt(fd.get('probability'))||null,expected_close_date:fd.get('expected_close_date')||null,description:fd.get('description')||null}; if(dealId){await api('/pipelines/'+selectedPipelineId+'/opportunities/'+dealId,{method:'PATCH',body:JSON.stringify(data)})} else{data.stage_id=stageId;await api('/pipelines/'+selectedPipelineId+'/opportunities',{method:'POST',body:JSON.stringify(data)})} showToast(dealId?'Deal updated โœ“':'Deal created โœ“'); btn.closest('.modal-overlay').remove(); document.querySelectorAll('.kanban-col').forEach(col=>loadStageOpps(col.dataset.stageId)); }catch(e){showToast(e.message,'error')} btn.textContent=dealId?'Update':'Create';btn.disabled=false; } async function showDealDetail(dealId){ try{ const d=await api('/pipelines/'+selectedPipelineId+'/opportunities/'+dealId); const stageInfo=pipelineCache[selectedPipelineId]?.stages?.find(s=>s.id===d.stage_id); const mc=$('main-content'); mc.innerHTML='

โ†Deal

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

๐Ÿ“‹ Deal Info

' +'
Name'+esc(d.name||'')+'
' +'
Stage'+esc(stageInfo?.name||d.stage_name||'--')+'
' +'
Value'+(d.value?' mc.innerHTML='

๐Ÿ”„ Pipelines

Loading pipelines...
'; try{ const pl=await api('/pipelines'); if(!Array.isArray(pl)){ mc.innerHTML='

๐Ÿ”„ Pipelines

๐Ÿ”„

No pipelines configured yet

'; return; } let html='

๐Ÿ”„ Pipelines

' +pl.map(p=>'

'+esc(p.name||'Pipeline')+'

' +'

'+plural(p.stages?.length||0,'stage')+' ยท '+plural(p.deal_count||0,'deal')+'

' +'
').join('') +'
'; mc.innerHTML=html; }catch(e){mc.innerHTML='

๐Ÿ”„ Pipelines

โš ๏ธ

'+esc(e.message)+'

'} } async function showPipeline(id){ showToast('Pipeline detail coming in Phase 2','info'); } /* ===== TAGS ===== */ async function renderTags(){ const mc=$('main-content'); mc.innerHTML='

๐Ÿท Tags

Loading...
'; try{ const tg=await api('/tags'); if(!Array.isArray(tg))tg=tg?.data||tg?.tags||[]; mc.innerHTML='

๐Ÿท Tags

' +'
' +'' +tg.map(t=>'' +'' +'' +'' +'' +'').join('') +'
NameColorTypeContacts
'+esc(t.name||'')+''+esc(t.color||'Default')+''+(t.is_system?'System':'Custom')+''+(t.contact_count||0)+'
' +(tg.length===0?'
๐Ÿท

No tags yet

':'') +'
'; }catch(e){mc.innerHTML='

๐Ÿท Tags

โš ๏ธ

'+esc(e.message)+'

'} } function showTagForm(){ 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 saveTag(btn){ const form=btn.closest('.modal').querySelector('#tag-form'); const fd=new FormData(form); try{ await api('/tags',{method:'POST',body:JSON.stringify({name:fd.get('name'),color:fd.get('color')})}); showToast('Tag created โœ“'); btn.closest('.modal-overlay').remove(); renderTags(); }catch(e){showToast(e.message,'error')} } /* ===== LISTS ===== */ async function renderLists(){ const mc=$('main-content'); mc.innerHTML='

๐Ÿ“‹ Lists

Loading...
'; try{ const ls=await api('/lists'); if(!Array.isArray(ls))ls=ls?.data||ls?.items||[]; mc.innerHTML='

๐Ÿ“‹ Lists

' +'
' +'' +ls.map(l=>'' +'' +'' +'' +'').join('') +'
NameDescriptionMembers
'+esc(l.name||'')+''+esc(l.description||'--')+''+(l.member_count||l.contact_count||0)+'
' +(ls.length===0?'
๐Ÿ“‹

No lists yet

':'') +'
'; }catch(e){mc.innerHTML='

๐Ÿ“‹ Lists

โš ๏ธ

'+esc(e.message)+'

'} } function showListForm(){ 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 saveList(btn){ const form=btn.closest('.modal').querySelector('#list-form'); const fd=new FormData(form); try{ await api('/lists',{method:'POST',body:JSON.stringify({name:fd.get('name'),description:fd.get('description')})}); showToast('List created โœ“'); btn.closest('.modal-overlay').remove(); renderLists(); }catch(e){showToast(e.message,'error')} } /* ===== COMMUNICATIONS ===== */ async function renderComms(){ const mc=$('main-content'); mc.innerHTML='

โœ‰๏ธ Communications

' +'

๐Ÿ“ง Email

' +'

Email provider settings and message log.

' +'

Coming in Phase 4 โ€” SMTP provider config, template manager, send history.

' +'
' +'

๐Ÿ“ฑ SMS

' +'

SMS provider settings and message log.

' +'

Coming in Phase 4 โ€” Twilio/Telnyx config, template manager, send history.

' +'
'; } /* ===== SETTINGS ===== */ async function renderSettings(){ const mc=$('main-content'); try{ const acct=await api('/account'); const acc=Array.isArray(acct)?acct[0]||{}:acct; mc.innerHTML='

โš™๏ธ Settings

' +'

๐Ÿ‘ค Profile

' +'
' +'' +'' +'' +'' +'
' +'
' +'
'; }catch(e){mc.innerHTML='

โš™๏ธ Settings

โš ๏ธ

'+esc(e.message)+'

'} } async function saveSettings(btn){ const form=document.getElementById('settings-form'); if(!form)return; const fd=new FormData(form); const data={name:fd.get('name'),email:fd.get('email'),phone:fd.get('phone'),company_name:fd.get('company_name')}; btn.textContent='Saving...';btn.disabled=true; try{ await api('/account',{method:'PATCH',body:JSON.stringify(data)}); showToast('Settings saved โœ“'); }catch(e){showToast(e.message,'error')} btn.textContent='Save Changes';btn.disabled=false; } /* ===== INIT ===== */ async function loadUser(){ try{ const u=await api('/auth/me'); user=u; $('avatar-letter').textContent=(u.name||u.email||'?')[0].toUpperCase(); $('user-name').textContent=u.name||'User'; $('user-email').textContent=u.email||''; }catch(e){ // Fallback: try getting from account endpoint try{ const acct=await api('/account'); const u=Array.isArray(acct)?acct[0]:acct; if(u){ $('avatar-letter').textContent=(u.name||u.email||'?')[0].toUpperCase(); $('user-name').textContent=u.name||'User'; $('user-email').textContent=u.email||''; } }catch(e2){ // silently fail, user info is cosmetic } } } function render(){ if(token){ hide('login-page'); show('app'); loadUser(); showPage('dashboard'); }else{ show('login-page'); hide('app'); } } render(); +Number(d.value).toLocaleString():'--')+'
' +'
Probability'+(d.probability?d.probability+'%':'--')+'
' +'
Close Date'+(d.expected_close_date?new Date(d.expected_close_date).toLocaleDateString():'--')+'
' +'

๐Ÿ“ Description

' +'

'+(d.description||'No description')+'

' +'
' +'

๐Ÿ‘ค Contact

'+(d.contact_name||'No contact linked')+'

' +'

๐Ÿ“… Timeline

' +'
Created'+(d.created_at?new Date(d.created_at).toLocaleDateString():'--')+'
' +'
Updated'+(d.updated_at?new Date(d.updated_at).toLocaleDateString():'--')+'
' +'
'; // Load timeline try{ loadDealTimeline(d.id); }catch(e){} }catch(e){showToast(e.message,'error')} } async function loadDealTimeline(dealId){ const tl=document.getElementById('deal-timeline-'+dealId); if(!tl)return; try{ const history=await api('/audit?entity_type=opportunity&entity_id='+dealId); const items=Array.isArray(history)?history:(history?.data||history?.items||history?.logs||[]); if(items.length===0){ tl.innerHTML='
No activity yet
'; return; } tl.innerHTML=items.slice(0,15).map(m=>{ const label=m.action?.replace(/\./g,' ').replace(/_/g,' ').replace(/\b\w/g,c=>c.toUpperCase())||'Updated'; const time=m.created_at?new Date(m.created_at).toLocaleString():''; return '
' +''+(label.includes('Create')?'โœจ':label.includes('Move')||label.includes('move')?'โžก๏ธ':label.includes('Win')?'๐Ÿ†':label.includes('Lost')||label.includes('lost')?'โŒ':'๐Ÿ“')+'' +'
'+esc(label)+'
' +(m.changes?'
'+esc(JSON.stringify(m.changes).slice(0,80))+'
':'') +'
'+esc(time)+'
'; }).join(''); }catch(e){ tl.innerHTML='
Timeline unavailable
'; } } function switchPipeline(id){selectedPipelineId=id;renderPipelineKanban()} /* === PHASE 2 KANBAN END === */ async function renderPipelines_deprecated(){ const mc=$('main-content'); mc.innerHTML='

๐Ÿ”„ Pipelines

Loading pipelines...
'; try{ const pl=await api('/pipelines'); if(!Array.isArray(pl)){ mc.innerHTML='

๐Ÿ”„ Pipelines

๐Ÿ”„

No pipelines configured yet

'; return; } let html='

๐Ÿ”„ Pipelines

' +pl.map(p=>'

'+esc(p.name||'Pipeline')+'

' +'

'+plural(p.stages?.length||0,'stage')+' ยท '+plural(p.deal_count||0,'deal')+'

' +'
').join('') +'
'; mc.innerHTML=html; }catch(e){mc.innerHTML='

๐Ÿ”„ Pipelines

โš ๏ธ

'+esc(e.message)+'

'} } async function showPipeline(id){ showToast('Pipeline detail coming in Phase 2','info'); } /* ===== TAGS ===== */ async function renderTags(){ const mc=$('main-content'); mc.innerHTML='

๐Ÿท Tags

Loading...
'; try{ const tg=await api('/tags'); if(!Array.isArray(tg))tg=tg?.data||tg?.tags||[]; mc.innerHTML='

๐Ÿท Tags

' +'
' +'' +tg.map(t=>'' +'' +'' +'' +'' +'').join('') +'
NameColorTypeContacts
'+esc(t.name||'')+''+esc(t.color||'Default')+''+(t.is_system?'System':'Custom')+''+(t.contact_count||0)+'
' +(tg.length===0?'
๐Ÿท

No tags yet

':'') +'
'; }catch(e){mc.innerHTML='

๐Ÿท Tags

โš ๏ธ

'+esc(e.message)+'

'} } function showTagForm(){ 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 saveTag(btn){ const form=btn.closest('.modal').querySelector('#tag-form'); const fd=new FormData(form); try{ await api('/tags',{method:'POST',body:JSON.stringify({name:fd.get('name'),color:fd.get('color')})}); showToast('Tag created โœ“'); btn.closest('.modal-overlay').remove(); renderTags(); }catch(e){showToast(e.message,'error')} } /* ===== LISTS ===== */ async function renderLists(){ const mc=$('main-content'); mc.innerHTML='

๐Ÿ“‹ Lists

Loading...
'; try{ const ls=await api('/lists'); if(!Array.isArray(ls))ls=ls?.data||ls?.items||[]; mc.innerHTML='

๐Ÿ“‹ Lists

' +'
' +'' +ls.map(l=>'' +'' +'' +'' +'').join('') +'
NameDescriptionMembers
'+esc(l.name||'')+''+esc(l.description||'--')+''+(l.member_count||l.contact_count||0)+'
' +(ls.length===0?'
๐Ÿ“‹

No lists yet

':'') +'
'; }catch(e){mc.innerHTML='

๐Ÿ“‹ Lists

โš ๏ธ

'+esc(e.message)+'

'} } function showListForm(){ 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 saveList(btn){ const form=btn.closest('.modal').querySelector('#list-form'); const fd=new FormData(form); try{ await api('/lists',{method:'POST',body:JSON.stringify({name:fd.get('name'),description:fd.get('description')})}); showToast('List created โœ“'); btn.closest('.modal-overlay').remove(); renderLists(); }catch(e){showToast(e.message,'error')} } /* ===== COMMUNICATIONS ===== */ async function renderComms(){ const mc=$('main-content'); mc.innerHTML='

โœ‰๏ธ Communications

' +'

๐Ÿ“ง Email

' +'

Email provider settings and message log.

' +'

Coming in Phase 4 โ€” SMTP provider config, template manager, send history.

' +'
' +'

๐Ÿ“ฑ SMS

' +'

SMS provider settings and message log.

' +'

Coming in Phase 4 โ€” Twilio/Telnyx config, template manager, send history.

' +'
'; } /* ===== SETTINGS ===== */ async function renderSettings(){ const mc=$('main-content'); try{ const acct=await api('/account'); const acc=Array.isArray(acct)?acct[0]||{}:acct; mc.innerHTML='

โš™๏ธ Settings

' +'

๐Ÿ‘ค Profile

' +'
' +'' +'' +'' +'' +'
' +'
' +'
'; }catch(e){mc.innerHTML='

โš™๏ธ Settings

โš ๏ธ

'+esc(e.message)+'

'} } async function saveSettings(btn){ const form=document.getElementById('settings-form'); if(!form)return; const fd=new FormData(form); const data={name:fd.get('name'),email:fd.get('email'),phone:fd.get('phone'),company_name:fd.get('company_name')}; btn.textContent='Saving...';btn.disabled=true; try{ await api('/account',{method:'PATCH',body:JSON.stringify(data)}); showToast('Settings saved โœ“'); }catch(e){showToast(e.message,'error')} btn.textContent='Save Changes';btn.disabled=false; } /* ===== INIT ===== */ async function loadUser(){ try{ const u=await api('/auth/me'); user=u; $('avatar-letter').textContent=(u.name||u.email||'?')[0].toUpperCase(); $('user-name').textContent=u.name||'User'; $('user-email').textContent=u.email||''; }catch(e){ // Fallback: try getting from account endpoint try{ const acct=await api('/account'); const u=Array.isArray(acct)?acct[0]:acct; if(u){ $('avatar-letter').textContent=(u.name||u.email||'?')[0].toUpperCase(); $('user-name').textContent=u.name||'User'; $('user-email').textContent=u.email||''; } }catch(e2){ // silently fail, user info is cosmetic } } } function render(){ if(token){ hide('login-page'); show('app'); loadUser(); showPage('dashboard'); }else{ show('login-page'); hide('app'); } } render(); +Number(p.price).toLocaleString():'--')+'' +''+(p.max_contacts||'Unlimited')+'' +''+(p.features?Object.entries(p.features).slice(0,3).map(([k,v])=>k+': '+v).join(', ')+(Object.keys(p.features).length>3?' ...':''):'--')+'' +'').join('') +'':'') +'

๐Ÿ”Œ Connected Apps

' +'

Your account is shared across all SwiftSoftware apps. No additional setup needed.

' +'
' +'๐Ÿง  CoreSwift CRM' +'๐Ÿ“Š FunnelSwift' +'๐ŸŽฏ IncentiveSwift' +'โšก WorkflowSwift' +'๐Ÿ“ž MissedCall Respondr' +'๐Ÿ“‹ Multi-Directory' +'
'; }catch(e){mc.innerHTML='

โš™๏ธ Settings

'+settingsHeader()+'
โš ๏ธ

'+esc(e.message)+'

'} } async function saveSettings(btn){ const form=document.getElementById('settings-form'); if(!form)return; const fd=new FormData(form); const data={name:fd.get('name'),email:fd.get('email'),phone:fd.get('phone'),company_name:fd.get('company_name')}; btn.textContent='Saving...';btn.disabled=true; try{ await api('/account',{method:'PATCH',body:JSON.stringify(data)}); showToast('Settings saved โœ“'); }catch(e){showToast(e.message,'error')} btn.textContent='Save Changes';btn.disabled=false; } /* ===== INIT ===== */ async function loadUser(){ try{ const u=await api('/auth/me'); user=u; $('avatar-letter').textContent=(u.name||u.email||'?')[0].toUpperCase(); $('user-name').textContent=u.name||'User'; $('user-email').textContent=u.email||''; }catch(e){ // Fallback: try getting from account endpoint try{ const acct=await api('/account'); const u=Array.isArray(acct)?acct[0]:acct; if(u){ $('avatar-letter').textContent=(u.name||u.email||'?')[0].toUpperCase(); $('user-name').textContent=u.name||'User'; $('user-email').textContent=u.email||''; } }catch(e2){ // silently fail, user info is cosmetic } } } function render(){ if(token){ hide('login-page'); show('app'); loadUser(); showPage('dashboard'); }else{ show('login-page'); hide('app'); } } render(); +Number(o.value).toLocaleString()+'':'') +(o.probability?'
๐ŸŽฏ '+o.probability+'%
':'') +(o.expected_close_date?'
๐Ÿ“… '+new Date(o.expected_close_date).toLocaleDateString()+'
':'') +'').join(''); }catch(e){} } let dragDealId=null; function onDragStart(e,dealId){dragDealId=dealId;e.dataTransfer.effectAllowed='move';e.target.classList.add('dragging')} function onDragOver(e){e.preventDefault();e.dataTransfer.dropEffect='move';e.currentTarget.classList.add('drag-over')} function onDragLeave(e){e.currentTarget.classList.remove('drag-over')} async function onDrop(e,stageId){ e.preventDefault();e.currentTarget.classList.remove('drag-over'); if(!dragDealId)return; const dealId=dragDealId;dragDealId=null; document.querySelectorAll('.kanban-card.dragging').forEach(el=>el.remove()); try{ await api('/pipelines/'+selectedPipelineId+'/stages/'+stageId+'/move/'+dealId,{method:'PATCH',body:'{}'}); showToast('Deal moved โœ“'); document.querySelectorAll('.kanban-col').forEach(col=>loadStageOpps(col.dataset.stageId)); }catch(e){showToast('Move failed: '+e.message,'error');document.querySelectorAll('.kanban-col').forEach(col=>loadStageOpps(col.dataset.stageId))} } function showPipelineForm(existing){ const overlay=document.createElement('div');overlay.className='modal-overlay'; overlay.onclick=e=>{if(e.target===overlay)overlay.remove()}; const d=existing||{}; overlay.innerHTML=''; document.body.appendChild(overlay); } async function savePipeline(btn,isEdit){ const form=btn.closest('.modal').querySelector('#pipeline-form'); const fd=new FormData(form); btn.textContent='Saving...';btn.disabled=true; try{ if(isEdit){await api('/pipelines/'+selectedPipelineId,{method:'PATCH',body:JSON.stringify({name:fd.get('name'),description:fd.get('description')})})} else{await api('/pipelines',{method:'POST',body:JSON.stringify({name:fd.get('name'),description:fd.get('description')})})} showToast(isEdit?'Pipeline updated โœ“':'Pipeline created โœ“'); btn.closest('.modal-overlay').remove();renderPipelines(); }catch(e){showToast(e.message,'error')} btn.textContent=isEdit?'Update':'Create';btn.disabled=false; } function showStageForm(existing){ const overlay=document.createElement('div');overlay.className='modal-overlay'; overlay.onclick=e=>{if(e.target===overlay)overlay.remove()}; const d=existing||{}; overlay.innerHTML=''; document.body.appendChild(overlay); } function editStage(id){ showStageForm(true,id) } async function saveStage(btn,isEdit,stageId){ const form=btn.closest('.modal').querySelector('#stage-form'); const fd=new FormData(form); btn.textContent='Saving...';btn.disabled=true; try{ const data={name:fd.get('name'),color:fd.get('color'),probability:parseInt(fd.get('probability'))||null}; if(isEdit){ data.is_won_stage=!!fd.get('is_won_stage');data.is_lost_stage=!!fd.get('is_lost_stage'); await api('/pipelines/'+selectedPipelineId+'/stages/'+stageId,{method:'PATCH',body:JSON.stringify(data)}); }else{await api('/pipelines/'+selectedPipelineId+'/stages',{method:'POST',body:JSON.stringify(data)})} showToast(isEdit?'Stage updated โœ“':'Stage created โœ“'); btn.closest('.modal-overlay').remove();renderPipelines(); }catch(e){showToast(e.message,'error')} btn.textContent=isEdit?'Update':'Create';btn.disabled=false; } async function deleteStage(stageId){ if(!confirm('Delete this stage?'))return; try{await api('/pipelines/'+selectedPipelineId+'/stages/'+stageId,{method:'DELETE'});showToast('Stage deleted');renderPipelines()} catch(e){showToast(e.message,'error')} } async function showDealForm(stageId,dealId){ let existing={}; if(dealId){try{const r=await api('/pipelines/'+selectedPipelineId+'/opportunities/'+dealId);existing=r}catch(e){}} const overlay=document.createElement('div');overlay.className='modal-overlay'; overlay.onclick=e=>{if(e.target===overlay)overlay.remove()}; const d=existing||{}; const stageInfo=pipelineCache[selectedPipelineId]?.stages?.find(s=>s.id===stageId); let contacts=[];try{contacts=await api('/contacts?limit=500')}catch(e){} if(!Array.isArray(contacts))contacts=contacts?.data||contacts?.items||[]; overlay.innerHTML=''; document.body.appendChild(overlay); } async function saveDeal(btn,stageId,dealId){ const form=btn.closest('.modal').querySelector('#deal-form'); const fd=new FormData(form); btn.textContent='Saving...';btn.disabled=true; try{ const data={name:fd.get('name'),contact_id:fd.get('contact_id')||null,value:parseFloat(fd.get('value'))||null,probability:parseInt(fd.get('probability'))||null,expected_close_date:fd.get('expected_close_date')||null,description:fd.get('description')||null}; if(dealId){await api('/pipelines/'+selectedPipelineId+'/opportunities/'+dealId,{method:'PATCH',body:JSON.stringify(data)})} else{data.stage_id=stageId;await api('/pipelines/'+selectedPipelineId+'/opportunities',{method:'POST',body:JSON.stringify(data)})} showToast(dealId?'Deal updated โœ“':'Deal created โœ“'); btn.closest('.modal-overlay').remove(); document.querySelectorAll('.kanban-col').forEach(col=>loadStageOpps(col.dataset.stageId)); }catch(e){showToast(e.message,'error')} btn.textContent=dealId?'Update':'Create';btn.disabled=false; } async function showDealDetail(dealId){ try{ const d=await api('/pipelines/'+selectedPipelineId+'/opportunities/'+dealId); const stageInfo=pipelineCache[selectedPipelineId]?.stages?.find(s=>s.id===d.stage_id); const mc=$('main-content'); mc.innerHTML='

โ†Deal

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

๐Ÿ“‹ Deal Info

' +'
Name'+esc(d.name||'')+'
' +'
Stage'+esc(stageInfo?.name||d.stage_name||'--')+'
' +'
Value'+(d.value?' mc.innerHTML='

๐Ÿ”„ Pipelines

Loading pipelines...
'; try{ const pl=await api('/pipelines'); if(!Array.isArray(pl)){ mc.innerHTML='

๐Ÿ”„ Pipelines

๐Ÿ”„

No pipelines configured yet

'; return; } let html='

๐Ÿ”„ Pipelines

' +pl.map(p=>'

'+esc(p.name||'Pipeline')+'

' +'

'+plural(p.stages?.length||0,'stage')+' ยท '+plural(p.deal_count||0,'deal')+'

' +'
').join('') +'
'; mc.innerHTML=html; }catch(e){mc.innerHTML='

๐Ÿ”„ Pipelines

โš ๏ธ

'+esc(e.message)+'

'} } async function showPipeline(id){ showToast('Pipeline detail coming in Phase 2','info'); } /* ===== TAGS ===== */ async function renderTags(){ const mc=$('main-content'); mc.innerHTML='

๐Ÿท Tags

Loading...
'; try{ const tg=await api('/tags'); if(!Array.isArray(tg))tg=tg?.data||tg?.tags||[]; mc.innerHTML='

๐Ÿท Tags

' +'
' +'' +tg.map(t=>'' +'' +'' +'' +'' +'').join('') +'
NameColorTypeContacts
'+esc(t.name||'')+''+esc(t.color||'Default')+''+(t.is_system?'System':'Custom')+''+(t.contact_count||0)+'
' +(tg.length===0?'
๐Ÿท

No tags yet

':'') +'
'; }catch(e){mc.innerHTML='

๐Ÿท Tags

โš ๏ธ

'+esc(e.message)+'

'} } function showTagForm(){ 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 saveTag(btn){ const form=btn.closest('.modal').querySelector('#tag-form'); const fd=new FormData(form); try{ await api('/tags',{method:'POST',body:JSON.stringify({name:fd.get('name'),color:fd.get('color')})}); showToast('Tag created โœ“'); btn.closest('.modal-overlay').remove(); renderTags(); }catch(e){showToast(e.message,'error')} } /* ===== LISTS ===== */ async function renderLists(){ const mc=$('main-content'); mc.innerHTML='

๐Ÿ“‹ Lists

Loading...
'; try{ const ls=await api('/lists'); if(!Array.isArray(ls))ls=ls?.data||ls?.items||[]; mc.innerHTML='

๐Ÿ“‹ Lists

' +'
' +'' +ls.map(l=>'' +'' +'' +'' +'').join('') +'
NameDescriptionMembers
'+esc(l.name||'')+''+esc(l.description||'--')+''+(l.member_count||l.contact_count||0)+'
' +(ls.length===0?'
๐Ÿ“‹

No lists yet

':'') +'
'; }catch(e){mc.innerHTML='

๐Ÿ“‹ Lists

โš ๏ธ

'+esc(e.message)+'

'} } function showListForm(){ 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 saveList(btn){ const form=btn.closest('.modal').querySelector('#list-form'); const fd=new FormData(form); try{ await api('/lists',{method:'POST',body:JSON.stringify({name:fd.get('name'),description:fd.get('description')})}); showToast('List created โœ“'); btn.closest('.modal-overlay').remove(); renderLists(); }catch(e){showToast(e.message,'error')} } /* ===== COMMUNICATIONS ===== */ async function renderComms(){ const mc=$('main-content'); mc.innerHTML='

โœ‰๏ธ Communications

' +'

๐Ÿ“ง Email

' +'

Email provider settings and message log.

' +'

Coming in Phase 4 โ€” SMTP provider config, template manager, send history.

' +'
' +'

๐Ÿ“ฑ SMS

' +'

SMS provider settings and message log.

' +'

Coming in Phase 4 โ€” Twilio/Telnyx config, template manager, send history.

' +'
'; } /* ===== SETTINGS ===== */ async function renderSettings(){ const mc=$('main-content'); try{ const acct=await api('/account'); const acc=Array.isArray(acct)?acct[0]||{}:acct; mc.innerHTML='

โš™๏ธ Settings

' +'

๐Ÿ‘ค Profile

' +'
' +'' +'' +'' +'' +'
' +'
' +'
'; }catch(e){mc.innerHTML='

โš™๏ธ Settings

โš ๏ธ

'+esc(e.message)+'

'} } async function saveSettings(btn){ const form=document.getElementById('settings-form'); if(!form)return; const fd=new FormData(form); const data={name:fd.get('name'),email:fd.get('email'),phone:fd.get('phone'),company_name:fd.get('company_name')}; btn.textContent='Saving...';btn.disabled=true; try{ await api('/account',{method:'PATCH',body:JSON.stringify(data)}); showToast('Settings saved โœ“'); }catch(e){showToast(e.message,'error')} btn.textContent='Save Changes';btn.disabled=false; } /* ===== INIT ===== */ async function loadUser(){ try{ const u=await api('/auth/me'); user=u; $('avatar-letter').textContent=(u.name||u.email||'?')[0].toUpperCase(); $('user-name').textContent=u.name||'User'; $('user-email').textContent=u.email||''; }catch(e){ // Fallback: try getting from account endpoint try{ const acct=await api('/account'); const u=Array.isArray(acct)?acct[0]:acct; if(u){ $('avatar-letter').textContent=(u.name||u.email||'?')[0].toUpperCase(); $('user-name').textContent=u.name||'User'; $('user-email').textContent=u.email||''; } }catch(e2){ // silently fail, user info is cosmetic } } } function render(){ if(token){ hide('login-page'); show('app'); loadUser(); showPage('dashboard'); }else{ show('login-page'); hide('app'); } } render(); +Number(d.value).toLocaleString():'--')+'
' +'
Probability'+(d.probability?d.probability+'%':'--')+'
' +'
Close Date'+(d.expected_close_date?new Date(d.expected_close_date).toLocaleDateString():'--')+'
' +'

๐Ÿ“ Description

' +'

'+(d.description||'No description')+'

' +'

๐Ÿ‘ค Contact

'+(d.contact_name||'No contact linked')+'

' +'

๐Ÿ“… Timeline

' +'
Created'+(d.created_at?new Date(d.created_at).toLocaleDateString():'--')+'
' +'
Updated'+(d.updated_at?new Date(d.updated_at).toLocaleDateString():'--')+'
' +'
'; }catch(e){showToast(e.message,'error')} } function switchPipeline(id){selectedPipelineId=id;renderPipelineKanban()} /* === PHASE 2 KANBAN END === */ async function renderPipelines_deprecated(){ const mc=$('main-content'); mc.innerHTML='

๐Ÿ”„ Pipelines

Loading pipelines...
'; try{ const pl=await api('/pipelines'); if(!Array.isArray(pl)){ mc.innerHTML='

๐Ÿ”„ Pipelines

๐Ÿ”„

No pipelines configured yet

'; return; } let html='

๐Ÿ”„ Pipelines

' +pl.map(p=>'

'+esc(p.name||'Pipeline')+'

' +'

'+plural(p.stages?.length||0,'stage')+' ยท '+plural(p.deal_count||0,'deal')+'

' +'
').join('') +'
'; mc.innerHTML=html; }catch(e){mc.innerHTML='

๐Ÿ”„ Pipelines

โš ๏ธ

'+esc(e.message)+'

'} } async function showPipeline(id){ showToast('Pipeline detail coming in Phase 2','info'); } /* ===== TAGS ===== */ async function renderTags(){ const mc=$('main-content'); mc.innerHTML='

๐Ÿท Tags

Loading...
'; try{ const tg=await api('/tags'); if(!Array.isArray(tg))tg=tg?.data||tg?.tags||[]; mc.innerHTML='

๐Ÿท Tags

' +'
' +'' +tg.map(t=>'' +'' +'' +'' +'' +'').join('') +'
NameColorTypeContacts
'+esc(t.name||'')+''+esc(t.color||'Default')+''+(t.is_system?'System':'Custom')+''+(t.contact_count||0)+'
' +(tg.length===0?'
๐Ÿท

No tags yet

':'') +'
'; }catch(e){mc.innerHTML='

๐Ÿท Tags

โš ๏ธ

'+esc(e.message)+'

'} } function showTagForm(){ 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 saveTag(btn){ const form=btn.closest('.modal').querySelector('#tag-form'); const fd=new FormData(form); try{ await api('/tags',{method:'POST',body:JSON.stringify({name:fd.get('name'),color:fd.get('color')})}); showToast('Tag created โœ“'); btn.closest('.modal-overlay').remove(); renderTags(); }catch(e){showToast(e.message,'error')} } /* ===== LISTS ===== */ async function renderLists(){ const mc=$('main-content'); mc.innerHTML='

๐Ÿ“‹ Lists

Loading...
'; try{ const ls=await api('/lists'); if(!Array.isArray(ls))ls=ls?.data||ls?.items||[]; mc.innerHTML='

๐Ÿ“‹ Lists

' +'
' +'' +ls.map(l=>'' +'' +'' +'' +'').join('') +'
NameDescriptionMembers
'+esc(l.name||'')+''+esc(l.description||'--')+''+(l.member_count||l.contact_count||0)+'
' +(ls.length===0?'
๐Ÿ“‹

No lists yet

':'') +'
'; }catch(e){mc.innerHTML='

๐Ÿ“‹ Lists

โš ๏ธ

'+esc(e.message)+'

'} } function showListForm(){ 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 saveList(btn){ const form=btn.closest('.modal').querySelector('#list-form'); const fd=new FormData(form); try{ await api('/lists',{method:'POST',body:JSON.stringify({name:fd.get('name'),description:fd.get('description')})}); showToast('List created โœ“'); btn.closest('.modal-overlay').remove(); renderLists(); }catch(e){showToast(e.message,'error')} } /* ===== COMMUNICATIONS ===== */ async function renderComms(){ const mc=$('main-content'); mc.innerHTML='

โœ‰๏ธ Communications

' +'

๐Ÿ“ง Email

' +'

Email provider settings and message log.

' +'

Coming in Phase 4 โ€” SMTP provider config, template manager, send history.

' +'
' +'

๐Ÿ“ฑ SMS

' +'

SMS provider settings and message log.

' +'

Coming in Phase 4 โ€” Twilio/Telnyx config, template manager, send history.

' +'
'; } /* ===== SETTINGS ===== */ async function renderSettings(){ const mc=$('main-content'); try{ const acct=await api('/account'); const acc=Array.isArray(acct)?acct[0]||{}:acct; mc.innerHTML='

โš™๏ธ Settings

' +'

๐Ÿ‘ค Profile

' +'
' +'' +'' +'' +'' +'
' +'
' +'
'; }catch(e){mc.innerHTML='

โš™๏ธ Settings

โš ๏ธ

'+esc(e.message)+'

'} } async function saveSettings(btn){ const form=document.getElementById('settings-form'); if(!form)return; const fd=new FormData(form); const data={name:fd.get('name'),email:fd.get('email'),phone:fd.get('phone'),company_name:fd.get('company_name')}; btn.textContent='Saving...';btn.disabled=true; try{ await api('/account',{method:'PATCH',body:JSON.stringify(data)}); showToast('Settings saved โœ“'); }catch(e){showToast(e.message,'error')} btn.textContent='Save Changes';btn.disabled=false; } /* ===== INIT ===== */ async function loadUser(){ try{ const u=await api('/auth/me'); user=u; $('avatar-letter').textContent=(u.name||u.email||'?')[0].toUpperCase(); $('user-name').textContent=u.name||'User'; $('user-email').textContent=u.email||''; }catch(e){ // Fallback: try getting from account endpoint try{ const acct=await api('/account'); const u=Array.isArray(acct)?acct[0]:acct; if(u){ $('avatar-letter').textContent=(u.name||u.email||'?')[0].toUpperCase(); $('user-name').textContent=u.name||'User'; $('user-email').textContent=u.email||''; } }catch(e2){ // silently fail, user info is cosmetic } } } function render(){ if(token){ hide('login-page'); show('app'); loadUser(); showPage('dashboard'); }else{ show('login-page'); hide('app'); } } render();