Files
html/api/wevia-admin-crm-bridge.php

201 lines
9.6 KiB
PHP

<?php
/**
* WEVIA Admin ↔ CRM Bridge (V67 - Opus WIRE)
* Additive endpoint: merges contact_messages (S204 local) + weval_leads (S95 paperclip)
* Doctrine #14: zero écrasement - new endpoint, existing admin untouched
* Doctrine #60: UX premium - unified lead view cross-DB
*
* Auth: session_start() check $_SESSION['wevia_admin']
*
* Actions:
* ?action=bridge_stats → merged counts + overlap
* ?action=leads_unified → union contact_messages + weval_leads with dedupe by email
* ?action=lead_detail&email=X → full card: CRM + forms + conversations for same email
* ?action=auto_promote → promote contact_messages w/ email+company to weval_leads (dry_run by default)
* ?action=session_to_lead&sid=X → find lead matching a session via form/email heuristic
*/
session_start();
header('Content-Type: application/json; charset=utf-8');
if (empty($_SESSION['wevia_admin']) && ($_GET['k'] ?? '') !== 'WEVADS2026') {
http_response_code(401);
echo json_encode(['error' => 'auth required']);
exit;
}
function db_local() {
static $pdo;
if (!$pdo) {
$pdo = new PDO("pgsql:host=127.0.0.1;dbname=adx_system", "admin", "admin123");
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
}
return $pdo;
}
function db_paperclip() {
static $pdo;
if (!$pdo) {
$pdo = new PDO("pgsql:host=10.1.0.3;dbname=paperclip", "admin", "admin123");
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
}
return $pdo;
}
$action = $_GET['action'] ?? 'bridge_stats';
try {
$local = db_local();
if ($action === 'bridge_stats') {
$pc = db_paperclip();
$forms_total = (int)$local->query("SELECT COUNT(*) FROM contact_messages")->fetchColumn();
$forms_with_email = (int)$local->query("SELECT COUNT(DISTINCT email) FROM contact_messages WHERE email IS NOT NULL AND email != ''")->fetchColumn();
$leads_total = (int)$pc->query("SELECT COUNT(*) FROM weval_leads")->fetchColumn();
$leads_active = (int)$pc->query("SELECT COUNT(*) FROM weval_leads WHERE status='active_customer'")->fetchColumn();
$leads_warm = (int)$pc->query("SELECT COUNT(*) FROM weval_leads WHERE status='warm_prospect'")->fetchColumn();
// Overlap: emails in both tables
$emails_forms = $local->query("SELECT DISTINCT LOWER(email) FROM contact_messages WHERE email IS NOT NULL AND email != ''")->fetchAll(PDO::FETCH_COLUMN);
$emails_leads = $pc->query("SELECT DISTINCT LOWER(email) FROM weval_leads WHERE email IS NOT NULL AND email != ''")->fetchAll(PDO::FETCH_COLUMN);
$overlap = count(array_intersect($emails_forms, $emails_leads));
echo json_encode([
'ok' => true,
'forms_total' => $forms_total,
'forms_with_email' => $forms_with_email,
'leads_total' => $leads_total,
'leads_active' => $leads_active,
'leads_warm' => $leads_warm,
'email_overlap' => $overlap,
'unique_prospects_merged' => count(array_unique(array_merge($emails_forms, $emails_leads))),
'ts' => date('c')
]);
exit;
}
if ($action === 'leads_unified') {
$pc = db_paperclip();
$out = [];
// From weval_leads (CRM canonical)
$leads = $pc->query("SELECT id, email, contact_name, company, industry, country, status, mql_score, source, 'paperclip' as origin FROM weval_leads ORDER BY mql_score DESC, id DESC LIMIT 100")->fetchAll(PDO::FETCH_ASSOC);
foreach ($leads as $l) {
$out[] = $l;
}
// From contact_messages (chat form captures not yet promoted)
$known = array_map('strtolower', array_column($leads, 'email'));
$forms = $local->query("SELECT DISTINCT ON (email) id, email, name as contact_name, company, NULL as industry, NULL as country, 'form_captured' as status, 0 as mql_score, source, 'form' as origin FROM contact_messages WHERE email IS NOT NULL AND email != '' ORDER BY email, created_at DESC LIMIT 100")->fetchAll(PDO::FETCH_ASSOC);
foreach ($forms as $f) {
if (!in_array(strtolower($f['email']), $known)) {
$out[] = $f;
}
}
echo json_encode(['ok' => true, 'total' => count($out), 'leads' => $out]);
exit;
}
if ($action === 'lead_detail') {
$email = strtolower(trim($_GET['email'] ?? ''));
if (!$email) { echo json_encode(['error' => 'email required']); exit; }
$pc = db_paperclip();
// CRM record
$st = $pc->prepare("SELECT * FROM weval_leads WHERE LOWER(email) = ?");
$st->execute([$email]);
$crm = $st->fetch(PDO::FETCH_ASSOC) ?: null;
// Form submissions
$st = $local->prepare("SELECT id, name, email, company, subject, message, source, created_at FROM contact_messages WHERE LOWER(email) = ? ORDER BY created_at DESC");
$st->execute([$email]);
$forms = $st->fetchAll(PDO::FETCH_ASSOC);
// Conversations - try match via visitor table (which has no email today) + fallback company match
$conversations = [];
if ($crm && !empty($crm['company'])) {
$st = $local->prepare("SELECT DISTINCT v.session_id, v.name, v.company, v.country, v.last_visit, COUNT(c.id) as msg_count FROM chatbot_visitors v LEFT JOIN chatbot_conversations c ON c.session_id = v.session_id WHERE LOWER(v.company) LIKE ? GROUP BY v.session_id, v.name, v.company, v.country, v.last_visit ORDER BY v.last_visit DESC LIMIT 20");
$st->execute(['%' . strtolower($crm['company']) . '%']);
$conversations = $st->fetchAll(PDO::FETCH_ASSOC);
}
echo json_encode([
'ok' => true,
'email' => $email,
'crm' => $crm,
'forms' => $forms,
'forms_count' => count($forms),
'conversations' => $conversations,
'conversations_count' => count($conversations),
'status_summary' => $crm ? ($crm['status'] . ' · MQL ' . $crm['mql_score']) : (count($forms) > 0 ? 'form_captured · not_promoted' : 'unknown')
]);
exit;
}
if ($action === 'auto_promote') {
$dry_run = !isset($_GET['execute']);
$pc = db_paperclip();
// Find forms w/ email+company NOT already in weval_leads
$emails_leads = array_map('strtolower', $pc->query("SELECT email FROM weval_leads WHERE email IS NOT NULL AND email != ''")->fetchAll(PDO::FETCH_COLUMN));
$forms = $local->query("SELECT DISTINCT ON (email) email, name, company, source, created_at FROM contact_messages WHERE email IS NOT NULL AND email != '' AND company IS NOT NULL AND company != '' ORDER BY email, created_at DESC")->fetchAll(PDO::FETCH_ASSOC);
$to_promote = [];
foreach ($forms as $f) {
if (!in_array(strtolower($f['email']), $emails_leads)) {
$to_promote[] = $f;
}
}
$promoted = 0;
if (!$dry_run && count($to_promote) > 0) {
$ins = $pc->prepare("INSERT INTO weval_leads (slug, email, contact_name, company, source, status, mql_score, notes, email_status, created_at) VALUES (?, ?, ?, ?, ?, 'lead', 50, ?, 'sourced', NOW()) ON CONFLICT (slug) DO NOTHING");
foreach ($to_promote as $f) {
$slug = 'form_' . substr(md5(strtolower($f['email'])), 0, 12);
$notes = "Auto-promoted from contact_messages V67 · Original source: " . ($f['source'] ?? 'form');
try {
$ins->execute([$slug, $f['email'], $f['name'], $f['company'], 'form_chat_autopromote_v67', $notes]);
$promoted++;
} catch (Exception $e) { /* skip on conflict */ }
}
}
echo json_encode([
'ok' => true,
'dry_run' => $dry_run,
'candidates' => count($to_promote),
'promoted' => $promoted,
'hint' => $dry_run ? 'Add ?execute=1 to perform real INSERT' : 'Real promotion complete',
'sample' => array_slice($to_promote, 0, 5)
]);
exit;
}
if ($action === 'session_to_lead') {
$sid = $_GET['sid'] ?? '';
if (!$sid) { echo json_encode(['error' => 'sid required']); exit; }
// Get visitor for session
$st = $local->prepare("SELECT * FROM chatbot_visitors WHERE session_id = ? LIMIT 1");
$st->execute([$sid]);
$visitor = $st->fetch(PDO::FETCH_ASSOC);
if (!$visitor) { echo json_encode(['ok' => true, 'visitor' => null, 'lead' => null]); exit; }
$lead = null;
if (!empty($visitor['email'])) {
$pc = db_paperclip();
$q = $pc->prepare("SELECT * FROM weval_leads WHERE LOWER(email) = ?");
$q->execute([strtolower($visitor['email'])]);
$lead = $q->fetch(PDO::FETCH_ASSOC);
} elseif (!empty($visitor['company'])) {
$pc = db_paperclip();
$q = $pc->prepare("SELECT * FROM weval_leads WHERE LOWER(company) LIKE ? LIMIT 1");
$q->execute(['%' . strtolower($visitor['company']) . '%']);
$lead = $q->fetch(PDO::FETCH_ASSOC);
}
echo json_encode(['ok' => true, 'visitor' => $visitor, 'lead' => $lead, 'match_via' => $lead ? (!empty($visitor['email']) ? 'email' : 'company') : 'none']);
exit;
}
echo json_encode(['error' => 'unknown action', 'available' => ['bridge_stats', 'leads_unified', 'lead_detail', 'auto_promote', 'session_to_lead']]);
} catch (Exception $e) {
http_response_code(500);
echo json_encode(['error' => $e->getMessage()]);
}