268 lines
14 KiB
PHP
268 lines
14 KiB
PHP
<?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']));
|
||
}
|