Files
html/crm.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>