cambios importantes
This commit is contained in:
@@ -1604,6 +1604,7 @@ HTML;
|
||||
<a href="/admin/pending" class="nav-link">⏳ Pendientes</a>
|
||||
<a href="/admin/chat" class="nav-link active">💬 Chat</a>
|
||||
<a href="/admin/companies" class="nav-link">🏢 Empresas</a>
|
||||
<a href="/admin/bot-config" class="nav-link">🤖 Bot Config</a>
|
||||
<a href="/admin/settings" class="nav-link">⚙️ Configuración</a>
|
||||
<a href="/admin/sync-companies" class="nav-link" onclick="return confirm('¿Sincronizar empresas desde el ERP?')">🔄 Sincronizar</a>
|
||||
<a href="/admin/process-queue" class="nav-link" onclick="return confirm('¿Procesar cola de mensajes?')">📨 Procesar Cola</a>
|
||||
@@ -1914,4 +1915,463 @@ HTML;
|
||||
|
||||
jsonResponse(200, ['status' => 'sent']);
|
||||
}
|
||||
|
||||
// ─── GET /admin/bot-config ──────────────────────────────────────────────
|
||||
|
||||
public static function botConfig(): void
|
||||
{
|
||||
SessionAuth::require();
|
||||
$user = SessionAuth::user();
|
||||
$userName = self::h($user['name'] ?? 'Admin');
|
||||
$companyId = (int)($_GET['id'] ?? 0);
|
||||
$companies = CompanyRepository::findAll();
|
||||
$company = null;
|
||||
$config = ['commands' => [], 'menus' => [], 'flows' => [], 'ai_prompt' => '', 'fallback' => '', 'greeting' => ''];
|
||||
$msg = $_GET['msg'] ?? '';
|
||||
$toastHtml = $msg === 'saved' ? '<div class="toast toast-success">Configuración guardada exitosamente.</div>' : '';
|
||||
|
||||
if ($companyId > 0) {
|
||||
$company = CompanyRepository::findById($companyId);
|
||||
if ($company) {
|
||||
$cj = $company['config_json'] ?? '';
|
||||
if ($cj) {
|
||||
$parsed = json_decode($cj, true);
|
||||
if (is_array($parsed)) $config = $parsed;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$configJson = self::h(json_encode($config, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
|
||||
|
||||
http_response_code(200);
|
||||
header('Content-Type: text/html; charset=utf-8');
|
||||
echo <<<HTML
|
||||
<!DOCTYPE html>
|
||||
<html lang="es">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Configurar Bot — Palmas360</title>
|
||||
<style>
|
||||
*{box-sizing:border-box;margin:0;padding:0}
|
||||
body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Oxygen,Ubuntu,Cantarell,sans-serif;background:#f5f7fb;color:#1a1d23;font-size:14px;line-height:1.5;-webkit-font-smoothing:antialiased}
|
||||
.topbar{background:linear-gradient(135deg,#0a2e6e,#0b3d91,#1565c0);color:#fff;padding:12px 28px;display:flex;align-items:center;justify-content:space-between;position:sticky;top:0;z-index:100;box-shadow:0 2px 12px rgba(0,0,0,.12)}
|
||||
.topbar h1{font-size:15px;font-weight:700;letter-spacing:.02em;display:flex;align-items:center;gap:8px}
|
||||
.topbar-r{display:flex;align-items:center;gap:10px}
|
||||
.ubadge{background:rgba(255,255,255,.15);border-radius:20px;padding:4px 14px;font-size:12px;font-weight:500;backdrop-filter:blur(4px)}
|
||||
.btn-out{background:rgba(255,255,255,.15);color:#fff;border:1px solid rgba(255,255,255,.2);border-radius:8px;padding:6px 16px;font-size:13px;font-weight:500;cursor:pointer;text-decoration:none;transition:all .15s;backdrop-filter:blur(4px)}
|
||||
.btn-out:hover{background:rgba(255,255,255,.25);border-color:rgba(255,255,255,.35)}
|
||||
.nav-bar{background:#fff;padding:10px 28px;display:flex;gap:2px;flex-wrap:wrap;border-bottom:1px solid #eef1f5;box-shadow:0 1px 3px rgba(0,0,0,.04);position:sticky;top:52px;z-index:99}
|
||||
.nav-link{padding:8px 18px;border-radius:10px;font-size:13px;font-weight:500;color:#5f6b7a;text-decoration:none;transition:all .2s ease;display:inline-flex;align-items:center;gap:6px;letter-spacing:.01em}
|
||||
.nav-link:hover{background:#f0f4ff;color:#0b3d91;transform:translateY(-1px)}
|
||||
.nav-link.active{background:#0b3d91;color:#fff;box-shadow:0 2px 8px rgba(11,61,145,.25)}
|
||||
.wrap{padding:24px;max-width:1100px;margin:0 auto}
|
||||
.toast{padding:12px 18px;border-radius:10px;font-size:13px;font-weight:600;margin-bottom:16px;text-align:center}
|
||||
.toast-success{background:#dcfce7;color:#166534;border:1px solid #a3e4bc}
|
||||
.tabs{display:flex;gap:2px;margin-bottom:0;flex-wrap:wrap;position:sticky;top:104px;z-index:90;background:#f5f7fb;padding:10px 0 0}
|
||||
.tab-btn{padding:10px 18px;border:none;border-radius:10px 10px 0 0;font-size:13px;font-weight:600;cursor:pointer;background:#e8ecf2;color:#5f6b7a;transition:all .15s}
|
||||
.tab-btn:hover{background:#dce1e8}
|
||||
.tab-btn.active{background:#fff;color:#0b3d91;box-shadow:0 -2px 4px rgba(0,0,0,.04)}
|
||||
.tab-content{display:none;background:#fff;border-radius:0 12px 12px 12px;box-shadow:0 1px 4px rgba(0,0,0,.04),0 4px 16px rgba(0,0,0,.03);padding:24px;border:1px solid #eef1f5;margin-bottom:24px}
|
||||
.tab-content.active{display:block}
|
||||
.card-h{font-size:16px;font-weight:700;color:#0b3d91;margin-bottom:16px;padding-bottom:10px;border-bottom:2px solid #eef1f5}
|
||||
.form-group{margin-bottom:14px}
|
||||
.form-group label{display:block;font-size:12px;font-weight:600;color:#3d4552;margin-bottom:4px}
|
||||
.form-group input,.form-group select,.form-group textarea{width:100%;padding:9px 12px;border:1px solid #e2e5ea;border-radius:8px;font-size:13px;outline:none;font-family:inherit;background:#fafbfc;transition:all .15s;color:#1a1d23}
|
||||
.form-group input:focus,.form-group select:focus,.form-group textarea:focus{border-color:#1565c0;background:#fff;box-shadow:0 0 0 3px rgba(21,101,192,.08)}
|
||||
.form-group textarea{min-height:60px;resize:vertical;font-family:'SF Mono',Monaco,monospace;font-size:12px;line-height:1.6}
|
||||
.form-row{display:flex;gap:12px;flex-wrap:wrap}
|
||||
.form-row .form-group{flex:1;min-width:150px}
|
||||
.btn-primary{background:#0b3d91;color:#fff;border:none;border-radius:8px;padding:10px 24px;font-size:14px;font-weight:600;cursor:pointer;transition:all .15s}
|
||||
.btn-primary:hover{background:#1565c0;box-shadow:0 2px 8px rgba(11,61,145,.2)}
|
||||
.btn-secondary{background:#f0f4ff;color:#0b3d91;border:1px solid #d6e4ff;border-radius:8px;padding:8px 16px;font-size:13px;font-weight:600;cursor:pointer;transition:all .15s}
|
||||
.btn-secondary:hover{background:#dbeafe}
|
||||
.btn-danger-sm{background:#fef2f2;color:#991b1b;border:1px solid #fecaca;border-radius:6px;padding:4px 10px;font-size:11px;font-weight:600;cursor:pointer;transition:all .15s}
|
||||
.btn-danger-sm:hover{background:#fee2e2}
|
||||
.item-card{background:#f8f9fd;border:1px solid #eef1f5;border-radius:10px;padding:16px;margin-bottom:12px}
|
||||
.item-card .item-head{display:flex;align-items:center;justify-content:space-between;margin-bottom:10px;font-weight:600;font-size:13px;color:#0b3d91}
|
||||
.item-card .item-head .del{cursor:pointer;color:#991b1b;font-size:12px;background:none;border:none;font-weight:600}
|
||||
.add-btn{background:none;border:2px dashed #d6e4ff;border-radius:8px;padding:10px;text-align:center;color:#0b3d91;font-size:13px;font-weight:600;cursor:pointer;width:100%;transition:all .15s}
|
||||
.add-btn:hover{background:#f0f4ff;border-color:#0b3d91}
|
||||
.inline-group{display:flex;gap:8px;align-items:center;margin-bottom:8px;flex-wrap:wrap}
|
||||
.inline-group input,.inline-group select{flex:1;min-width:100px}
|
||||
.inline-group .del{margin-left:auto}
|
||||
table.simple{width:100%;border-collapse:collapse;margin-bottom:12px}
|
||||
table.simple th{text-align:left;padding:8px 10px;font-size:11px;font-weight:700;color:#0b3d91;text-transform:uppercase;letter-spacing:.3px;border-bottom:2px solid #e8eaf6;background:#f8f9fd}
|
||||
table.simple td{padding:8px 10px;border-bottom:1px solid #f0f2f5;font-size:13px;vertical-align:middle}
|
||||
.company-select{display:flex;gap:12px;align-items:center;margin-bottom:20px;flex-wrap:wrap}
|
||||
.company-select select{padding:9px 14px;border:1px solid #e2e5ea;border-radius:8px;font-size:13px;outline:none;background:#fff;min-width:250px}
|
||||
.company-select select:focus{border-color:#1565c0}
|
||||
@media(max-width:768px){.wrap{padding:12px}.tabs{top:auto}.tab-content{padding:16px}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="topbar">
|
||||
<div><h1><img src="https://app.palmas360.com/Test/imgLogin/logo.png" alt="Palmas360" style="height:32px;vertical-align:middle;margin-right:8px;filter:brightness(0) invert(1)"> Configurar Bot</h1></div>
|
||||
<div class="topbar-r"><span class="ubadge">👤 {$userName}</span><a href="/logout" class="btn-out">Salir</a></div>
|
||||
</div>
|
||||
<nav class="nav-bar">
|
||||
<a href="/admin/dashboard" class="nav-link">📊 Dashboard</a>
|
||||
<a href="/admin/live" class="nav-link">🔴 Live Feed</a>
|
||||
<a href="/admin/pending" class="nav-link">⏳ Pendientes</a>
|
||||
<a href="/admin/chat" class="nav-link">💬 Chat</a>
|
||||
<a href="/admin/companies" class="nav-link">🏢 Empresas</a>
|
||||
<a href="/admin/bot-config" class="nav-link active">🤖 Bot Config</a>
|
||||
<a href="/admin/settings" class="nav-link">⚙️ Configuración</a>
|
||||
<a href="/admin/sync-companies" class="nav-link" onclick="return confirm('¿Sincronizar empresas desde el ERP?')">🔄 Sincronizar</a>
|
||||
<a href="/admin/process-queue" class="nav-link" onclick="return confirm('¿Procesar cola de mensajes?')">📨 Procesar Cola</a>
|
||||
</nav>
|
||||
<div class="wrap">
|
||||
{$toastHtml}
|
||||
<div class="company-select">
|
||||
<label style="font-weight:600;font-size:13px">Seleccionar empresa:</label>
|
||||
<select onchange="if(this.value) window.location='?id='+this.value">
|
||||
<option value="">— Selecciona —</option>
|
||||
HTML;
|
||||
foreach ($companies as $c) {
|
||||
$sel = $companyId === (int)$c['id'] ? 'selected' : '';
|
||||
echo "<option value=\"{$c['id']}\" {$sel}>" . self::h($c['name'] . ' — ' . ($c['display_name'] ?? '')) . "</option>";
|
||||
}
|
||||
echo '</select></div>';
|
||||
|
||||
if (!$company) {
|
||||
echo '<div class="tab-content active" style="border-radius:12px;text-align:center;padding:40px;color:#a0a8b8;font-size:15px">Selecciona una empresa para configurar su bot.</div>';
|
||||
echo '</div></body></html>';
|
||||
exit;
|
||||
}
|
||||
|
||||
$c = $company;
|
||||
$cId = (int)$c['id'];
|
||||
$cName = self::h($c['name'] ?? '');
|
||||
|
||||
// Pre-fill form values from config
|
||||
$greetingVal = self::h($config['greeting'] ?? '');
|
||||
$fallbackVal = self::h($config['fallback'] ?? '');
|
||||
$aiPromptVal = self::h($config['ai_prompt'] ?? '');
|
||||
$aiModelVal = self::h($config['ai_model'] ?? 'gpt-4o-mini');
|
||||
$aiTempVal = self::h((string)($config['ai_temperature'] ?? 0.7));
|
||||
$aiProviderVal = self::h($config['ai_provider'] ?? '');
|
||||
$approvalWebhookVal = self::h($config['approval_webhook'] ?? '');
|
||||
$ignoreVal = self::h(implode("\n", $config['ignore_prefixes'] ?? []));
|
||||
$commands = $config['commands'] ?? [];
|
||||
$menus = $config['menus'] ?? [];
|
||||
$flows = $config['flows'] ?? [];
|
||||
$aiProviderOpenai = $aiProviderVal === 'openai' ? ' selected' : '';
|
||||
$aiProviderMock = $aiProviderVal === 'mock' ? ' selected' : '';
|
||||
$aiModelMini = $aiModelVal === 'gpt-4o-mini' ? ' selected' : '';
|
||||
$aiModel4o = $aiModelVal === 'gpt-4o' ? ' selected' : '';
|
||||
$aiModel35 = $aiModelVal === 'gpt-3.5-turbo' ? ' selected' : '';
|
||||
|
||||
echo <<<HTML
|
||||
<form method="POST" action="/admin/bot-config/save">
|
||||
<input type="hidden" name="company_id" value="{$cId}">
|
||||
|
||||
<div class="tabs">
|
||||
<button type="button" class="tab-btn active" onclick="switchTab('commands',this)">📋 Comandos</button>
|
||||
<button type="button" class="tab-btn" onclick="switchTab('menus',this)">📑 Menús</button>
|
||||
<button type="button" class="tab-btn" onclick="switchTab('flows',this)">🔀 Flujos</button>
|
||||
<button type="button" class="tab-btn" onclick="switchTab('ai',this)">🤖 IA</button>
|
||||
<button type="button" class="tab-btn" onclick="switchTab('general',this)">⚙️ General</button>
|
||||
</div>
|
||||
|
||||
<!-- ─── COMMANDS ─────────────────────────────────────────────────────── -->
|
||||
<div class="tab-content active" id="tab-commands">
|
||||
<div class="card-h">📋 Comandos</div>
|
||||
<p style="font-size:12px;color:#7a8291;margin-bottom:14px">Palabras clave que el usuario escribe para ejecutar acciones. Ej: "menu", "info", "contacto"</p>
|
||||
<table class="simple" id="cmdTable">
|
||||
<thead><tr><th>Palabra clave</th><th>Acción (menú o flujo)</th><th></th></tr></thead>
|
||||
<tbody id="cmdBody">
|
||||
HTML;
|
||||
$cmdIdx = 0;
|
||||
foreach ($commands as $keyword => $action) {
|
||||
echo "<tr id=\"cmd-row-{$cmdIdx}\">
|
||||
<td><input type=\"text\" name=\"cmd_keyword[]\" value=\"" . self::h($keyword) . "\" style=\"width:100%;padding:6px 10px;border:1px solid #e2e5ea;border-radius:6px;font-size:13px\"></td>
|
||||
<td><input type=\"text\" name=\"cmd_action[]\" value=\"" . self::h($action) . "\" style=\"width:100%;padding:6px 10px;border:1px solid #e2e5ea;border-radius:6px;font-size:13px\" placeholder=\"ej: show_main, servicios...\"></td>
|
||||
<td><button type=\"button\" class=\"btn-danger-sm\" onclick=\"this.closest('tr').remove()\">✕</button></td>
|
||||
</tr>";
|
||||
$cmdIdx++;
|
||||
}
|
||||
echo <<<HTML
|
||||
</tbody>
|
||||
</table>
|
||||
<button type="button" class="add-btn" onclick="addCmd()">+ Agregar comando</button>
|
||||
</div>
|
||||
|
||||
<!-- ─── MENUS ────────────────────────────────────────────────────────── -->
|
||||
<div class="tab-content" id="tab-menus">
|
||||
<div class="card-h">📑 Menús Interactivos</div>
|
||||
<p style="font-size:12px;color:#7a8291;margin-bottom:14px">Menús tipo lista o botones que se envían al usuario. Cada menú tiene un ID (ej: show_main) y puede tener secciones con filas.</p>
|
||||
<div id="menuContainer">
|
||||
HTML;
|
||||
$menuIdx = 0;
|
||||
foreach ($menus as $mk => $menu) {
|
||||
$mt = $menu['type'] ?? 'list';
|
||||
$mh = self::h($menu['header'] ?? '');
|
||||
$mb = self::h($menu['body'] ?? '');
|
||||
$mf = self::h($menu['footer'] ?? '');
|
||||
$mbtn = self::h($menu['button'] ?? '');
|
||||
echo "<div class=\"item-card\" id=\"menu-card-{$menuIdx}\">
|
||||
<div class=\"item-head\">
|
||||
<span>Menú: <input type=\"text\" name=\"menu_key[]\" value=\"" . self::h($mk) . "\" style=\"border:none;background:transparent;font-weight:700;color:#0b3d91;width:200px;font-size:13px\" placeholder=\"ID del menú (ej: show_main)\"></span>
|
||||
<button type=\"button\" class=\"del\" onclick=\"this.closest('.item-card').remove()\">✕ Eliminar menú</button>
|
||||
</div>
|
||||
<div class=\"form-row\">
|
||||
<div class=\"form-group\"><label>Tipo</label><select name=\"menu_type[]\"><option value=\"list\"" . ($mt==='list'?' selected':'') . ">Lista</option><option value=\"button\"" . ($mt==='button'?' selected':'') . ">Botones</option></select></div>
|
||||
<div class=\"form-group\"><label>Header</label><input type=\"text\" name=\"menu_header[]\" value=\"{$mh}\" placeholder=\"Título del menú\"></div>
|
||||
<div class=\"form-group\"><label>Body</label><input type=\"text\" name=\"menu_body[]\" value=\"{$mb}\" placeholder=\"Cuerpo del mensaje\"></div>
|
||||
<div class=\"form-group\"><label>Footer</label><input type=\"text\" name=\"menu_footer[]\" value=\"{$mf}\" placeholder=\"Footer (opcional)\"></div>
|
||||
<div class=\"form-group\"><label>Botón</label><input type=\"text\" name=\"menu_button[]\" value=\"{$mbtn}\" placeholder=\"Texto del botón\"></div>
|
||||
</div>
|
||||
<div style=\"margin-top:8px\">
|
||||
<div style=\"font-size:12px;font-weight:600;color:#3d4552;margin-bottom:6px\">Secciones:</div>
|
||||
<div class=\"sections-container\" id=\"menu-sections-{$menuIdx}\">";
|
||||
$si = 0;
|
||||
foreach ($menu['sections'] ?? [] as $section) {
|
||||
$st = self::h($section['title'] ?? '');
|
||||
echo "<div class=\"section-card\" style=\"background:#fff;border:1px solid #eef1f5;border-radius:8px;padding:12px;margin-bottom:8px\">
|
||||
<div style=\"display:flex;align-items:center;gap:8px;margin-bottom:8px\">
|
||||
<input type=\"text\" name=\"menu_sections[{$menuIdx}][{$si}][title]\" value=\"{$st}\" placeholder=\"Título de sección\" style=\"flex:1;padding:6px 10px;border:1px solid #e2e5ea;border-radius:6px;font-size:13px\">
|
||||
<button type=\"button\" class=\"btn-danger-sm\" onclick=\"this.closest('.section-card').remove()\">✕</button>
|
||||
</div>
|
||||
<div style=\"font-size:11px;font-weight:600;color:#7a8291;margin-bottom:4px\">Filas (cada fila es una opción):</div>";
|
||||
$ri = 0;
|
||||
foreach ($section['rows'] ?? [] as $row) {
|
||||
$rid = self::h($row['id'] ?? '');
|
||||
$rt = self::h($row['title'] ?? '');
|
||||
$rd = self::h($row['description'] ?? '');
|
||||
echo "<div class=\"inline-group\" style=\"margin-bottom:4px\">
|
||||
<input type=\"text\" name=\"menu_sections[{$menuIdx}][{$si}][rows][{$ri}][id]\" value=\"{$rid}\" placeholder=\"ID (ej: servicios)\" style=\"flex:1\">
|
||||
<input type=\"text\" name=\"menu_sections[{$menuIdx}][{$si}][rows][{$ri}][title]\" value=\"{$rt}\" placeholder=\"Título\" style=\"flex:1\">
|
||||
<input type=\"text\" name=\"menu_sections[{$menuIdx}][{$si}][rows][{$ri}][description]\" value=\"{$rd}\" placeholder=\"Descripción\" style=\"flex:1.5\">
|
||||
<button type=\"button\" class=\"btn-danger-sm\" onclick=\"this.closest('.inline-group').remove()\">✕</button>
|
||||
</div>";
|
||||
$ri++;
|
||||
}
|
||||
echo "<button type=\"button\" class=\"btn-secondary\" style=\"font-size:11px;padding:4px 10px;margin-top:4px\" onclick=\"addRow(this,{$menuIdx},{$si})\">+ Fila</button>
|
||||
</div>";
|
||||
$si++;
|
||||
}
|
||||
echo "</div>
|
||||
<button type=\"button\" class=\"btn-secondary\" style=\"font-size:11px;padding:4px 10px;margin-top:6px\" onclick=\"addSection({$menuIdx})\">+ Sección</button>
|
||||
</div>
|
||||
</div>";
|
||||
$menuIdx++;
|
||||
}
|
||||
echo <<<HTML
|
||||
</div>
|
||||
<button type="button" class="add-btn" onclick="addMenu()">+ Agregar menú</button>
|
||||
</div>
|
||||
|
||||
<!-- ─── FLOWS ────────────────────────────────────────────────────────── -->
|
||||
<div class="tab-content" id="tab-flows">
|
||||
<div class="card-h">🔀 Flujos</div>
|
||||
<p style="font-size:12px;color:#7a8291;margin-bottom:14px">Acciones que se ejecutan cuando un comando o fila de menú es seleccionada.</p>
|
||||
<div id="flowContainer">
|
||||
HTML;
|
||||
$flowIdx = 0;
|
||||
foreach ($flows as $fk => $flow) {
|
||||
$ft = $flow['type'] ?? 'text';
|
||||
$fMsg = self::h($flow['message'] ?? '');
|
||||
$fFn = self::h($flow['function'] ?? 'forward_to_ai');
|
||||
$fMenu = self::h($flow['menu'] ?? '');
|
||||
echo "<div class=\"item-card\" id=\"flow-card-{$flowIdx}\">
|
||||
<div class=\"item-head\">
|
||||
<span>ID: <input type=\"text\" name=\"flow_key[]\" value=\"" . self::h($fk) . "\" style=\"border:none;background:transparent;font-weight:700;color:#0b3d91;width:200px;font-size:13px\" placeholder=\"ID del flujo (ej: servicios)\"></span>
|
||||
<button type=\"button\" class=\"del\" onclick=\"this.closest('.item-card').remove()\">✕</button>
|
||||
</div>
|
||||
<div class=\"form-row\">
|
||||
<div class=\"form-group\" style=\"min-width:120px\">
|
||||
<label>Tipo</label>
|
||||
<select name=\"flow_type[]\" onchange=\"flowTypeChange(this)\">
|
||||
<option value=\"text\"" . ($ft==='text'?' selected':'') . ">Texto (respuesta fija)</option>
|
||||
<option value=\"function\"" . ($ft==='function'?' selected':'') . ">Función (acción especial)</option>
|
||||
<option value=\"menu\"" . ($ft==='menu'?' selected':'') . ">Menú (submenú)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class=\"form-group flow-text\" style=\"" . ($ft==='text'?'':'display:none') . "\">
|
||||
<label>Mensaje de respuesta</label>
|
||||
<textarea name=\"flow_message[]\" rows=\"2\" placeholder=\"Texto que se enviará al usuario\">{$fMsg}</textarea>
|
||||
</div>
|
||||
<div class=\"form-group flow-function\" style=\"" . ($ft==='function'?'':'display:none') . "\">
|
||||
<label>Función</label>
|
||||
<select name=\"flow_function[]\">
|
||||
<option value=\"forward_to_ai\"" . ($fFn==='forward_to_ai'?' selected':'') . ">Escalar a IA</option>
|
||||
<option value=\"forward_to_agent\"" . ($fFn==='forward_to_agent'?' selected':'') . ">Escalar a agente humano</option>
|
||||
<option value=\"webhook\"" . ($fFn==='webhook'?' selected':'') . ">Llamar webhook externo</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class=\"form-group flow-menu\" style=\"" . ($ft==='menu'?'':'display:none') . "\">
|
||||
<label>Menú a mostrar</label>
|
||||
<input type=\"text\" name=\"flow_menu[]\" value=\"{$fMenu}\" placeholder=\"ID del menú (ej: show_main)\">
|
||||
</div>
|
||||
</div>
|
||||
</div>";
|
||||
$flowIdx++;
|
||||
}
|
||||
echo <<<HTML
|
||||
</div>
|
||||
<button type="button" class="add-btn" onclick="addFlow()">+ Agregar flujo</button>
|
||||
</div>
|
||||
|
||||
<!-- ─── AI ───────────────────────────────────────────────────────────── -->
|
||||
<div class="tab-content" id="tab-ai">
|
||||
<div class="card-h">🤖 Inteligencia Artificial</div>
|
||||
<div class="form-group">
|
||||
<label>Proveedor (sobrescribe el global)</label>
|
||||
<select name="ai_provider">
|
||||
<option value="">Usar configuración global</option>
|
||||
<option value="openai"{$aiProviderOpenai}>OpenAI</option>
|
||||
<option value="mock"{$aiProviderMock}>Mock (simulado)</option>
|
||||
</select>
|
||||
<div style="font-size:11px;color:#7a8291;margin-top:3px">Si no se selecciona, usa el proveedor de Configuración General</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label>Modelo</label>
|
||||
<select name="ai_model">
|
||||
<option value="gpt-4o-mini"{$aiModelMini}>GPT-4o Mini</option>
|
||||
<option value="gpt-4o"{$aiModel4o}>GPT-4o</option>
|
||||
<option value="gpt-3.5-turbo"{$aiModel35}>GPT-3.5 Turbo</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Temperatura ({$aiTempVal})</label>
|
||||
<input type="range" name="ai_temperature" min="0" max="1" step="0.1" value="{$aiTempVal}" oninput="this.previousElementSibling.textContent='Temperatura ('+this.value+')'">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>System Prompt</label>
|
||||
<textarea name="ai_prompt" rows="4" placeholder="Eres un asistente virtual de {$cName}...">{$aiPromptVal}</textarea>
|
||||
<div style="font-size:11px;color:#7a8291;margin-top:3px">Instrucciones que define el comportamiento de la IA</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ─── GENERAL ──────────────────────────────────────────────────────── -->
|
||||
<div class="tab-content" id="tab-general">
|
||||
<div class="card-h">⚙️ General</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label>Mensaje de bienvenida (greeting)</label>
|
||||
<textarea name="greeting" rows="2" placeholder="¡Bienvenido! Escribe 'menu' para ver opciones...">{$greetingVal}</textarea>
|
||||
<div style="font-size:11px;color:#7a8291;margin-top:3px">Primer mensaje que recibe un usuario nuevo</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Mensaje por defecto (fallback)</label>
|
||||
<textarea name="fallback" rows="2" placeholder="No entendí. Escribe menu para ver opciones.">{$fallbackVal}</textarea>
|
||||
<div style="font-size:11px;color:#7a8291;margin-top:3px">Cuando el bot no entiende el mensaje</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Webhook de aprobación (approval_webhook)</label>
|
||||
<input type="text" name="approval_webhook" value="{$approvalWebhookVal}" placeholder="https://api.ejemplo.com/webhook-aprobacion">
|
||||
<div style="font-size:11px;color:#7a8291;margin-top:3px">URL a la que se notificará cuando se requiera aprobación</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Prefijos a ignorar (uno por línea)</label>
|
||||
<textarea name="ignore_prefixes" rows="3" placeholder="Ej: 555 111">{$ignoreVal}</textarea>
|
||||
<div style="font-size:11px;color:#7a8291;margin-top:3px">Números que comiencen con estos prefijos no recibirán respuestas del bot</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display:flex;gap:12px;margin-top:16px">
|
||||
<button type="submit" class="btn-primary">💾 Guardar configuración</button>
|
||||
<a href="/admin/companies" class="btn-secondary" style="text-decoration:none;display:inline-flex;align-items:center">↩ Volver a empresas</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function switchTab(tab, btn) {
|
||||
document.querySelectorAll('.tab-content').forEach(t => t.classList.remove('active'));
|
||||
document.querySelectorAll('.tab-btn').forEach(t => t.classList.remove('active'));
|
||||
document.getElementById('tab-' + tab).classList.add('active');
|
||||
btn.classList.add('active');
|
||||
}
|
||||
|
||||
// Commands
|
||||
function addCmd() {
|
||||
const tbody = document.getElementById('cmdBody');
|
||||
const idx = tbody.children.length;
|
||||
const tr = document.createElement('tr');
|
||||
tr.id = 'cmd-row-' + idx;
|
||||
tr.innerHTML = '<td><input type="text" name="cmd_keyword[]" style="width:100%;padding:6px 10px;border:1px solid #e2e5ea;border-radius:6px;font-size:13px" placeholder="ej: menu"></td>' +
|
||||
'<td><input type="text" name="cmd_action[]" style="width:100%;padding:6px 10px;border:1px solid #e2e5ea;border-radius:6px;font-size:13px" placeholder="ej: show_main"></td>' +
|
||||
'<td><button type="button" class="btn-danger-sm" onclick="this.closest(\'tr\').remove()">✕</button></td>';
|
||||
tbody.appendChild(tr);
|
||||
}
|
||||
|
||||
// Menus
|
||||
function addMenu() {
|
||||
const container = document.getElementById('menuContainer');
|
||||
const idx = container.children.length;
|
||||
const div = document.createElement('div');
|
||||
div.className = 'item-card';
|
||||
div.id = 'menu-card-' + idx;
|
||||
div.innerHTML = '<div class="item-head"><span>Menú: <input type="text" name="menu_key[]" style="border:none;background:transparent;font-weight:700;color:#0b3d91;width:200px;font-size:13px" placeholder="ID del menú (ej: show_main)"></span><button type="button" class="del" onclick="this.closest(\'.item-card\').remove()">✕ Eliminar menú</button></div>' +
|
||||
'<div class="form-row">' +
|
||||
'<div class="form-group"><label>Tipo</label><select name="menu_type[]"><option value="list">Lista</option><option value="button">Botones</option></select></div>' +
|
||||
'<div class="form-group"><label>Header</label><input type="text" name="menu_header[]" placeholder="Título del menú"></div>' +
|
||||
'<div class="form-group"><label>Body</label><input type="text" name="menu_body[]" placeholder="Cuerpo del mensaje"></div>' +
|
||||
'<div class="form-group"><label>Footer</label><input type="text" name="menu_footer[]" placeholder="Footer (opcional)"></div>' +
|
||||
'<div class="form-group"><label>Botón</label><input type="text" name="menu_button[]" placeholder="Texto del botón"></div>' +
|
||||
'</div>' +
|
||||
'<div style="margin-top:8px"><div style="font-size:12px;font-weight:600;color:#3d4552;margin-bottom:6px">Secciones:</div><div class="sections-container" id="menu-sections-' + idx + '"></div><button type="button" class="btn-secondary" style="font-size:11px;padding:4px 10px;margin-top:6px" onclick="addSection(' + idx + ')">+ Sección</button></div>';
|
||||
container.appendChild(div);
|
||||
}
|
||||
|
||||
function addSection(mi) {
|
||||
const container = document.getElementById('menu-sections-' + mi);
|
||||
const si = container.children.length;
|
||||
const div = document.createElement('div');
|
||||
div.className = 'section-card';
|
||||
div.style.cssText = 'background:#fff;border:1px solid #eef1f5;border-radius:8px;padding:12px;margin-bottom:8px';
|
||||
div.innerHTML = '<div style="display:flex;align-items:center;gap:8px;margin-bottom:8px"><input type="text" name="menu_sections[' + mi + '][' + si + '][title]" placeholder="Título de sección" style="flex:1;padding:6px 10px;border:1px solid #e2e5ea;border-radius:6px;font-size:13px"><button type="button" class="btn-danger-sm" onclick="this.closest(\'.section-card\').remove()">✕</button></div>' +
|
||||
'<div style="font-size:11px;font-weight:600;color:#7a8291;margin-bottom:4px">Filas:</div>' +
|
||||
'<div class="rows-container" id="menu-rows-' + mi + '-' + si + '"></div>' +
|
||||
'<button type="button" class="btn-secondary" style="font-size:11px;padding:4px 10px;margin-top:4px" onclick="addRow(this,' + mi + ',' + si + ')">+ Fila</button>';
|
||||
container.appendChild(div);
|
||||
}
|
||||
|
||||
function addRow(btn, mi, si) {
|
||||
const container = btn.closest('.section-card').querySelector('.rows-container');
|
||||
const ri = container.children.length;
|
||||
const div = document.createElement('div');
|
||||
div.className = 'inline-group';
|
||||
div.style.cssText = 'display:flex;gap:8px;align-items:center;margin-bottom:4px';
|
||||
div.innerHTML = '<input type="text" name="menu_sections[' + mi + '][' + si + '][rows][' + ri + '][id]" placeholder="ID" style="flex:1;padding:6px 10px;border:1px solid #e2e5ea;border-radius:6px;font-size:12px">' +
|
||||
'<input type="text" name="menu_sections[' + mi + '][' + si + '][rows][' + ri + '][title]" placeholder="Título" style="flex:1;padding:6px 10px;border:1px solid #e2e5ea;border-radius:6px;font-size:12px">' +
|
||||
'<input type="text" name="menu_sections[' + mi + '][' + si + '][rows][' + ri + '][description]" placeholder="Descripción" style="flex:1.5;padding:6px 10px;border:1px solid #e2e5ea;border-radius:6px;font-size:12px">' +
|
||||
'<button type="button" class="btn-danger-sm" onclick="this.closest(\'.inline-group\').remove()">✕</button>';
|
||||
container.appendChild(div);
|
||||
}
|
||||
|
||||
// Flows
|
||||
function addFlow() {
|
||||
const container = document.getElementById('flowContainer');
|
||||
const idx = container.children.length;
|
||||
const div = document.createElement('div');
|
||||
div.className = 'item-card';
|
||||
div.id = 'flow-card-' + idx;
|
||||
div.innerHTML = '<div class="item-head"><span>ID: <input type="text" name="flow_key[]" style="border:none;background:transparent;font-weight:700;color:#0b3d91;width:200px;font-size:13px" placeholder="ID del flujo (ej: servicios)"></span><button type="button" class="del" onclick="this.closest(\'.item-card\').remove()">✕</button></div>' +
|
||||
'<div class="form-row">' +
|
||||
'<div class="form-group" style="min-width:120px"><label>Tipo</label><select name="flow_type[]" onchange="flowTypeChange(this)"><option value="text">Texto (respuesta fija)</option><option value="function">Función (acción especial)</option><option value="menu">Menú (submenú)</option></select></div>' +
|
||||
'<div class="form-group flow-text"><label>Mensaje de respuesta</label><textarea name="flow_message[]" rows="2" placeholder="Texto que se enviará al usuario"></textarea></div>' +
|
||||
'<div class="form-group flow-function" style="display:none"><label>Función</label><select name="flow_function[]"><option value="forward_to_ai">Escalar a IA</option><option value="forward_to_agent">Escalar a agente humano</option><option value="webhook">Llamar webhook externo</option></select></div>' +
|
||||
'<div class="form-group flow-menu" style="display:none"><label>Menú a mostrar</label><input type="text" name="flow_menu[]" placeholder="ID del menú (ej: show_main)"></div>' +
|
||||
'</div>';
|
||||
container.appendChild(div);
|
||||
}
|
||||
|
||||
function flowTypeChange(sel) {
|
||||
const card = sel.closest('.item-card');
|
||||
card.querySelector('.flow-text').style.display = sel.value === 'text' ? '' : 'none';
|
||||
card.querySelector('.flow-function').style.display = sel.value === 'function' ? '' : 'none';
|
||||
card.querySelector('.flow-menu').style.display = sel.value === 'menu' ? '' : 'none';
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
HTML;
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,6 +109,13 @@ class WpWebhook
|
||||
|
||||
// Persistir evento crudo para auditoría
|
||||
self::saveRawEvent($raw);
|
||||
|
||||
// Procesar cola outbound inmediatamente para respuestas automáticas
|
||||
try {
|
||||
OutboundWorker::processQueue();
|
||||
} catch (\Throwable $e) {
|
||||
self::log('WARN', 'OutboundWorker falló: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private static function resolveCompany(array $payload): void
|
||||
|
||||
@@ -282,6 +282,110 @@ $routes = [
|
||||
jsonResponse(200, ['status' => 'rejected', 'message' => 'Mensaje rechazado']);
|
||||
})()],
|
||||
|
||||
// ─── Admin: configurador visual del bot ────────────────────────────────
|
||||
['GET', '/admin/bot-config', fn() => DashboardController::botConfig()],
|
||||
|
||||
['POST', '/admin/bot-config/save', fn() => (function () {
|
||||
SessionAuth::require();
|
||||
$companyId = (int)($_POST['company_id'] ?? 0);
|
||||
if ($companyId === 0) { header('Location: /admin/bot-config?msg=error'); exit; }
|
||||
|
||||
$config = [];
|
||||
// Commands
|
||||
$keywords = $_POST['cmd_keyword'] ?? [];
|
||||
$actions = $_POST['cmd_action'] ?? [];
|
||||
$commands = [];
|
||||
foreach ($keywords as $i => $kw) {
|
||||
$kw = trim($kw);
|
||||
$act = trim($actions[$i] ?? '');
|
||||
if ($kw !== '' && $act !== '') $commands[$kw] = $act;
|
||||
}
|
||||
if (!empty($commands)) $config['commands'] = $commands;
|
||||
|
||||
// Menus
|
||||
$menuKeys = $_POST['menu_key'] ?? [];
|
||||
$menuTypes = $_POST['menu_type'] ?? [];
|
||||
$menuHeaders = $_POST['menu_header'] ?? [];
|
||||
$menuBodies = $_POST['menu_body'] ?? [];
|
||||
$menuFooters = $_POST['menu_footer'] ?? [];
|
||||
$menuButtons = $_POST['menu_button'] ?? [];
|
||||
$menuSections = $_POST['menu_sections'] ?? [];
|
||||
$menus = [];
|
||||
foreach ($menuKeys as $i => $mk) {
|
||||
$mk = trim($mk);
|
||||
if ($mk === '') continue;
|
||||
$sections = [];
|
||||
if (!empty($menuSections[$i])) {
|
||||
foreach ($menuSections[$i] as $si => $section) {
|
||||
$title = trim($section['title'] ?? '');
|
||||
if ($title === '') continue;
|
||||
$rows = [];
|
||||
foreach ($section['rows'] ?? [] as $row) {
|
||||
$rid = trim($row['id'] ?? '');
|
||||
$rtitle = trim($row['title'] ?? '');
|
||||
if ($rid === '' || $rtitle === '') continue;
|
||||
$rows[] = ['id' => $rid, 'title' => $rtitle, 'description' => trim($row['description'] ?? '')];
|
||||
}
|
||||
$sections[] = ['title' => $title, 'rows' => $rows];
|
||||
}
|
||||
}
|
||||
$menus[$mk] = [
|
||||
'type' => $menuTypes[$i] ?? 'list',
|
||||
'header' => trim($menuHeaders[$i] ?? ''),
|
||||
'body' => trim($menuBodies[$i] ?? ''),
|
||||
'footer' => trim($menuFooters[$i] ?? ''),
|
||||
'button' => trim($menuButtons[$i] ?? ''),
|
||||
'sections' => $sections,
|
||||
];
|
||||
}
|
||||
if (!empty($menus)) $config['menus'] = $menus;
|
||||
|
||||
// Flows
|
||||
$flowKeys = $_POST['flow_key'] ?? [];
|
||||
$flowTypes = $_POST['flow_type'] ?? [];
|
||||
$flowMessages = $_POST['flow_message'] ?? [];
|
||||
$flowFunctions = $_POST['flow_function'] ?? [];
|
||||
$flowMenus = $_POST['flow_menu'] ?? [];
|
||||
$flows = [];
|
||||
foreach ($flowKeys as $i => $fk) {
|
||||
$fk = trim($fk);
|
||||
if ($fk === '') continue;
|
||||
$ftype = $flowTypes[$i] ?? 'text';
|
||||
$f = ['type' => $ftype];
|
||||
if ($ftype === 'text') $f['message'] = trim($flowMessages[$i] ?? '');
|
||||
elseif ($ftype === 'function') $f['function'] = trim($flowFunctions[$i] ?? 'forward_to_ai');
|
||||
elseif ($ftype === 'menu') $f['menu'] = trim($flowMenus[$i] ?? '');
|
||||
$flows[$fk] = $f;
|
||||
}
|
||||
if (!empty($flows)) $config['flows'] = $flows;
|
||||
|
||||
// AI
|
||||
$aiPrompt = trim($_POST['ai_prompt'] ?? '');
|
||||
if ($aiPrompt !== '') $config['ai_prompt'] = $aiPrompt;
|
||||
$aiModel = trim($_POST['ai_model'] ?? '');
|
||||
if ($aiModel !== '') $config['ai_model'] = $aiModel;
|
||||
$aiTemp = trim($_POST['ai_temperature'] ?? '');
|
||||
if ($aiTemp !== '') $config['ai_temperature'] = (float)$aiTemp;
|
||||
$aiProvider = trim($_POST['ai_provider'] ?? '');
|
||||
if ($aiProvider !== '') $config['ai_provider'] = $aiProvider;
|
||||
|
||||
// General
|
||||
$greeting = trim($_POST['greeting'] ?? '');
|
||||
if ($greeting !== '') $config['greeting'] = $greeting;
|
||||
$fallback = trim($_POST['fallback'] ?? '');
|
||||
if ($fallback !== '') $config['fallback'] = $fallback;
|
||||
$ignorePrefixes = trim($_POST['ignore_prefixes'] ?? '');
|
||||
if ($ignorePrefixes !== '') {
|
||||
$config['ignore_prefixes'] = array_map('trim', explode("\n", $ignorePrefixes));
|
||||
}
|
||||
$approvalWebhook = trim($_POST['approval_webhook'] ?? '');
|
||||
if ($approvalWebhook !== '') $config['approval_webhook'] = $approvalWebhook;
|
||||
|
||||
CompanyRepository::save(['id' => $companyId, 'config_json' => json_encode($config, JSON_UNESCAPED_UNICODE)]);
|
||||
header('Location: /admin/bot-config?id=' . $companyId . '&msg=saved');
|
||||
exit;
|
||||
})()],
|
||||
|
||||
// ─── Admin: configuración general ──────────────────────────────────────
|
||||
['GET', '/admin/settings', fn() => DashboardController::settings()],
|
||||
|
||||
|
||||
Reference in New Issue
Block a user