364 lines
29 KiB
PHP
Executable File
364 lines
29 KiB
PHP
Executable File
<?php
|
|
error_reporting(E_ALL);
|
|
ini_set('display_errors', 0);
|
|
ini_set('memory_limit', '1G');
|
|
ini_set('max_execution_time', 3600);
|
|
|
|
function getDB() {
|
|
static $pdo = null;
|
|
if ($pdo === null) {
|
|
$pdo = new PDO("pgsql:host=localhost;dbname=adx_system", "admin", "admin123",
|
|
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);
|
|
}
|
|
return $pdo;
|
|
}
|
|
|
|
$DATA_TYPES = [
|
|
'leads' => ['name' => 'Leads', 'icon' => 'fa-users', 'color' => '#6366f1', 'table' => 'admin.import_leads', 'unique' => 'email', 'desc' => 'Contacts pour prospection', 'fields' => ['email','first_name','last_name','company','phone','title','country','linkedin','industry']],
|
|
'send_data' => ['name' => 'Send Data', 'icon' => 'fa-paper-plane', 'color' => '#3b82f6', 'table' => 'admin.send_data', 'unique' => null, 'desc' => 'Data pour envoi campagne', 'fields' => ['email','first_name','last_name','company','phone','title','country','custom1','custom2','custom3']],
|
|
'inbox' => ['name' => 'Inbox', 'icon' => 'fa-inbox', 'color' => '#8b5cf6', 'table' => 'admin.inbox_accounts', 'unique' => 'email', 'desc' => 'Comptes email IMAP/SMTP', 'fields' => ['email','password','imap_host','imap_port','smtp_host','smtp_port','provider','proxy']],
|
|
'seed_inbox' => ['name' => 'Seed Inbox', 'icon' => 'fa-seedling', 'color' => '#10b981', 'table' => 'admin.seed_inboxes', 'unique' => 'email', 'desc' => 'Comptes pour seeding/warmup', 'fields' => ['email','password','imap_host','imap_port','smtp_host','smtp_port','provider']],
|
|
'o365' => ['name' => 'Office 365', 'icon' => 'fa-microsoft', 'color' => '#0078d4', 'table' => 'admin.office_accounts', 'unique' => 'email', 'desc' => 'Comptes Microsoft 365', 'fields' => ['email','password','status']],
|
|
'smtp' => ['name' => 'SMTP', 'icon' => 'fa-server', 'color' => '#f59e0b', 'table' => 'admin.smtp_servers', 'unique' => 'host', 'desc' => 'Serveurs envoi email', 'fields' => ['host','port','username','password','encryption','from_email','daily_limit']],
|
|
'domains' => ['name' => 'Domains', 'icon' => 'fa-globe', 'color' => '#ec4899', 'table' => 'admin.monitored_domains', 'unique' => 'domain', 'desc' => 'Domaines à monitorer', 'fields' => ['domain','provider','status']],
|
|
'warmup' => ['name' => 'Warmup', 'icon' => 'fa-fire', 'color' => '#ef4444', 'table' => 'admin.warmup_accounts', 'unique' => 'email', 'desc' => 'Comptes en réchauffement', 'fields' => ['email','password','smtp_host','smtp_port','daily_limit']]
|
|
];
|
|
|
|
$SOURCES = ['manual','linkedin','hunter','apollo','zoominfo','snov','dropcontact','lusha','scraping','api','csv_upload','crm_export','webform','other'];
|
|
|
|
function ensureTables() {
|
|
$pdo = getDB();
|
|
$pdo->exec("CREATE TABLE IF NOT EXISTS admin.import_jobs (id SERIAL PRIMARY KEY, data_type VARCHAR(50), filename VARCHAR(255), source VARCHAR(100) DEFAULT 'csv_upload', tags TEXT, total_rows INTEGER DEFAULT 0, processed_rows INTEGER DEFAULT 0, imported_rows INTEGER DEFAULT 0, duplicate_rows INTEGER DEFAULT 0, error_rows INTEGER DEFAULT 0, status VARCHAR(20) DEFAULT 'pending', speed_rows_sec DECIMAL(10,2) DEFAULT 0, started_at TIMESTAMP, completed_at TIMESTAMP, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP)");
|
|
$pdo->exec("CREATE TABLE IF NOT EXISTS admin.import_leads (id SERIAL PRIMARY KEY, job_id INTEGER, email VARCHAR(255) NOT NULL, first_name VARCHAR(100), last_name VARCHAR(100), company VARCHAR(255), phone VARCHAR(50), title VARCHAR(255), country VARCHAR(100), linkedin VARCHAR(500), industry VARCHAR(100), source VARCHAR(100), tags TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP)");
|
|
$pdo->exec("CREATE UNIQUE INDEX IF NOT EXISTS idx_leads_email ON admin.import_leads(email)");
|
|
$pdo->exec("CREATE TABLE IF NOT EXISTS admin.send_data (id SERIAL PRIMARY KEY, job_id INTEGER, email VARCHAR(255) NOT NULL, first_name VARCHAR(100), last_name VARCHAR(100), company VARCHAR(255), phone VARCHAR(50), title VARCHAR(255), country VARCHAR(100), custom1 VARCHAR(500), custom2 VARCHAR(500), custom3 VARCHAR(500), source VARCHAR(100), tags TEXT, status VARCHAR(50) DEFAULT 'pending', created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP)");
|
|
$pdo->exec("CREATE INDEX IF NOT EXISTS idx_send_email ON admin.send_data(email)");
|
|
$pdo->exec("CREATE TABLE IF NOT EXISTS admin.inbox_accounts (id SERIAL PRIMARY KEY, job_id INTEGER, email VARCHAR(255) NOT NULL, password VARCHAR(255), imap_host VARCHAR(255), imap_port INTEGER DEFAULT 993, smtp_host VARCHAR(255), smtp_port INTEGER DEFAULT 587, provider VARCHAR(100), proxy VARCHAR(255), status VARCHAR(50) DEFAULT 'active', source VARCHAR(100), tags TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP)");
|
|
$pdo->exec("CREATE UNIQUE INDEX IF NOT EXISTS idx_inbox_email ON admin.inbox_accounts(email)");
|
|
$pdo->exec("CREATE TABLE IF NOT EXISTS admin.seed_inboxes (id SERIAL PRIMARY KEY, job_id INTEGER, email VARCHAR(255) NOT NULL, password VARCHAR(255), imap_host VARCHAR(255), imap_port INTEGER DEFAULT 993, smtp_host VARCHAR(255), smtp_port INTEGER DEFAULT 587, provider VARCHAR(100), status VARCHAR(50) DEFAULT 'active', source VARCHAR(100), tags TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP)");
|
|
$pdo->exec("CREATE UNIQUE INDEX IF NOT EXISTS idx_seed_email ON admin.seed_inboxes(email)");
|
|
}
|
|
|
|
if (isset($_GET['action']) || isset($_POST['action'])) {
|
|
header('Content-Type: application/json');
|
|
ensureTables();
|
|
$action = $_GET['action'] ?? $_POST['action'];
|
|
$pdo = getDB();
|
|
|
|
try {
|
|
switch ($action) {
|
|
case 'stats':
|
|
echo json_encode(['success' => true, 'stats' => [
|
|
'jobs' => $pdo->query("SELECT COUNT(*) FROM admin.import_jobs")->fetchColumn(),
|
|
'leads' => $pdo->query("SELECT COUNT(*) FROM admin.import_leads")->fetchColumn(),
|
|
'send_data' => $pdo->query("SELECT COUNT(*) FROM admin.send_data")->fetchColumn(),
|
|
'inbox' => $pdo->query("SELECT COUNT(*) FROM admin.inbox_accounts")->fetchColumn(),
|
|
'seeds' => $pdo->query("SELECT COUNT(*) FROM admin.seed_inboxes")->fetchColumn(),
|
|
'today' => $pdo->query("SELECT COUNT(*) FROM admin.import_jobs WHERE created_at > CURRENT_DATE")->fetchColumn(),
|
|
]]);
|
|
break;
|
|
|
|
case 'upload':
|
|
if (!isset($_FILES['file'])) { echo json_encode(['success' => false, 'error' => 'No file']); break; }
|
|
$dataType = $_POST['data_type'] ?? 'leads';
|
|
$source = $_POST['source'] ?? 'csv_upload';
|
|
$tags = $_POST['tags'] ?? '';
|
|
$hasHeader = ($_POST['has_header'] ?? '1') === '1';
|
|
|
|
$uploadDir = '/tmp/imports/';
|
|
@mkdir($uploadDir, 0777, true);
|
|
$origName = $_FILES['file']['name'];
|
|
$ext = strtolower(pathinfo($origName, PATHINFO_EXTENSION));
|
|
$filename = uniqid('imp_') . '.' . $ext;
|
|
$filepath = $uploadDir . $filename;
|
|
move_uploaded_file($_FILES['file']['tmp_name'], $filepath);
|
|
|
|
if (in_array($ext, ['xlsx', 'xls'])) {
|
|
$csvFile = $filepath . '.csv';
|
|
exec("python3 -c \"import pandas as pd; pd.read_excel('$filepath').to_csv('$csvFile', index=False)\" 2>/dev/null || ssconvert '$filepath' '$csvFile' 2>/dev/null");
|
|
if (file_exists($csvFile)) { unlink($filepath); $filepath = $csvFile; $filename .= '.csv'; }
|
|
}
|
|
|
|
$lineCount = 0;
|
|
$handle = fopen($filepath, 'r');
|
|
$sample = fread($handle, 8192);
|
|
rewind($handle);
|
|
while (!feof($handle)) { if (trim(fgets($handle)) !== '') $lineCount++; }
|
|
fclose($handle);
|
|
if ($hasHeader) $lineCount = max(0, $lineCount - 1);
|
|
|
|
$delims = [';' => 0, ',' => 0, "\t" => 0, '|' => 0];
|
|
foreach ($delims as $d => &$c) $c = substr_count($sample, $d);
|
|
arsort($delims);
|
|
$delimiter = key($delims);
|
|
|
|
$stmt = $pdo->prepare("INSERT INTO admin.import_jobs (data_type, filename, source, tags, total_rows, status) VALUES (?, ?, ?, ?, ?, 'ready') RETURNING id");
|
|
$stmt->execute([$dataType, $filename, $source, $tags, $lineCount]);
|
|
$jobId = $stmt->fetchColumn();
|
|
|
|
$preview = ['headers' => null, 'rows' => []];
|
|
$handle = fopen($filepath, 'r');
|
|
$row = 0;
|
|
while (($line = fgets($handle)) !== false && $row < 6) {
|
|
$line = trim($line);
|
|
if ($line === '') continue;
|
|
$data = str_getcsv($line, $delimiter);
|
|
if ($row === 0 && $hasHeader) $preview['headers'] = $data;
|
|
else $preview['rows'][] = $data;
|
|
$row++;
|
|
}
|
|
fclose($handle);
|
|
|
|
echo json_encode(['success' => true, 'job_id' => $jobId, 'filename' => $filename, 'total_rows' => $lineCount, 'delimiter' => $delimiter, 'preview' => $preview]);
|
|
break;
|
|
|
|
case 'process':
|
|
$jobId = intval($_POST['job_id'] ?? 0);
|
|
$delimiter = $_POST['delimiter'] ?? ';';
|
|
$hasHeader = ($_POST['has_header'] ?? '1') === '1';
|
|
$columnMap = json_decode($_POST['column_map'] ?? '{}', true);
|
|
$dedupe = ($_POST['dedupe'] ?? '1') === '1';
|
|
$chunkSize = max(500, min(50000, intval($_POST['chunk_size'] ?? 5000)));
|
|
|
|
$stmt = $pdo->prepare("SELECT * FROM admin.import_jobs WHERE id = ?");
|
|
$stmt->execute([$jobId]);
|
|
$job = $stmt->fetch(PDO::FETCH_ASSOC);
|
|
if (!$job) { echo json_encode(['success' => false, 'error' => 'Job not found']); break; }
|
|
|
|
$filepath = '/tmp/imports/' . $job['filename'];
|
|
if (!file_exists($filepath)) { echo json_encode(['success' => false, 'error' => 'File not found']); break; }
|
|
|
|
global $DATA_TYPES;
|
|
$typeConfig = $DATA_TYPES[$job['data_type']] ?? $DATA_TYPES['leads'];
|
|
$table = $typeConfig['table'];
|
|
$uniqueField = $typeConfig['unique'];
|
|
|
|
$pdo->exec("UPDATE admin.import_jobs SET status='processing', started_at=NOW() WHERE id=$jobId");
|
|
|
|
$handle = fopen($filepath, 'r');
|
|
if ($hasHeader) fgets($handle);
|
|
|
|
$rowNum = $imported = $duplicates = $errors = 0;
|
|
$startTime = microtime(true);
|
|
$batch = [];
|
|
$seenKeys = [];
|
|
|
|
while (($line = fgets($handle)) !== false) {
|
|
$line = trim($line);
|
|
if ($line === '') continue;
|
|
$rowNum++;
|
|
$data = str_getcsv($line, $delimiter);
|
|
|
|
$mapped = ['job_id' => $jobId, 'source' => $job['source'], 'tags' => $job['tags']];
|
|
foreach ($columnMap as $idx => $field) {
|
|
if ($field && $field !== 'skip') $mapped[$field] = trim($data[$idx] ?? '');
|
|
}
|
|
|
|
$key = $mapped[$uniqueField ?? 'email'] ?? $mapped['email'] ?? '';
|
|
if (!$key && $uniqueField) { $errors++; continue; }
|
|
if ($dedupe && $uniqueField && isset($seenKeys[$key])) { $duplicates++; continue; }
|
|
if ($uniqueField) $seenKeys[$key] = true;
|
|
|
|
$batch[] = $mapped;
|
|
|
|
if (count($batch) >= $chunkSize) {
|
|
$imported += bulkInsert($pdo, $table, $batch, $uniqueField);
|
|
$batch = [];
|
|
}
|
|
}
|
|
|
|
if (count($batch) > 0) $imported += bulkInsert($pdo, $table, $batch, $uniqueField);
|
|
fclose($handle);
|
|
@unlink($filepath);
|
|
|
|
$elapsed = microtime(true) - $startTime;
|
|
$speed = $rowNum / max($elapsed, 0.001);
|
|
|
|
$pdo->exec("UPDATE admin.import_jobs SET processed_rows=$rowNum, imported_rows=$imported, duplicate_rows=$duplicates, error_rows=$errors, speed_rows_sec=".round($speed,2).", status='completed', completed_at=NOW() WHERE id=$jobId");
|
|
|
|
echo json_encode(['success' => true, 'processed' => $rowNum, 'imported' => $imported, 'duplicates' => $duplicates, 'errors' => $errors, 'speed' => round($speed, 2), 'elapsed' => round($elapsed, 2)]);
|
|
break;
|
|
|
|
case 'history':
|
|
$jobs = $pdo->query("SELECT * FROM admin.import_jobs ORDER BY created_at DESC LIMIT 100")->fetchAll(PDO::FETCH_ASSOC);
|
|
echo json_encode(['success' => true, 'jobs' => $jobs]);
|
|
break;
|
|
|
|
default:
|
|
echo json_encode(['success' => false, 'error' => 'Unknown action']);
|
|
}
|
|
} catch (Exception $e) {
|
|
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
|
|
}
|
|
exit;
|
|
}
|
|
|
|
function bulkInsert($pdo, $table, $batch, $uniqueField) {
|
|
if (empty($batch)) return 0;
|
|
$columns = array_keys($batch[0]);
|
|
$values = [];
|
|
$placeholders = [];
|
|
foreach ($batch as $row) {
|
|
$ph = [];
|
|
foreach ($columns as $col) { $values[] = $row[$col] ?? null; $ph[] = '?'; }
|
|
$placeholders[] = '(' . implode(',', $ph) . ')';
|
|
}
|
|
$sql = "INSERT INTO $table (" . implode(',', $columns) . ") VALUES " . implode(',', $placeholders);
|
|
if ($uniqueField) $sql .= " ON CONFLICT ($uniqueField) DO NOTHING";
|
|
try {
|
|
$stmt = $pdo->prepare($sql);
|
|
$stmt->execute($values);
|
|
return $stmt->rowCount();
|
|
} catch (Exception $e) { return 0; }
|
|
}
|
|
|
|
ensureTables();
|
|
?>
|
|
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Import System v3</title>
|
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
|
<style>
|
|
:root{--bg:#0a0a0f;--bg2:#12121a;--bg3:#1a1a25;--primary:#6366f1;--success:#10b981;--warning:#f59e0b;--danger:#ef4444;--text:#e2e8f0;--text2:#94a3b8;--border:#2a2a3a}
|
|
*{margin:0;padding:0;box-sizing:border-box}body{font-family:'Inter',sans-serif;background:var(--bg);color:var(--text);min-height:100vh}
|
|
.header{background:linear-gradient(135deg,var(--bg2),var(--bg3));border-bottom:1px solid var(--border);padding:1rem 2rem;display:flex;justify-content:space-between;align-items:center}
|
|
.header h1{font-size:1.3rem;display:flex;align-items:center;gap:.5rem}.header h1 i{color:var(--primary)}
|
|
.badge{background:linear-gradient(135deg,var(--primary),#8b5cf6);padding:.25rem .75rem;border-radius:15px;font-size:.7rem;font-weight:700}
|
|
.container{max-width:1400px;margin:0 auto;padding:1.5rem}
|
|
.stats{display:grid;grid-template-columns:repeat(6,1fr);gap:.75rem;margin-bottom:1.5rem}
|
|
.stat{background:var(--bg2);border:1px solid var(--border);border-radius:8px;padding:.75rem;text-align:center}
|
|
.stat .v{font-size:1.4rem;font-weight:700;color:var(--primary)}.stat .l{color:var(--text2);font-size:.7rem}
|
|
.types{display:grid;grid-template-columns:repeat(4,1fr);gap:.75rem;margin-bottom:1.5rem}
|
|
.type{background:var(--bg2);border:2px solid var(--border);border-radius:10px;padding:1rem;text-align:center;cursor:pointer;transition:all .2s}
|
|
.type:hover{border-color:var(--primary);transform:translateY(-2px)}
|
|
.type.sel{border-color:var(--primary);background:rgba(99,102,241,.1)}
|
|
.type i{font-size:1.5rem;margin-bottom:.5rem;display:block}
|
|
.type h3{font-size:.8rem;font-weight:600;margin-bottom:.25rem}
|
|
.type p{font-size:.65rem;color:var(--text2);line-height:1.3}
|
|
.meta{display:grid;grid-template-columns:repeat(4,1fr);gap:.75rem;margin-bottom:1.5rem}
|
|
.meta-item{background:var(--bg2);border:1px solid var(--border);border-radius:8px;padding:.75rem}
|
|
.meta-item label{display:block;margin-bottom:.25rem;color:var(--text2);font-size:.7rem}
|
|
.meta-item select,.meta-item input{width:100%;padding:.5rem;background:var(--bg3);border:1px solid var(--border);border-radius:5px;color:var(--text);font-size:.8rem}
|
|
.upload{background:var(--bg2);border:3px dashed var(--border);border-radius:12px;padding:2rem;text-align:center;cursor:pointer;transition:all .2s;margin-bottom:1.5rem}
|
|
.upload:hover,.upload.drag{border-color:var(--primary);background:rgba(99,102,241,.05)}
|
|
.upload i{font-size:2rem;color:var(--primary);margin-bottom:.5rem}.upload input{display:none}
|
|
.upload h2{font-size:1rem;margin-bottom:.25rem}.upload p{color:var(--text2);font-size:.8rem}
|
|
.formats{margin-top:.5rem;display:flex;justify-content:center;gap:.5rem}
|
|
.formats span{background:var(--bg3);padding:.2rem .5rem;border-radius:4px;font-size:.65rem;color:var(--text2)}
|
|
.section{background:var(--bg2);border:1px solid var(--border);border-radius:8px;padding:1rem;margin-bottom:1rem;display:none}
|
|
.section.active{display:block}.section h3{margin-bottom:.75rem;font-size:.9rem;display:flex;align-items:center;gap:.4rem}
|
|
.map-row{display:grid;grid-template-columns:1fr 25px 1fr;gap:.4rem;align-items:center;margin-bottom:.4rem;padding:.4rem .6rem;background:var(--bg3);border-radius:5px}
|
|
.map-row .src{font-family:monospace;font-size:.75rem;color:var(--text2);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
.map-row .arr{color:var(--primary);text-align:center;font-size:.7rem}
|
|
.map-row select{padding:.4rem;background:var(--bg);border:1px solid var(--border);border-radius:4px;color:var(--text);font-size:.75rem}
|
|
.opts{display:flex;gap:1rem;margin:.75rem 0;flex-wrap:wrap;align-items:center}
|
|
.chk{display:flex;align-items:center;gap:.3rem;cursor:pointer;font-size:.8rem}
|
|
.chk input{width:14px;height:14px;accent-color:var(--primary)}
|
|
.btn{padding:.5rem 1rem;border:none;border-radius:6px;font-weight:600;cursor:pointer;transition:all .2s;display:inline-flex;align-items:center;gap:.3rem;font-size:.85rem}
|
|
.btn:hover{transform:translateY(-1px)}
|
|
.btn-go{background:linear-gradient(135deg,var(--success),#059669);color:white;padding:.6rem 1.5rem;font-size:.9rem}
|
|
.prog-bar{height:20px;background:var(--bg3);border-radius:10px;overflow:hidden;margin:.75rem 0}
|
|
.prog-fill{height:100%;background:linear-gradient(90deg,var(--primary),var(--success));border-radius:10px;transition:width .3s;display:flex;align-items:center;justify-content:center;font-weight:600;font-size:.75rem;color:white}
|
|
.prog-stats{display:grid;grid-template-columns:repeat(6,1fr);gap:.5rem}
|
|
.prog-stat{background:var(--bg3);padding:.5rem;border-radius:5px;text-align:center}
|
|
.prog-stat .v{font-size:1.1rem;font-weight:700}.prog-stat .l{font-size:.6rem;color:var(--text2)}
|
|
.prog-stat.ok .v{color:var(--success)}.prog-stat.warn .v{color:var(--warning)}.prog-stat.err .v{color:var(--danger)}
|
|
.preview table{width:100%;border-collapse:collapse;font-size:.75rem}
|
|
.preview th,.preview td{padding:.4rem;border:1px solid var(--border);text-align:left}
|
|
.preview th{background:var(--bg3)}
|
|
.info{margin-top:.5rem;padding:.5rem;background:var(--bg3);border-radius:5px;display:flex;gap:1rem;font-size:.75rem}
|
|
.info span{color:var(--text2)}.info b{color:var(--text)}
|
|
.hist{width:100%;border-collapse:collapse;font-size:.75rem}
|
|
.hist th,.hist td{padding:.5rem;text-align:left;border-bottom:1px solid var(--border)}
|
|
.hist th{color:var(--text2);font-size:.65rem}
|
|
.badge-s{padding:.15rem .4rem;border-radius:10px;font-size:.6rem;font-weight:600}
|
|
.badge-completed{background:rgba(16,185,129,.15);color:var(--success)}
|
|
.badge-processing{background:rgba(99,102,241,.15);color:var(--primary)}
|
|
.badge-ready{background:rgba(245,158,11,.15);color:var(--warning)}
|
|
.src-badge{background:var(--bg3);padding:.15rem .4rem;border-radius:3px;font-size:.6rem;color:var(--text2)}
|
|
.tag{background:rgba(99,102,241,.2);color:var(--primary);padding:.1rem .3rem;border-radius:2px;font-size:.55rem;margin-right:.15rem}
|
|
@media(max-width:1000px){.types{grid-template-columns:repeat(2,1fr)}.meta{grid-template-columns:repeat(2,1fr)}.stats{grid-template-columns:repeat(3,1fr)}}
|
|
</style>
|
|
|
|
</head>
|
|
<body>
|
|
<div class="header"><h1><i class="fas fa-bolt"></i> Import System</h1><span class="badge">v3 ULTRA</span></div>
|
|
<div class="container">
|
|
<div class="stats">
|
|
<div class="stat"><div class="v" id="sJ">0</div><div class="l">Imports</div></div>
|
|
<div class="stat"><div class="v" id="sL">0</div><div class="l">Leads</div></div>
|
|
<div class="stat"><div class="v" id="sD">0</div><div class="l">Send Data</div></div>
|
|
<div class="stat"><div class="v" id="sI">0</div><div class="l">Inbox</div></div>
|
|
<div class="stat"><div class="v" id="sS">0</div><div class="l">Seeds</div></div>
|
|
<div class="stat"><div class="v" id="sT">0</div><div class="l">Today</div></div>
|
|
</div>
|
|
<div class="types">
|
|
<?php foreach ($DATA_TYPES as $k => $t): ?>
|
|
<div class="type <?=$k==='leads'?'sel':''?>" data-t="<?=$k?>">
|
|
<i class="fas <?=$t['icon']?>" style="color:<?=$t['color']?>"></i>
|
|
<h3><?=$t['name']?></h3>
|
|
<p><?=$t['desc']?></p>
|
|
</div>
|
|
<?php endforeach; ?>
|
|
</div>
|
|
<div class="meta">
|
|
<div class="meta-item"><label><i class="fas fa-database"></i> Source</label><select id="src"><?php foreach($SOURCES as $s):?><option value="<?=$s?>"><?=ucfirst(str_replace('_',' ',$s))?></option><?php endforeach;?></select></div>
|
|
<div class="meta-item"><label><i class="fas fa-tags"></i> Tags</label><input type="text" id="tags" placeholder="tag1, tag2"></div>
|
|
<div class="meta-item"><label><i class="fas fa-layer-group"></i> Chunk</label><select id="chunk"><option value="1000">1K</option><option value="5000" selected>5K</option><option value="10000">10K</option><option value="25000">25K</option></select></div>
|
|
<div class="meta-item"><label><i class="fas fa-grip-lines"></i> Delimiter</label><select id="delim"><option value="auto">Auto</option><option value=";">;</option><option value=",">,</option><option value="|">|</option></select></div>
|
|
</div>
|
|
<div class="upload" id="zone">
|
|
<i class="fas fa-cloud-upload-alt"></i><h2>Drop file or click</h2><p>CSV, Excel, TXT</p>
|
|
<input type="file" id="file" accept=".csv,.txt,.tsv,.xlsx,.xls">
|
|
<div class="formats"><span>CSV</span><span>XLSX</span><span>TXT</span></div>
|
|
</div>
|
|
<div class="section" id="prevSec"><h3><i class="fas fa-eye"></i> Preview</h3><div class="preview" id="prev"></div><div class="info" id="info"></div></div>
|
|
<div class="section" id="mapSec">
|
|
<h3><i class="fas fa-columns"></i> Mapping</h3><div id="mapGrid"></div>
|
|
<div class="opts"><label class="chk"><input type="checkbox" id="hdr" checked> Headers</label><label class="chk"><input type="checkbox" id="ded" checked> Dedupe</label></div>
|
|
<button class="btn btn-go" id="go"><i class="fas fa-rocket"></i> START IMPORT</button>
|
|
</div>
|
|
<div class="section" id="progSec">
|
|
<h3><i class="fas fa-spinner fa-spin"></i> Processing</h3>
|
|
<div class="prog-bar"><div class="prog-fill" id="pFill">0%</div></div>
|
|
<div class="prog-stats">
|
|
<div class="prog-stat"><div class="v" id="pT">0</div><div class="l">Total</div></div>
|
|
<div class="prog-stat"><div class="v" id="pP">0</div><div class="l">Done</div></div>
|
|
<div class="prog-stat ok"><div class="v" id="pI">0</div><div class="l">Imported</div></div>
|
|
<div class="prog-stat warn"><div class="v" id="pD">0</div><div class="l">Duplicates</div></div>
|
|
<div class="prog-stat err"><div class="v" id="pE">0</div><div class="l">Errors</div></div>
|
|
<div class="prog-stat"><div class="v" id="pS">0</div><div class="l">rows/sec</div></div>
|
|
</div>
|
|
</div>
|
|
<div class="section active" id="histSec">
|
|
<h3><i class="fas fa-history"></i> Import History</h3>
|
|
<table class="hist"><thead><tr><th>ID</th><th>Type</th><th>Source</th><th>Tags</th><th>Total</th><th>Imported</th><th>Speed</th><th>Status</th><th>Date</th></tr></thead><tbody id="histBody"></tbody></table>
|
|
</div>
|
|
</div>
|
|
<script>
|
|
const T=<?=json_encode($DATA_TYPES)?>;let sel='leads',job=null,pv=null;
|
|
document.querySelectorAll('.type').forEach(c=>{c.onclick=()=>{document.querySelectorAll('.type').forEach(x=>x.classList.remove('sel'));c.classList.add('sel');sel=c.dataset.t;if(pv)map();}});
|
|
const z=document.getElementById('zone'),f=document.getElementById('file');
|
|
z.onclick=()=>f.click();z.ondragover=e=>{e.preventDefault();z.classList.add('drag')};z.ondragleave=()=>z.classList.remove('drag');
|
|
z.ondrop=e=>{e.preventDefault();z.classList.remove('drag');if(e.dataTransfer.files[0])up(e.dataTransfer.files[0])};
|
|
f.onchange=e=>{if(e.target.files[0])up(e.target.files[0])};
|
|
function up(file){const fd=new FormData();fd.append('file',file);fd.append('data_type',sel);fd.append('source',document.getElementById('src').value);fd.append('tags',document.getElementById('tags').value);fd.append('has_header',document.getElementById('hdr').checked?'1':'0');
|
|
fetch('?action=upload',{method:'POST',body:fd}).then(r=>r.json()).then(d=>{if(d.success){job=d;pv=d.preview;prev(d);map();show('prevSec');show('mapSec');document.getElementById('pT').textContent=fmt(d.total_rows);}else alert(d.error);});}
|
|
function prev(d){const c=d.preview.headers||d.preview.rows[0]?.map((_,i)=>'Col '+i)||[];let h='<table><tr>'+c.map((x,i)=>`<th>${i}:${esc(x)}</th>`).join('')+'</tr>';(d.preview.rows||[]).forEach(r=>{h+='<tr>'+r.map(x=>`<td>${esc(x)}</td>`).join('')+'</tr>'});h+='</table>';document.getElementById('prev').innerHTML=h;document.getElementById('info').innerHTML=`<span>File:<b>${d.filename}</b></span><span>Rows:<b>${fmt(d.total_rows)}</b></span><span>Delim:<b>${d.delimiter}</b></span>`;}
|
|
function map(){const flds=T[sel]?.fields||[];const cols=pv?.headers||pv?.rows[0]?.map((_,i)=>'Col '+i)||[];let h='';cols.forEach((c,i)=>{const g=guess(c,flds);h+=`<div class="map-row"><div class="src">${esc(c)}</div><div class="arr"><i class="fas fa-arrow-right"></i></div><select data-c="${i}"><option value="skip">--</option>${flds.map(f=>`<option value="${f}"${f===g?' selected':''}>${f}</option>`).join('')}</select></div>`;});document.getElementById('mapGrid').innerHTML=h;}
|
|
function guess(c,f){c=c.toLowerCase();for(const x of f){if(c.includes(x.replace('_','')))return x;if(x==='email'&&c.includes('mail'))return x;if(x==='first_name'&&(c.includes('first')||c.includes('fname')||c.includes('prenom')))return x;if(x==='last_name'&&(c.includes('last')||c.includes('lname')||c.includes('nom')))return x;if(x==='company'&&c.includes('company'))return x;if(x==='password'&&c.includes('pass'))return x;if(x==='imap_host'&&c.includes('imap'))return x;if(x==='smtp_host'&&c.includes('smtp'))return x;}return'skip';}
|
|
document.getElementById('go').onclick=()=>{if(!job)return;const m={};document.querySelectorAll('#mapGrid select').forEach(s=>m[s.dataset.c]=s.value);const fd=new FormData();fd.append('action','process');fd.append('job_id',job.job_id);fd.append('delimiter',document.getElementById('delim').value==='auto'?job.delimiter:document.getElementById('delim').value);fd.append('has_header',document.getElementById('hdr').checked?'1':'0');fd.append('column_map',JSON.stringify(m));fd.append('dedupe',document.getElementById('ded').checked?'1':'0');fd.append('chunk_size',document.getElementById('chunk').value);hide('mapSec');show('progSec');
|
|
fetch('',{method:'POST',body:fd}).then(r=>r.json()).then(d=>{if(d.success){document.getElementById('pFill').style.width='100%';document.getElementById('pFill').textContent='100%';document.getElementById('pP').textContent=fmt(d.processed);document.getElementById('pI').textContent=fmt(d.imported);document.getElementById('pD').textContent=fmt(d.duplicates);document.getElementById('pE').textContent=fmt(d.errors);document.getElementById('pS').textContent=fmt(d.speed);setTimeout(()=>{alert('✅ Done! '+fmt(d.imported)+' imported in '+d.elapsed+'s');hist();stats();reset();},400);}else alert(d.error);});};
|
|
function show(i){document.getElementById(i).classList.add('active')}function hide(i){document.getElementById(i).classList.remove('active')}function reset(){hide('progSec');hide('prevSec');hide('mapSec');job=null;pv=null;}
|
|
function stats(){fetch('?action=stats').then(r=>r.json()).then(d=>{if(d.success){document.getElementById('sJ').textContent=fmt(d.stats.jobs);document.getElementById('sL').textContent=fmt(d.stats.leads);document.getElementById('sD').textContent=fmt(d.stats.send_data);document.getElementById('sI').textContent=fmt(d.stats.inbox);document.getElementById('sS').textContent=fmt(d.stats.seeds);document.getElementById('sT').textContent=fmt(d.stats.today);}});}
|
|
function hist(){fetch('?action=history').then(r=>r.json()).then(d=>{if(d.success){document.getElementById('histBody').innerHTML=d.jobs.map(j=>`<tr><td>#${j.id}</td><td>${j.data_type}</td><td><span class="src-badge">${j.source||'-'}</span></td><td>${(j.tags||'').split(',').filter(t=>t).map(t=>`<span class="tag">${t.trim()}</span>`).join('')||'-'}</td><td>${fmt(j.total_rows)}</td><td style="color:var(--success)">${fmt(j.imported_rows)}</td><td>${fmt(j.speed_rows_sec)}/s</td><td><span class="badge-s badge-${j.status}">${j.status}</span></td><td>${new Date(j.created_at).toLocaleDateString()}</td></tr>`).join('');}});}
|
|
function fmt(n){return Number(n||0).toLocaleString()}function esc(t){if(!t)return'';const d=document.createElement('div');d.textContent=t;return d.innerHTML}
|
|
stats();hist();
|
|
</script>
|
|
|
|
</body>
|
|
</html>
|