setAttribute(PDO::ATTR_ERRMODE,PDO::ERRMODE_EXCEPTION);} catch(Exception $e){die(json_encode(['error'=>$e->getMessage()]));} $action=$_GET['action']??''; $in=json_decode(file_get_contents('php://input'),true)?:[]; switch($action){ // ═══ DASHBOARD STATS ═══ case 'stats': $deals=$db->query("SELECT stage,count(*) as c,coalesce(sum(value),0) as v FROM crm.deals GROUP BY stage ORDER BY c DESC")->fetchAll(PDO::FETCH_ASSOC); $pipeline=$db->query("SELECT coalesce(sum(value*probability/100),0) as weighted FROM crm.deals WHERE stage NOT IN ('won','lost')")->fetchColumn(); $contacts=$db->query("SELECT count(*) FROM crm.contacts")->fetchColumn(); $companies=$db->query("SELECT count(*) FROM crm.companies")->fetchColumn(); $recent=$db->query("SELECT a.type,a.subject,a.completed_at,c.first_name||' '||c.last_name as contact FROM crm.activities a LEFT JOIN crm.contacts c ON a.contact_id=c.id ORDER BY a.completed_at DESC LIMIT 5")->fetchAll(PDO::FETCH_ASSOC); die(json_encode(['deals'=>$deals,'pipeline'=>$pipeline,'contacts'=>$contacts,'companies'=>$companies,'recent_activities'=>$recent])); // ═══ DEALS ═══ case 'deals': if($_SERVER['REQUEST_METHOD']==='GET'){ $r=$db->query("SELECT d.*,c.name as company_name FROM crm.deals d LEFT JOIN crm.companies c ON d.company_id=c.id ORDER BY d.updated_at DESC"); die(json_encode($r->fetchAll(PDO::FETCH_ASSOC))); } if($_SERVER['REQUEST_METHOD']==='POST'){ $s=$db->prepare("INSERT INTO crm.deals(company_id,contact_id,title,stage,value,currency,partner,probability,expected_close,notes) VALUES(?,?,?,?,?,?,?,?,?,?) RETURNING id"); $s->execute([$in['company_id']??null,$in['contact_id']??null,$in['title'],$in['stage']??'prospect',$in['value']??0,$in['currency']??'MAD',$in['partner']??'',$in['probability']??10,$in['expected_close']??null,$in['notes']??'']); die(json_encode(['id'=>$s->fetchColumn(),'ok'=>true])); } break; case 'deal_update': $s=$db->prepare("UPDATE crm.deals SET stage=?,value=?,probability=?,notes=?,updated_at=NOW() WHERE id=?"); $s->execute([$in['stage'],$in['value']??0,$in['probability']??0,$in['notes']??'',$in['id']]); die(json_encode(['ok'=>true])); // ═══ COMPANIES ═══ case 'companies': if($_SERVER['REQUEST_METHOD']==='GET'){ die(json_encode($db->query("SELECT * FROM crm.companies ORDER BY name")->fetchAll(PDO::FETCH_ASSOC))); } if($_SERVER['REQUEST_METHOD']==='POST'){ $s=$db->prepare("INSERT INTO crm.companies(name,domain,industry,country,city,size,notes) VALUES(?,?,?,?,?,?,?) RETURNING id"); $s->execute([$in['name'],$in['domain']??'',$in['industry']??'',$in['country']??'MA',$in['city']??'',$in['size']??'PME',$in['notes']??'']); die(json_encode(['id'=>$s->fetchColumn(),'ok'=>true])); } break; // ═══ CONTACTS ═══ case 'contacts': if($_SERVER['REQUEST_METHOD']==='GET'){ $cid=$_GET['company_id']??''; $q="SELECT ct.*,c.name as company_name FROM crm.contacts ct LEFT JOIN crm.companies c ON ct.company_id=c.id"; if($cid)$q.=" WHERE ct.company_id=".intval($cid); $q.=" ORDER BY ct.created_at DESC LIMIT 100"; die(json_encode($db->query($q)->fetchAll(PDO::FETCH_ASSOC))); } if($_SERVER['REQUEST_METHOD']==='POST'){ $s=$db->prepare("INSERT INTO crm.contacts(company_id,first_name,last_name,email,phone,linkedin_url,job_title,department,source) VALUES(?,?,?,?,?,?,?,?,?) RETURNING id"); $s->execute([$in['company_id']??null,$in['first_name']??'',$in['last_name'],$in['email']??'',$in['phone']??'',$in['linkedin_url']??'',$in['job_title']??'',$in['department']??'',$in['source']??'manual']); die(json_encode(['id'=>$s->fetchColumn(),'ok'=>true])); } break; // ═══ ACTIVITIES ═══ case 'activities': if($_SERVER['REQUEST_METHOD']==='POST'){ $s=$db->prepare("INSERT INTO crm.activities(deal_id,contact_id,type,subject,body,status) VALUES(?,?,?,?,?,?)"); $s->execute([$in['deal_id']??null,$in['contact_id']??null,$in['type'],$in['subject']??'',$in['body']??'',$in['status']??'done']); die(json_encode(['ok'=>true])); } break; // ═══ ENRICHMENT ═══ case 'enrich': $domain=$in['domain']??''; if(!$domain)die(json_encode(['error'=>'domain required'])); // theHarvester $cmd="timeout 30 python3 -c \" from theHarvester.discovery.dnsdumpster import SearchDnsdumpster from theHarvester.discovery.crtsh import SearchCrtsh import asyncio,json async def run(): results={'emails':[],'hosts':[]} try: s=SearchCrtsh('$domain') await s.process() results['hosts']=list(set(s.get_hostnames()))[:20] except:pass print(json.dumps(results)) asyncio.run(run()) \" 2>/dev/null"; $out=shell_exec($cmd); $data=json_decode($out,true)?:['emails'=>[],'hosts'=>[]]; // EmailFinder $cmd2="timeout 20 emailfinder -d $domain 2>/dev/null | head -20"; $emails_raw=shell_exec($cmd2); $emails=[]; if($emails_raw){ preg_match_all('/[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/',$emails_raw,$m); $emails=$m[0]??[]; } $data['emails']=array_merge($data['emails'],$emails); $data['emails']=array_unique($data['emails']); // Log $s=$db->prepare("INSERT INTO crm.enrichment_log(domain,tool,results,contacts_found) VALUES(?,?,?,?)"); $s->execute([$domain,'theharvester+emailfinder',json_encode($data),count($data['emails'])]); // Auto-create contacts $cid=$in['company_id']??null; foreach($data['emails'] as $email){ $parts=explode('@',$email); $name=str_replace('.',' ',$parts[0]); $db->prepare("INSERT INTO crm.contacts(company_id,email,last_name,source) VALUES(?,?,?,?) ON CONFLICT DO NOTHING") ->execute([$cid,$email,$name,'theharvester']); } die(json_encode(['domain'=>$domain,'emails'=>$data['emails'],'hosts'=>$data['hosts'],'count'=>count($data['emails'])])); // ═══ PROPOSALS ═══ case 'proposal_generate': require_once '/opt/wevads/vault/credentials.php'; $deal_id=$in['deal_id']??0; $deal=$db->query("SELECT d.*,c.name as company_name FROM crm.deals d LEFT JOIN crm.companies c ON d.company_id=c.id WHERE d.id=$deal_id")->fetch(PDO::FETCH_ASSOC); if(!$deal)die(json_encode(['error'=>'deal not found'])); $prompt="Genere une proposition commerciale professionnelle pour:\n- Client: {$deal['company_name']}\n- Projet: {$deal['title']}\n- Budget: {$deal['value']} {$deal['currency']}\n- Partenaire: {$deal['partner']}\nInclure: resume executif, scope, planning, tableau prix, conditions."; // Fallback chain: Groq -> Cerebras -> SambaNova $providers = [ ['https://api.groq.com/openai/v1/chat/completions', GROQ_KEY, 'llama-3.3-70b-versatile', 10], ['https://api.cerebras.ai/v1/chat/completions', CEREBRAS_KEY, 'qwen-3-235b-a22b-instruct-2507', 8], ['https://api.sambanova.ai/v1/chat/completions', '9541b2a0-6ddc-4e7d-a957-c348d6119c3f', 'Meta-Llama-3.3-70B-Instruct', 8], ]; $text='';$provider='?'; foreach($providers as $pv){ $ch=curl_init($pv[0]); curl_setopt_array($ch,[CURLOPT_POST=>true,CURLOPT_RETURNTRANSFER=>true,CURLOPT_TIMEOUT=>$pv[3],CURLOPT_CONNECTTIMEOUT=>3, CURLOPT_HTTPHEADER=>['Content-Type: application/json','Authorization: Bearer '.$pv[1]], CURLOPT_POSTFIELDS=>json_encode(['model'=>$pv[2],'messages'=>[['role'=>'user','content'=>$prompt]],'max_tokens'=>2000,'temperature'=>0.7])]); $r=curl_exec($ch);$code=curl_getinfo($ch,CURLINFO_HTTP_CODE);curl_close($ch); if($code==200&&$r){$d=json_decode($r,true);$text=trim($d['choices'][0]['message']['content']??''); if(strlen($text)>50){$provider=$pv[2];break;}} } if(strlen($text)>50){ // Generate PDF $id=uniqid('prop_'); $txt_f="/tmp/{$id}.txt";$pdf_f="/var/www/html/test-report/{$id}.pdf"; file_put_contents($txt_f,$text); shell_exec("python3 /var/www/html/api/_pdf_gen.py $txt_f ".escapeshellarg($deal['title'])." $pdf_f 2>&1"); $pdf_url=file_exists($pdf_f)?"/test-report/{$id}.pdf":null; $s=$db->prepare("INSERT INTO crm.proposals(deal_id,title,content,pdf_url,amount,status) VALUES(?,?,?,?,?,?) RETURNING id"); $s->execute([$deal_id,$deal['title'],$text,$pdf_url,$deal['value'],'draft']); die(json_encode(['ok'=>true,'proposal_id'=>$s->fetchColumn(),'pdf_url'=>$pdf_url,'content'=>$text])); } die(json_encode(['error'=>'AI generation failed'])); // ═══ SEQUENCES ═══ case 'sequences': if($_SERVER['REQUEST_METHOD']==='GET'){ die(json_encode($db->query("SELECT * FROM crm.sequences ORDER BY id DESC")->fetchAll(PDO::FETCH_ASSOC))); } if($_SERVER['REQUEST_METHOD']==='POST'){ $s=$db->prepare("INSERT INTO crm.sequences(name,steps) VALUES(?,?) RETURNING id"); $s->execute([$in['name'],json_encode($in['steps'])]); die(json_encode(['id'=>$s->fetchColumn(),'ok'=>true])); } break; case 'sequence_enroll': $s=$db->prepare("INSERT INTO crm.sequence_enrollments(sequence_id,contact_id,next_action_at) VALUES(?,?,NOW()+interval '1 day') RETURNING id"); $s->execute([$in['sequence_id'],$in['contact_id']]); die(json_encode(['ok'=>true,'id'=>$s->fetchColumn()])); default: case 'enrich_contact': $cid = (int)($in['contact_id'] ?? 0); $first = $in['first_name'] ?? ''; $last = $in['last_name'] ?? ''; $company = $in['company'] ?? ''; $domain = $in['domain'] ?? ''; if ($cid) { $c = $db->prepare("SELECT c.*, co.name as company_name, co.domain as company_domain FROM crm.contacts c LEFT JOIN crm.companies co ON c.company_id=co.id WHERE c.id=?"); $c->execute([$cid]); $row = $c->fetch(PDO::FETCH_ASSOC); if ($row) { $first=$row['first_name']; $last=$row['last_name']; $company=$row['company_name']??''; $domain=$row['company_domain']??''; } } if (!$last) die(json_encode(['error'=>'name required'])); $results = ['phone'=>null,'linkedin_url'=>null,'job_title'=>null,'email'=>null,'sources'=>[]]; $SEARX = 'https://weval-consulting.com/api/searxng-proxy.php?k=WEVSX2026&q='; foreach (["$first $last $company telephone","$first $last $company linkedin","$first $last site:linkedin.com/in"] as $q) { $resp = @file_get_contents($SEARX . urlencode($q), false, stream_context_create(['http'=>['timeout'=>10]])); if (!$resp) continue; $data = json_decode($resp, true); foreach ($data['results'] ?? [] as $r) { $text = ($r['title']??'').' '.($r['content']??'').' '.($r['url']??''); if (!$results['phone']) { preg_match_all('/(?:\+(?:212|33|216|213|1)\s*[-.]?\s*\d[\d\s\-\.]{7,12}|\b0[5-7]\d{2}[\s\-\.]?\d{2}[\s\-\.]?\d{2}[\s\-\.]?\d{2}\b)/', $text, $phones); foreach ($phones[0] as $ph) { $cl=preg_replace('/[^\d+]/','',$ph); if(strlen($cl)>=9){$results['phone']=$cl;$results['sources'][]='searxng';break;} } } if (!$results['linkedin_url'] && preg_match('#(https?://[a-z]{2,3}\.linkedin\.com/in/[a-zA-Z0-9\-]+)#', $text, $m)) { $results['linkedin_url'] = $m[1]; $results['sources'][] = 'linkedin'; } } } if ($cid && ($results['phone']||$results['linkedin_url'])) { $u=[]; $p=[]; if($results['phone']){$u[]="phone=?";$p[]=$results['phone'];} if($results['linkedin_url']){$u[]="linkedin_url=?";$p[]=$results['linkedin_url'];} $u[]="enriched_at=NOW()"; $p[]=$cid; $db->prepare("UPDATE crm.contacts SET ".implode(',',$u)." WHERE id=?")->execute($p); } $db->prepare("INSERT INTO crm.enrichment_log(domain,tool,results,contacts_found) VALUES(?,?,?,?)") ->execute([$domain??$company,'searxng_enrich',json_encode($results),($results['phone']?1:0)+($results['linkedin_url']?1:0)]); // Scrape company website for phone numbers if (!$results['phone'] && $domain) { $html = @file_get_contents("https://$domain", false, stream_context_create(['http'=>['timeout'=>8,'user_agent'=>'Mozilla/5.0']])); if ($html) { preg_match_all('/(?:\+(?:212|33|216|213|1|44|49)\s*[-.]?\s*\d[\d\s\-\.]{7,12}|0[1-9]\d{2}[\s\-\.]?\d{2}[\s\-\.]?\d{2}[\s\-\.]?\d{2})/', $html, $wp); foreach ($wp[0] as $ph) { $cl=preg_replace('/[^\d+]/','',$ph); if(strlen($cl)>=9&&!$results['phone']){$results['phone']=$cl;$results['sources'][]='website';break;} } } } die(json_encode($results)); case 'enrich_all': $limit=(int)($_GET['limit']??$in['limit']??10); $contacts=$db->query("SELECT c.id,c.first_name,c.last_name,co.name as co,co.domain FROM crm.contacts c LEFT JOIN crm.companies co ON c.company_id=co.id WHERE (c.phone IS NULL OR c.phone='') AND c.enriched_at IS NULL ORDER BY c.id LIMIT $limit")->fetchAll(PDO::FETCH_ASSOC); $ok=0; foreach($contacts as $c){ $url="http://localhost/api/crm-api.php?action=enrich_contact"; $resp=@file_get_contents($url,false,stream_context_create(['http'=>['method'=>'POST','header'=>'Content-Type: application/json','content'=>json_encode(['contact_id'=>$c['id']]),'timeout'=>30]])); $d=json_decode($resp,true); if($d&&($d['phone']||$d['linkedin_url']))$ok++; usleep(500000); } die(json_encode(['total'=>count($contacts),'enriched'=>$ok])); die(json_encode(['error'=>'unknown action','actions'=>'stats,deals,deal_update,companies,contacts,activities,enrich,proposal_generate,sequences,sequence_enroll'])); }