Files
html/api/wevia-admin-crm-bridge-v68.php
opus 20d1d7c38e
Some checks failed
WEVAL NonReg / nonreg (push) Has been cancelled
auto-commit via WEVIA vault_git intent 2026-04-20T00:08:54+00:00
2026-04-20 02:08:54 +02:00

187 lines
14 KiB
PHP

<?php
/**
* WEVIA Admin CRM Bridge V68 - 3 CRMs unified (Paperclip + Twenty + Forms)
* Depth: visitors geo/device/browser/time + SaaS chat (turbo) separated
* Doctrine #14: new endpoint, zero écrasement V67 bridge
*/
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 $p;
if (!$p) { $p = new PDO("pgsql:host=127.0.0.1;dbname=adx_system","admin","admin123"); $p->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); }
return $p;
}
function db_paperclip() {
static $p;
if (!$p) { $p = new PDO("pgsql:host=10.1.0.3;dbname=paperclip","admin","admin123"); $p->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); }
return $p;
}
function db_twenty() {
static $p;
if (!$p) { $p = new PDO("pgsql:host=127.0.0.1;dbname=twenty_db","admin","admin123"); $p->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); }
return $p;
}
$TWENTY_WS = 'workspace_c8qrnq9rmk5wya777sxdjsfb5';
$action = $_GET['action'] ?? 'global_stats';
try {
$local = db_local();
if ($action === 'global_stats') {
$pc = db_paperclip();
$tw = db_twenty();
$data = [
'chat' => [
'messages_total' => (int)$local->query("SELECT COUNT(*) FROM chatbot_conversations")->fetchColumn(),
'sessions_total' => (int)$local->query("SELECT COUNT(DISTINCT session_id) FROM chatbot_conversations")->fetchColumn(),
'today' => (int)$local->query("SELECT COUNT(*) FROM chatbot_conversations WHERE created_at::date = CURRENT_DATE")->fetchColumn(),
'by_source' => []
],
'forms' => [
'total' => (int)$local->query("SELECT COUNT(*) FROM contact_messages")->fetchColumn(),
'with_email' => (int)$local->query("SELECT COUNT(DISTINCT email) FROM contact_messages WHERE email IS NOT NULL AND email != ''")->fetchColumn(),
'by_source' => []
],
'visitors' => [
'site_visits_total' => (int)$local->query("SELECT COUNT(*) FROM site_visits")->fetchColumn(),
'unique_ips' => (int)$local->query("SELECT COUNT(DISTINCT ip) FROM site_visits")->fetchColumn(),
'countries' => (int)$local->query("SELECT COUNT(DISTINCT country) FROM site_visits WHERE country IS NOT NULL AND country != ''")->fetchColumn(),
],
'crm' => [
'paperclip_leads' => (int)$pc->query("SELECT COUNT(*) FROM weval_leads")->fetchColumn(),
'paperclip_active' => (int)$pc->query("SELECT COUNT(*) FROM weval_leads WHERE status='active_customer'")->fetchColumn(),
'twenty_companies' => (int)$tw->query("SELECT COUNT(*) FROM $TWENTY_WS.company")->fetchColumn(),
'twenty_people' => (int)$tw->query("SELECT COUNT(*) FROM $TWENTY_WS.person")->fetchColumn(),
'twenty_opportunities' => (int)$tw->query("SELECT COUNT(*) FROM $TWENTY_WS.opportunity")->fetchColumn(),
'weval_crm_deals' => (int)$local->query("SELECT COUNT(*) FROM crm.deals")->fetchColumn(),
'weval_crm_companies' => (int)$local->query("SELECT COUNT(*) FROM crm.companies")->fetchColumn(),
'weval_crm_contacts' => (int)$local->query("SELECT COUNT(*) FROM crm.contacts")->fetchColumn(),
'weval_crm_pipeline_weighted' => (float)$local->query("SELECT COALESCE(SUM(value::float * probability::float/100), 0) FROM crm.deals WHERE stage NOT IN ('won','lost')")->fetchColumn()
]
];
// Chat source breakdown (SaaS = turbo_, Widget = web_, Direct = default)
$sources = $local->query("SELECT CASE WHEN session_id LIKE 'web_%' THEN 'Widget' WHEN session_id LIKE 'turbo_%' THEN 'SaaS' WHEN session_id='default' THEN 'Direct' WHEN session_id LIKE 'admin_%' THEN 'Admin' ELSE 'Other' END as src, COUNT(*) as msgs, COUNT(DISTINCT session_id) as sessions FROM chatbot_conversations GROUP BY src ORDER BY msgs DESC")->fetchAll(PDO::FETCH_ASSOC);
$data['chat']['by_source'] = $sources;
// Forms by source
$data['forms']['by_source'] = $local->query("SELECT source, COUNT(*) as c FROM contact_messages GROUP BY source ORDER BY c DESC")->fetchAll(PDO::FETCH_ASSOC);
echo json_encode(['ok' => true, 'data' => $data, 'ts' => date('c')]);
exit;
}
if ($action === 'visitors_rich') {
// site_visits enriched + by country/device/browser/day/hour
$r = [
'recent' => $local->query("SELECT ip, page, country, city, device, browser, referrer, ts FROM site_visits ORDER BY ts DESC LIMIT 50")->fetchAll(PDO::FETCH_ASSOC),
'by_country' => $local->query("SELECT country, COUNT(*) as c FROM site_visits WHERE country IS NOT NULL AND country != '' GROUP BY country ORDER BY c DESC LIMIT 10")->fetchAll(PDO::FETCH_ASSOC),
'by_device' => $local->query("SELECT device, COUNT(*) as c FROM site_visits WHERE device IS NOT NULL AND device != '' GROUP BY device ORDER BY c DESC LIMIT 5")->fetchAll(PDO::FETCH_ASSOC),
'by_browser' => $local->query("SELECT browser, COUNT(*) as c FROM site_visits WHERE browser IS NOT NULL AND browser != '' GROUP BY browser ORDER BY c DESC LIMIT 5")->fetchAll(PDO::FETCH_ASSOC),
'by_page' => $local->query("SELECT LEFT(page, 60) as page, COUNT(*) as c FROM site_visits WHERE page IS NOT NULL GROUP BY LEFT(page, 60) ORDER BY c DESC LIMIT 10")->fetchAll(PDO::FETCH_ASSOC),
'daily_7d' => $local->query("SELECT ts::date::text as day, COUNT(*) as visits, COUNT(DISTINCT ip) as unique_ips FROM site_visits WHERE ts > NOW() - INTERVAL '30 days' GROUP BY ts::date ORDER BY 1 DESC")->fetchAll(PDO::FETCH_ASSOC),
'hourly' => $local->query("SELECT EXTRACT(HOUR FROM ts)::int as hour, COUNT(*) as visits FROM site_visits GROUP BY hour ORDER BY hour")->fetchAll(PDO::FETCH_ASSOC)
];
echo json_encode(['ok' => true, 'visitors' => $r]);
exit;
}
if ($action === 'forms_rich') {
// All forms with full details + cross-link to sessions + leads
$forms = $local->query("SELECT id, name, email, company, subject, LEFT(message, 300) as preview, source, read as is_read, created_at FROM contact_messages ORDER BY created_at DESC LIMIT 100")->fetchAll(PDO::FETCH_ASSOC);
$by_source = $local->query("SELECT source, COUNT(*) as c, COUNT(DISTINCT email) as unique_emails FROM contact_messages GROUP BY source ORDER BY c DESC")->fetchAll(PDO::FETCH_ASSOC);
$by_day = $local->query("SELECT created_at::date::text as day, COUNT(*) as c FROM contact_messages GROUP BY created_at::date ORDER BY 1 DESC LIMIT 30")->fetchAll(PDO::FETCH_ASSOC);
echo json_encode(['ok' => true, 'forms' => $forms, 'by_source' => $by_source, 'by_day' => $by_day]);
exit;
}
if ($action === 'saas_chats') {
// SaaS = session_id LIKE 'turbo_%'
$stats = [
'total_msgs' => (int)$local->query("SELECT COUNT(*) FROM chatbot_conversations WHERE session_id LIKE 'turbo_%'")->fetchColumn(),
'total_sessions' => (int)$local->query("SELECT COUNT(DISTINCT session_id) FROM chatbot_conversations WHERE session_id LIKE 'turbo_%'")->fetchColumn(),
'today_msgs' => (int)$local->query("SELECT COUNT(*) FROM chatbot_conversations WHERE session_id LIKE 'turbo_%' AND created_at::date = CURRENT_DATE")->fetchColumn(),
'active_today' => (int)$local->query("SELECT COUNT(DISTINCT session_id) FROM chatbot_conversations WHERE session_id LIKE 'turbo_%' AND created_at::date = CURRENT_DATE")->fetchColumn()
];
$recent_sessions = $local->query("SELECT session_id, COUNT(*) as msg_count, MIN(created_at) as first_msg, MAX(created_at) as last_msg FROM chatbot_conversations WHERE session_id LIKE 'turbo_%' GROUP BY session_id ORDER BY MAX(created_at) DESC LIMIT 30")->fetchAll(PDO::FETCH_ASSOC);
$recent_msgs = $local->query("SELECT session_id, role, LEFT(message, 200) as preview, created_at FROM chatbot_conversations WHERE session_id LIKE 'turbo_%' ORDER BY created_at DESC LIMIT 30")->fetchAll(PDO::FETCH_ASSOC);
echo json_encode(['ok' => true, 'stats' => $stats, 'recent_sessions' => $recent_sessions, 'recent_msgs' => $recent_msgs]);
exit;
}
if ($action === 'twenty_crm') {
$tw = db_twenty();
$r = [
'total_companies' => (int)$tw->query("SELECT COUNT(*) FROM $TWENTY_WS.company WHERE \"deletedAt\" IS NULL")->fetchColumn(),
'total_people' => (int)$tw->query("SELECT COUNT(*) FROM $TWENTY_WS.person WHERE \"deletedAt\" IS NULL")->fetchColumn(),
'total_opportunities' => (int)$tw->query("SELECT COUNT(*) FROM $TWENTY_WS.opportunity WHERE \"deletedAt\" IS NULL")->fetchColumn(),
'top_companies_by_employees' => $tw->query("SELECT name, \"domainNamePrimaryLinkUrl\" as domain, employees, \"addressAddressCountry\" as country FROM $TWENTY_WS.company WHERE \"deletedAt\" IS NULL AND employees IS NOT NULL ORDER BY employees DESC NULLS LAST LIMIT 10")->fetchAll(PDO::FETCH_ASSOC),
'recent_people' => $tw->query("SELECT \"nameFirstName\" as first_name, \"nameLastName\" as last_name, \"emailsPrimaryEmail\" as email, \"jobTitle\" as job_title, city, \"createdAt\" FROM $TWENTY_WS.person WHERE \"deletedAt\" IS NULL ORDER BY \"createdAt\" DESC LIMIT 10")->fetchAll(PDO::FETCH_ASSOC),
'opportunities' => $tw->query("SELECT name, \"amountAmountMicros\"::bigint/1000000 as amount_eur, stage, \"closeDate\" FROM $TWENTY_WS.opportunity WHERE \"deletedAt\" IS NULL ORDER BY \"createdAt\" DESC LIMIT 20")->fetchAll(PDO::FETCH_ASSOC)
];
echo json_encode(['ok' => true, 'twenty' => $r]);
exit;
}
if ($action === 'crm_triple_lookup') {
$email = strtolower(trim($_GET['email'] ?? ''));
if (!$email) { echo json_encode(['error' => 'email required']); exit; }
$pc = db_paperclip();
$tw = db_twenty();
$res = ['email' => $email, 'found_in' => []];
// Paperclip
$q = $pc->prepare("SELECT * FROM weval_leads WHERE LOWER(email) = ?");
$q->execute([$email]);
$res['paperclip'] = $q->fetch(PDO::FETCH_ASSOC) ?: null;
if ($res['paperclip']) $res['found_in'][] = 'paperclip';
// Twenty person
$q = $tw->prepare("SELECT \"nameFirstName\" as first_name, \"nameLastName\" as last_name, \"emailsPrimaryEmail\" as email, \"jobTitle\" as job_title, city, \"linkedinLinkPrimaryLinkUrl\" as linkedin FROM $TWENTY_WS.person WHERE LOWER(\"emailsPrimaryEmail\") = ? AND \"deletedAt\" IS NULL LIMIT 1");
$q->execute([$email]);
$res['twenty'] = $q->fetch(PDO::FETCH_ASSOC) ?: null;
if ($res['twenty']) $res['found_in'][] = 'twenty';
// Forms
$q = $local->prepare("SELECT id, name, company, subject, LEFT(message, 200) as preview, source, created_at FROM contact_messages WHERE LOWER(email) = ? ORDER BY created_at DESC");
$q->execute([$email]);
$res['forms'] = $q->fetchAll(PDO::FETCH_ASSOC);
// WEVAL CRM deals (by domain/company match)
$domain_part = substr(strrchr($email, "@"), 1);
if ($domain_part) {
$q = $local->prepare("SELECT d.id, d.title, d.stage, d.value::float as value, d.currency, d.probability, d.partner, c.name as company, c.domain FROM crm.deals d LEFT JOIN crm.companies c ON c.id = d.company_id WHERE c.domain ILIKE ? OR LOWER(c.name) ILIKE ?");
$q->execute(['%' . $domain_part . '%', '%' . strtolower(explode('.', $domain_part)[0]) . '%']);
$res['weval_crm'] = $q->fetchAll(PDO::FETCH_ASSOC);
if (count($res['weval_crm']) > 0) $res['found_in'][] = 'weval_crm';
} else {
$res['weval_crm'] = [];
}
if (count($res['forms']) > 0) $res['found_in'][] = 'forms';
$res['status'] = count($res['found_in']) === 0 ? 'unknown' : (count($res['found_in']) === 3 ? 'fully_mapped' : implode('+', $res['found_in']));
echo json_encode(['ok' => true] + $res);
exit;
}
if ($action === 'weval_crm_deals') {
$deals = $local->query("SELECT d.id, d.title, d.stage, d.value::float as value, d.currency, d.probability, d.partner, d.expected_close, d.created_at, COALESCE(c.name, '-') as company_name FROM crm.deals d LEFT JOIN crm.companies c ON c.id = d.company_id ORDER BY d.value::float DESC")->fetchAll(PDO::FETCH_ASSOC);
$stats = $local->query("SELECT stage, COUNT(*) as c, SUM(value::float) as v FROM crm.deals GROUP BY stage ORDER BY 3 DESC")->fetchAll(PDO::FETCH_ASSOC);
$companies_count = (int)$local->query("SELECT COUNT(*) FROM crm.companies")->fetchColumn();
$contacts_count = (int)$local->query("SELECT COUNT(*) FROM crm.contacts")->fetchColumn();
$won = (int)$local->query("SELECT COUNT(*) FROM crm.deals WHERE stage='won'")->fetchColumn();
// Weighted pipeline (value * probability/100)
$weighted = (float)$local->query("SELECT COALESCE(SUM(value::float * probability::float/100), 0) FROM crm.deals WHERE stage NOT IN ('won','lost')")->fetchColumn();
echo json_encode(['ok'=>true, 'deals'=>$deals, 'stats_by_stage'=>$stats, 'companies_count'=>$companies_count, 'contacts_count'=>$contacts_count, 'won'=>$won, 'pipeline_weighted'=>$weighted]);
exit;
}
echo json_encode(['error' => 'unknown', 'actions' => ['global_stats','visitors_rich','forms_rich','saas_chats','twenty_crm','weval_crm_deals','crm_triple_lookup']]);
} catch (Exception $e) {
http_response_code(500);
echo json_encode(['error' => $e->getMessage()]);
}