Files
html/api/crm-api.php
2026-04-12 22:57:03 +02:00

268 lines
14 KiB
PHP
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
// === INPUT SANITIZATION ===
function weval_input($key, $type='string', $method='GET') {
$src = $method === 'POST' ? INPUT_POST : INPUT_GET;
$val = filter_input($src, $key, FILTER_SANITIZE_FULL_SPECIAL_CHARS);
if ($val === null || $val === false) {
$val = ($method === 'POST') ? ($_POST[$key] ?? '') : ($_GET[$key] ?? '');
$val = htmlspecialchars(strip_tags(trim($val)), ENT_QUOTES, 'UTF-8');
}
if ($type === 'int') return intval($val);
if ($type === 'email') return filter_var($val, FILTER_SANITIZE_EMAIL);
return $val;
}
require_once __DIR__ . '/_secrets.php';
header('Content-Type: application/json');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET,POST,PUT,DELETE');
header('Access-Control-Allow-Headers: Content-Type');
if($_SERVER['REQUEST_METHOD']==='OPTIONS'){http_response_code(200);exit;}
try{$db=new PDO('pgsql:host=127.0.0.1;dbname=adx_system','admin',weval_secret('WEVAL_PG_ADMIN_PASS'));
$db->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']));
}