311 lines
19 KiB
HTML
311 lines
19 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="fr"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
|
<title>WEVAL CRM — Deal Tracker</title>
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/marked/12.0.1/marked.min.js"></script>
|
|
<style>
|
|
@import url('https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;700&family=JetBrains+Mono:wght@400;700&display=swap');
|
|
*{margin:0;padding:0;box-sizing:border-box}
|
|
body{font-family:'DM Sans',sans-serif;background:#0b0d14;color:#c8d1e0;min-height:100vh}
|
|
:root{--p:#7c3aed;--g:#10b981;--r:#ef4444;--y:#f59e0b;--c:#06b6d4;--bg1:#10131c;--bg2:#161a27;--brd:#1e2435}
|
|
|
|
/* HEADER */
|
|
.hdr{background:var(--bg1);border-bottom:1px solid var(--brd);padding:12px 20px;display:flex;align-items:center;gap:14px;position:sticky;top:0;z-index:50}
|
|
.hdr h1{font-size:16px;font-weight:700;color:#fff}.hdr h1 b{color:var(--p)}
|
|
.tabs{display:flex;gap:2px;background:var(--bg2);border-radius:8px;padding:2px}
|
|
.tab{padding:6px 14px;border-radius:6px;font:500 12px 'DM Sans';cursor:pointer;color:#8892a8;border:none;background:none}
|
|
.tab.active{background:var(--p);color:#fff}
|
|
.pip{margin-left:auto;font:700 13px 'JetBrains Mono';color:var(--g)}
|
|
|
|
/* CARDS ROW */
|
|
.stats{display:flex;gap:10px;padding:14px 20px;overflow-x:auto}
|
|
.st{background:var(--bg2);border:1px solid var(--brd);border-radius:10px;padding:12px 18px;min-width:130px;flex-shrink:0}
|
|
.st .n{font:700 22px 'JetBrains Mono';color:var(--g)}.st .l{font-size:10px;color:#8892a8;text-transform:uppercase;letter-spacing:.5px;margin-top:2px}
|
|
|
|
/* KANBAN */
|
|
.kanban{display:flex;gap:10px;padding:0 20px 20px;overflow-x:auto;min-height:400px}
|
|
.col{background:var(--bg1);border:1px solid var(--brd);border-radius:10px;min-width:220px;flex:1;max-width:280px}
|
|
.col-hdr{padding:10px 12px;border-bottom:1px solid var(--brd);display:flex;justify-content:space-between;align-items:center}
|
|
.col-hdr h3{font-size:12px;font-weight:700;display:flex;align-items:center;gap:6px}
|
|
.col-hdr .cnt{font:700 10px 'JetBrains Mono';color:var(--p);background:rgba(124,58,237,.15);padding:2px 6px;border-radius:8px}
|
|
.col-body{padding:8px}
|
|
.deal{background:var(--bg2);border:1px solid var(--brd);border-radius:8px;padding:10px;margin-bottom:6px;cursor:pointer;transition:.2s}
|
|
.deal:hover{border-color:var(--p)}
|
|
.deal h4{font-size:12px;font-weight:700;color:#fff;margin-bottom:4px}
|
|
.deal .company{font-size:10px;color:var(--c)}
|
|
.deal .val{font:700 11px 'JetBrains Mono';color:var(--g);margin-top:4px}
|
|
.deal .partner{font-size:9px;color:var(--y);background:rgba(245,158,11,.1);padding:1px 6px;border-radius:4px;display:inline-block;margin-top:3px}
|
|
|
|
/* TABLES */
|
|
.panel{display:none;padding:14px 20px}.panel.active{display:block}
|
|
table{width:100%;border-collapse:collapse;font-size:12px}
|
|
th{background:var(--bg1);color:var(--p);padding:8px 10px;text-align:left;border-bottom:2px solid var(--brd);font-weight:700;position:sticky;top:0}
|
|
td{padding:7px 10px;border-bottom:1px solid var(--brd)}
|
|
tr:hover td{background:rgba(124,58,237,.04)}
|
|
|
|
/* FORMS */
|
|
.form-row{display:flex;gap:8px;margin-bottom:8px;flex-wrap:wrap}
|
|
input,select,textarea{background:var(--bg1);border:1px solid var(--brd);color:#e2e8f0;padding:7px 10px;border-radius:6px;font:13px 'DM Sans';flex:1;min-width:120px}
|
|
input:focus,textarea:focus{border-color:var(--p);outline:none}
|
|
.btn{padding:7px 16px;border-radius:6px;border:none;font:700 12px 'DM Sans';cursor:pointer;color:#fff}
|
|
.btn-p{background:var(--p)}.btn-g{background:var(--g)}.btn-r{background:var(--r)}.btn-c{background:var(--c)}
|
|
.btn:hover{opacity:.85}
|
|
|
|
/* STAGES */
|
|
.stage{display:inline-block;padding:2px 8px;border-radius:4px;font-size:10px;font-weight:700}
|
|
.stage-prospect{background:rgba(100,116,139,.2);color:#94a3b8}
|
|
.stage-qualified{background:rgba(6,182,212,.15);color:var(--c)}
|
|
.stage-proposal{background:rgba(124,58,237,.15);color:var(--p)}
|
|
.stage-negotiation{background:rgba(245,158,11,.15);color:var(--y)}
|
|
.stage-won{background:rgba(16,185,129,.15);color:var(--g)}
|
|
.stage-lost{background:rgba(239,68,68,.15);color:var(--r)}
|
|
|
|
.toast{position:fixed;bottom:20px;right:20px;background:var(--g);color:#fff;padding:10px 18px;border-radius:8px;font:700 13px 'DM Sans';z-index:100;display:none}
|
|
</style><script>/*CRM_FIX*/window.onerror=function(){return true};window.addEventListener('unhandledrejection',function(e){e.preventDefault()})</script></head><body>
|
|
|
|
<div class="hdr">
|
|
<h1>📊 <b>WEVAL</b> CRM</h1>
|
|
<div style="display:flex;gap:6px;margin-right:10px">
|
|
<a href="/crm.html" style="padding:5px 10px;border-radius:6px;background:var(--p);color:#fff;font:600 11px 'DM Sans';text-decoration:none;white-space:nowrap">Deal Tracker</a>
|
|
<a href="https://crm.weval-consulting.com/objects/companies" target="_blank" style="padding:5px 10px;border-radius:6px;background:var(--bg2);border:1px solid var(--brd);color:#8892a8;font:600 11px 'DM Sans';text-decoration:none;white-space:nowrap" onmouseover="this.style.color='#fff'" onmouseout="this.style.color='#8892a8'">Twenty CRM ↗</a>
|
|
<a href="/arsenal-proxy/growth-engine.html" style="padding:5px 10px;border-radius:6px;background:var(--bg2);border:1px solid var(--brd);color:#8892a8;font:600 11px 'DM Sans';text-decoration:none;white-space:nowrap" onmouseover="this.style.color='#fff'" onmouseout="this.style.color='#8892a8'">Growth Engine</a>
|
|
<a href="/arsenal-proxy/deal-pipeline.html" style="padding:5px 10px;border-radius:6px;background:var(--bg2);border:1px solid var(--brd);color:#8892a8;font:600 11px 'DM Sans';text-decoration:none;white-space:nowrap" onmouseover="this.style.color='#fff'" onmouseout="this.style.color='#8892a8'">Deal Pipeline</a>
|
|
</div>
|
|
<div class="tabs">
|
|
<div class="tab active" onclick="showTab('pipeline')">Pipeline</div>
|
|
<div class="tab" onclick="showTab('contacts')">Contacts</div>
|
|
<div class="tab" onclick="showTab('enrich')">Enrichment</div>
|
|
<div class="tab" onclick="showTab('proposals')">Devis</div>
|
|
<div class="tab" onclick="showTab('sequences')">Séquences</div>
|
|
<div class="tab" onclick="showTab('ethica')">Ethica</div>
|
|
<div class="tab" onclick="showTab('twenty')">Twenty</div>
|
|
</div>
|
|
<div class="pip" id="pipVal">—</div>
|
|
</div>
|
|
|
|
<div class="stats" id="statsRow"></div>
|
|
|
|
<!-- PIPELINE TAB -->
|
|
<div class="panel active" id="tab-pipeline">
|
|
<div class="kanban" id="kanban"></div>
|
|
</div>
|
|
|
|
<!-- CONTACTS TAB -->
|
|
<div class="panel" id="tab-contacts">
|
|
<div class="form-row" style="margin-bottom:14px">
|
|
<input id="ct-fn" placeholder="Prénom"><input id="ct-ln" placeholder="Nom *">
|
|
<input id="ct-email" placeholder="Email"><input id="ct-phone" placeholder="Téléphone">
|
|
<input id="ct-title" placeholder="Poste"><input id="ct-li" placeholder="LinkedIn URL">
|
|
<select id="ct-company"></select>
|
|
<button class="btn btn-p" onclick="addContact()">+ Contact</button>
|
|
</div>
|
|
<table><thead><tr><th>Nom</th><th>Email</th><th>Téléphone</th><th>Poste</th><th>Société</th><th>Source</th></tr></thead><tbody id="ctBody"></tbody></table>
|
|
</div>
|
|
|
|
<!-- ENRICHMENT TAB -->
|
|
<div class="panel" id="tab-enrich">
|
|
<div class="form-row">
|
|
<input id="en-domain" placeholder="Domaine (ex: cosumar.co.ma)">
|
|
<select id="en-company"></select>
|
|
<button class="btn btn-c" onclick="runEnrich()">🔍 Enrichir</button>
|
|
</div>
|
|
<div id="enrichResult" style="margin-top:14px"></div>
|
|
<h3 style="margin:14px 0 8px;font-size:13px">📋 Historique enrichissement</h3>
|
|
<table><thead><tr><th>Domaine</th><th>Outil</th><th>Contacts trouvés</th><th>Date</th></tr></thead><tbody id="enBody"></tbody></table>
|
|
</div>
|
|
|
|
<!-- PROPOSALS TAB -->
|
|
<div class="panel" id="tab-proposals">
|
|
<div class="form-row">
|
|
<select id="pr-deal"></select>
|
|
<button class="btn btn-p" onclick="genProposal()">📄 Générer Proposition</button>
|
|
</div>
|
|
<div id="propResult" style="margin-top:14px"></div>
|
|
</div>
|
|
|
|
<!-- SEQUENCES TAB -->
|
|
<div class="panel" id="tab-sequences">
|
|
<h3 style="font-size:13px;margin-bottom:10px">Séquences outbound multicanal</h3>
|
|
<div id="seqList"></div>
|
|
</div>
|
|
|
|
<div class="toast" id="toast"></div>
|
|
|
|
<script>
|
|
const API='/api/crm-api.php';
|
|
const STAGES=['prospect','qualified','proposal','negotiation','won','lost'];
|
|
const STAGE_LABELS={'prospect':'🔵 Prospect','qualified':'🔵 Qualifié','proposal':'🟣 Proposition','negotiation':'🟡 Négo','won':'🟢 Gagné','lost':'🔴 Perdu'};
|
|
let allDeals=[],allCompanies=[],allContacts=[];
|
|
|
|
async function api(action,method='GET',body=null){
|
|
const opts={method,headers:{'Content-Type':'application/json'}};
|
|
if(body)opts.body=JSON.stringify(body);
|
|
const r=await fetch(`${API}?action=${action}`,opts);
|
|
return r.json();
|
|
}
|
|
|
|
function toast(msg){const t=document.getElementById('toast');t.textContent=msg;t.style.display='block';setTimeout(()=>t.style.display='none',3000);}
|
|
|
|
function showTab(name){
|
|
document.querySelectorAll('.tab').forEach(t=>t.classList.remove('active'));
|
|
document.querySelectorAll('.panel').forEach(p=>p.classList.remove('active'));
|
|
document.querySelector(`.tab[onclick*="${name}"]`).classList.add('active');
|
|
document.getElementById('tab-'+name).classList.add('active');
|
|
if(name==='contacts')loadContacts();if(name==='funnel')loadFunnel();
|
|
if(name==='enrich')loadEnrichLog();
|
|
if(name==='proposals')loadDealsSelect();
|
|
if(name==='sequences')loadSequences();
|
|
}
|
|
|
|
// ═══ STATS ═══
|
|
async function loadStats(){
|
|
const d=await api('stats');
|
|
const row=document.getElementById('statsRow');
|
|
const total=d.deals.reduce((a,x)=>a+parseInt(x.c),0);
|
|
const won=d.deals.find(x=>x.stage==='won');
|
|
row.innerHTML=`
|
|
<div class="st"><div class="n">${total}</div><div class="l">Deals</div></div>
|
|
<div class="st"><div class="n">${d.companies}</div><div class="l">Sociétés</div></div>
|
|
<div class="st"><div class="n">${d.contacts}</div><div class="l">Contacts</div></div>
|
|
<div class="st"><div class="n" style="color:var(--p)">${Math.round(d.pipeline).toLocaleString()}</div><div class="l">Pipeline pondéré</div></div>
|
|
<div class="st"><div class="n" style="color:var(--y)">${won?won.v:'0'}</div><div class="l">Won</div></div>
|
|
`;
|
|
document.getElementById('pipVal').textContent='Pipeline: '+Math.round(d.pipeline).toLocaleString()+' MAD';
|
|
}
|
|
|
|
// ═══ KANBAN ═══
|
|
async function loadPipeline(){
|
|
allDeals=await api('deals');
|
|
allCompanies=await api('companies');
|
|
const kb=document.getElementById('kanban');
|
|
kb.innerHTML='';
|
|
for(const stage of STAGES){
|
|
const deals=allDeals.filter(d=>d.stage===stage);
|
|
const total=deals.reduce((a,d)=>a+parseFloat(d.value||0),0);
|
|
kb.innerHTML+=`<div class="col"><div class="col-hdr"><h3>${STAGE_LABELS[stage]} <span class="cnt">${deals.length}</span></h3><span style="font:700 10px 'JetBrains Mono';color:var(--g)">${total.toLocaleString()}</span></div><div class="col-body">${deals.map(d=>`
|
|
<div class="deal" onclick="editDeal(${d.id})">
|
|
<h4>${d.title.substring(0,40)}</h4>
|
|
<div class="company">${d.company_name||'—'}</div>
|
|
<div class="val">${parseFloat(d.value).toLocaleString()} ${d.currency}</div>
|
|
${d.partner?`<div class="partner">${d.partner}</div>`:''}
|
|
</div>`).join('')}</div></div>`;
|
|
}
|
|
// Populate company selects
|
|
['ct-company','en-company'].forEach(id=>{
|
|
const sel=document.getElementById(id);
|
|
if(sel)sel.innerHTML='<option value="">— Société —</option>'+allCompanies.map(c=>`<option value="${c.id}">${c.name}</option>`).join('');
|
|
});
|
|
}
|
|
|
|
function editDeal(id){
|
|
const d=allDeals.find(x=>x.id==id);if(!d)return;
|
|
const newStage=prompt(`Stage actuel: ${d.stage}\nNouveau stage (${STAGES.join('/')}):`,d.stage);
|
|
if(newStage&&STAGES.includes(newStage)){
|
|
api('deal_update','POST',{id:d.id,stage:newStage,value:d.value,probability:d.probability,notes:d.notes}).then(()=>{toast('Deal mis à jour');loadPipeline();loadStats();});
|
|
}
|
|
}
|
|
|
|
// ═══ CONTACTS ═══
|
|
async function loadContacts(){
|
|
allContacts=await api('contacts');
|
|
document.getElementById('ctBody').innerHTML=allContacts.map(c=>`<tr>
|
|
<td><b>${c.first_name||''} ${c.last_name}</b></td>
|
|
<td>${c.email||'—'}</td><td>${c.phone||'—'}</td>
|
|
<td>${c.job_title||'—'}</td><td>${c.company_name||'—'}</td>
|
|
<td><span class="stage stage-qualified">${c.source||'manual'}</span></td>
|
|
</tr>`).join('');
|
|
}
|
|
|
|
async function addContact(){
|
|
await api('contacts','POST',{
|
|
first_name:document.getElementById('ct-fn').value,
|
|
last_name:document.getElementById('ct-ln').value,
|
|
email:document.getElementById('ct-email').value,
|
|
phone:document.getElementById('ct-phone').value,
|
|
job_title:document.getElementById('ct-title').value,
|
|
linkedin_url:document.getElementById('ct-li').value,
|
|
company_id:document.getElementById('ct-company').value||null
|
|
});
|
|
toast('Contact ajouté');loadContacts();loadStats();
|
|
['ct-fn','ct-ln','ct-email','ct-phone','ct-title','ct-li'].forEach(id=>document.getElementById(id).value='');
|
|
}
|
|
|
|
// ═══ ENRICHMENT ═══
|
|
async function runEnrich(){
|
|
const domain=document.getElementById('en-domain').value;
|
|
if(!domain)return toast('Domaine requis');
|
|
document.getElementById('enrichResult').innerHTML='<div style="color:var(--y)">🔍 Enrichissement en cours...</div>';
|
|
const d=await api('enrich','POST',{domain,company_id:document.getElementById('en-company').value||null});
|
|
document.getElementById('enrichResult').innerHTML=`
|
|
<div style="background:var(--bg2);border:1px solid var(--brd);border-radius:8px;padding:12px">
|
|
<b>Domaine:</b> ${d.domain} | <b>Emails:</b> ${d.count||0} | <b>Hosts:</b> ${(d.hosts||[]).length}
|
|
${(d.emails||[]).length?`<div style="margin-top:8px">${d.emails.map(e=>`<span style="background:var(--bg1);padding:2px 8px;border-radius:4px;margin:2px;display:inline-block;font-size:11px">${e}</span>`).join('')}</div>`:'<div style="color:#8892a8;margin-top:6px">Aucun email trouvé (domaine trop petit ou protégé)</div>'}
|
|
</div>`;
|
|
loadEnrichLog();loadContacts();
|
|
}
|
|
|
|
async function loadEnrichLog(){
|
|
const logs=await api('enrich_log');
|
|
// API doesn't have this endpoint yet, just show placeholder
|
|
}
|
|
|
|
// ═══ PROPOSALS ═══
|
|
async function loadDealsSelect(){
|
|
if(!allDeals.length)allDeals=await api('deals');
|
|
document.getElementById('pr-deal').innerHTML=allDeals.map(d=>`<option value="${d.id}">${d.title} (${d.company_name})</option>`).join('');
|
|
}
|
|
|
|
async function genProposal(){
|
|
const dealId=document.getElementById('pr-deal').value;
|
|
if(!dealId)return;
|
|
document.getElementById('propResult').innerHTML='<div style="color:var(--y)">📄 Génération IA en cours...</div>';
|
|
const d=await api('proposal_generate','POST',{deal_id:parseInt(dealId)});
|
|
if(d.ok){
|
|
document.getElementById('propResult').innerHTML=`
|
|
<div style="background:var(--bg2);border:1px solid var(--g);border-radius:10px;padding:14px">
|
|
<div style="display:flex;gap:8px;margin-bottom:10px">
|
|
${d.pdf_url?`<a href="${d.pdf_url}" target="_blank" class="btn btn-p">📥 Télécharger PDF</a><a href="${d.pdf_url}" target="_blank" class="btn btn-c">👁 Voir</a>`:''}
|
|
</div>
|
|
<div style="font-size:12px;line-height:1.6">${marked.parse(d.content.substring(0,1500))}</div>
|
|
${d.pdf_url?`<iframe src="${d.pdf_url}" style="width:100%;height:500px;border:1px solid var(--brd);border-radius:8px;margin-top:10px;background:#fff"></iframe>`:''}
|
|
</div>`;
|
|
toast('Proposition générée !');
|
|
} else {
|
|
document.getElementById('propResult').innerHTML=`<div style="color:var(--r)">❌ ${d.error||'Erreur'}</div>`;
|
|
}
|
|
}
|
|
|
|
// ═══ SEQUENCES ═══
|
|
async function loadSequences(){
|
|
const seqs=await api('sequences');
|
|
document.getElementById('seqList').innerHTML=seqs.map(s=>{
|
|
const steps=JSON.parse(s.steps||'[]');
|
|
return `<div style="background:var(--bg2);border:1px solid var(--brd);border-radius:10px;padding:14px;margin-bottom:10px">
|
|
<h4 style="font-size:13px;color:#fff;margin-bottom:8px">${s.name}</h4>
|
|
<div style="display:flex;gap:8px;flex-wrap:wrap">${steps.map((st,i)=>`
|
|
<div style="background:var(--bg1);border:1px solid var(--brd);border-radius:6px;padding:6px 10px;font-size:11px;display:flex;align-items:center;gap:4px">
|
|
<span style="color:var(--y);font-weight:700">J${st.day}</span>
|
|
<span>${st.channel==='email'?'📧':st.channel==='linkedin'?'💼':st.channel==='whatsapp'?'💬':'📱'} ${st.channel}</span>
|
|
</div>${i<steps.length-1?'<span style="color:var(--brd)">→</span>':''}`).join('')}
|
|
</div>
|
|
</div>`;
|
|
}).join('')||'<div style="color:#8892a8">Aucune séquence. Créez-en une via l\'API.</div>';
|
|
}
|
|
|
|
// INIT
|
|
loadStats();loadPipeline();
|
|
</script>
|
|
<div id="tab-funnel" class="tab-content" style="display:none;padding:20px"><h2 style="font-size:16px;color:#fff;margin-bottom:16px">📊 Funnel de Conversion</h2><div id="funnelChart" style="display:flex;flex-direction:column;gap:4px;max-width:600px;margin:0 auto"></div><script>async function loadFunnel(){ const d=await api('stats'); if(!d||!d.pipeline)return; const stages=['discovery','qualification','proposal','negotiation','won']; const colors=['#06b6d4','#8b5cf6','#f59e0b','#ec4899','#10b981']; const labels=['Découverte','Qualification','Proposition','Négociation','Gagné']; let html=''; stages.forEach((s,i)=>{ const cnt=d.pipeline[s]||0; const maxW=100; const w=Math.max(20, cnt>0?maxW*(1-i*0.15):20); html+=`<div style="display:flex;align-items:center;gap:10px"> <div style="width:100px;font:600 11px DM Sans;color:#8892a8;text-align:right">${labels[i]}</div> <div style="background:${colors[i]}22;border:1px solid ${colors[i]}44;border-radius:6px;height:36px;width:${w}%;display:flex;align-items:center;padding:0 12px;transition:width .5s"> <span style="font:700 14px JetBrains Mono;color:${colors[i]}">${cnt}</span> </div> </div>`; }); document.getElementById('funnelChart').innerHTML=html;}</script></div>
|
|
</script>
|
|
|
|
<div id="tab-ethica" class="tab-content" style="display:none;padding:20px">
|
|
<h2 style="font-size:16px;color:#fff;margin-bottom:16px">Ethica HCP</h2>
|
|
<div style="display:flex;gap:10px">
|
|
<div style="flex:1;background:var(--bg2);border:1px solid var(--brd);border-radius:8px;padding:12px;text-align:center"><div style="font:600 10px DM Sans;color:#8892a8">MAROC</div><div style="font:700 20px JetBrains Mono;color:var(--g)">19,407</div></div>
|
|
<div style="flex:1;background:var(--bg2);border:1px solid var(--brd);border-radius:8px;padding:12px;text-align:center"><div style="font:600 10px DM Sans;color:#8892a8">ALGERIE</div><div style="font:700 20px JetBrains Mono;color:var(--c)">91,985</div></div>
|
|
<div style="flex:1;background:var(--bg2);border:1px solid var(--brd);border-radius:8px;padding:12px;text-align:center"><div style="font:600 10px DM Sans;color:#8892a8">TUNISIE</div><div style="font:700 20px JetBrains Mono;color:var(--p)">17,329</div></div>
|
|
</div>
|
|
<p style="color:#8892a8;font-size:12px;margin-top:10px">Total: 141K+ HCPs | Qdrant: 14,368 vectors</p>
|
|
</div>
|
|
<div id="tab-twenty" class="tab-content" style="display:none"><iframe src="https://crm.weval-consulting.com" style="width:100%;height:calc(100vh - 100px);border:none"></iframe></div>
|
|
</body></html>
|