Compare commits
10
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
34322ae3dc | ||
|
|
97a48d0fe4 | ||
|
|
0ce766a9eb | ||
|
|
601169525e | ||
|
|
ae5b7266d4 | ||
|
|
3c79fac8d7 | ||
|
|
8f51659f1e | ||
|
|
cae448a9c6 | ||
|
|
9b0e6ecb71 | ||
|
|
37eef6a705 |
@@ -2875,14 +2875,14 @@ HTML;
|
||||
$approvalWebhookVal = self::h($config['approval_webhook'] ?? '');
|
||||
$ignoreVal = self::h(implode("\n", $config['ignore_prefixes'] ?? []));
|
||||
$welcomeMenuVal = self::h($config['welcome_menu'] ?? '');
|
||||
$commands = $config['commands'] ?? [];
|
||||
$menus = $config['menus'] ?? [];
|
||||
$flows = $config['flows'] ?? [];
|
||||
$welcomeMenuOpts = '';
|
||||
foreach (array_keys($menus) as $mk) {
|
||||
$s = ($config['welcome_menu'] ?? '') === $mk ? ' selected' : '';
|
||||
$welcomeMenuOpts .= '<option value="' . self::h($mk) . '"' . $s . '>' . self::h($mk) . '</option>';
|
||||
}
|
||||
$commands = $config['commands'] ?? [];
|
||||
$menus = $config['menus'] ?? [];
|
||||
$flows = $config['flows'] ?? [];
|
||||
$aiProviderOpenai = $aiProviderVal === 'openai' ? ' selected' : '';
|
||||
$aiProviderGemini = $aiProviderVal === 'gemini' ? ' selected' : '';
|
||||
$aiProviderMock = $aiProviderVal === 'mock' ? ' selected' : '';
|
||||
@@ -3347,6 +3347,7 @@ HTML;
|
||||
<div class="card-h">👥 Configuración por Categoría</div>
|
||||
<p style="font-size:12px;color:#7a8291;margin-bottom:16px">Define qué menú de bienvenida ve cada categoría de usuario cuando escribe por primera vez o sin contexto activo.</p>
|
||||
{$categoryTabHtml}
|
||||
<button type="button" id="savePerTypeBtn" onclick="savePerType()" class="btn-primary" style="margin-top:8px">💾 Guardar categorías</button>
|
||||
</div>
|
||||
|
||||
<!-- ─── AI ───────────────────────────────────────────────────────────── -->
|
||||
@@ -3491,6 +3492,28 @@ HTML;
|
||||
const CONFIG = {$botConfigJson};
|
||||
const ENDPOINTS = {$botEndpointsJson};
|
||||
|
||||
function savePerType() {
|
||||
const cid = document.querySelector('input[name="company_id"]').value;
|
||||
const fd = new FormData();
|
||||
fd.set('company_id', cid);
|
||||
[1, 2, 3].forEach(function(cat) {
|
||||
var sel = document.querySelector('[name="per_type_menu[' + cat + ']"]');
|
||||
if (sel) fd.append('per_type_menu[' + cat + ']', sel.value);
|
||||
});
|
||||
var btn = document.getElementById('savePerTypeBtn');
|
||||
if (btn) { btn.textContent = 'Guardando...'; btn.disabled = true; }
|
||||
fetch('/admin/bot-config/save-per-type', { method: 'POST', body: fd })
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(d) {
|
||||
if (btn) { btn.textContent = '💾 Guardar categorías'; btn.disabled = false; }
|
||||
alert(d.ok ? '✅ Categorías guardadas correctamente.' : '❌ Error al guardar: ' + JSON.stringify(d));
|
||||
})
|
||||
.catch(function(e) {
|
||||
if (btn) { btn.textContent = '💾 Guardar categorías'; btn.disabled = false; }
|
||||
alert('❌ Error de red: ' + e.message);
|
||||
});
|
||||
}
|
||||
|
||||
function switchTab(tab, btn) {
|
||||
document.querySelectorAll('.tab-content').forEach(t => t.classList.remove('active'));
|
||||
document.querySelectorAll('.tab-btn').forEach(t => t.classList.remove('active'));
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
class DbSessionHandler implements SessionHandlerInterface
|
||||
{
|
||||
private const TTL = 86400 * 30; // 30 days
|
||||
|
||||
public function open($path, $name): bool { return true; }
|
||||
public function close(): bool { return true; }
|
||||
|
||||
public function read($id): string
|
||||
{
|
||||
$stmt = db()->prepare("SELECT data FROM sessions WHERE id = ? AND updated_at > DATE_SUB(NOW(), INTERVAL " . self::TTL . " SECOND)");
|
||||
$stmt->execute([$id]);
|
||||
$row = $stmt->fetch();
|
||||
return $row ? $row['data'] : '';
|
||||
}
|
||||
|
||||
public function write($id, $data): bool
|
||||
{
|
||||
db()->prepare("INSERT INTO sessions (id, data) VALUES (?, ?) ON DUPLICATE KEY UPDATE data = VALUES(data), updated_at = NOW()")
|
||||
->execute([$id, $data]);
|
||||
return true;
|
||||
}
|
||||
|
||||
public function destroy($id): bool
|
||||
{
|
||||
db()->prepare("DELETE FROM sessions WHERE id = ?")->execute([$id]);
|
||||
return true;
|
||||
}
|
||||
|
||||
public function gc($max_lifetime): int
|
||||
{
|
||||
$stmt = db()->prepare("DELETE FROM sessions WHERE updated_at < DATE_SUB(NOW(), INTERVAL ? SECOND)");
|
||||
$stmt->execute([self::TTL]);
|
||||
return (int)$stmt->rowCount();
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,16 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/DbSessionHandler.php';
|
||||
|
||||
class SessionAuth
|
||||
{
|
||||
public static function start(): void
|
||||
{
|
||||
if (session_status() === PHP_SESSION_NONE) {
|
||||
session_set_save_handler(new DbSessionHandler(), true);
|
||||
session_set_cookie_params([
|
||||
'lifetime' => 0,
|
||||
'lifetime' => 86400 * 30, // 30 days — survives deploys
|
||||
'path' => '/',
|
||||
'secure' => isset($_SERVER['HTTPS']),
|
||||
'httponly' => true,
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
max_input_vars = 5000
|
||||
+56
-13
@@ -320,6 +320,10 @@ $routes = [
|
||||
$companyId = (int)($_POST['company_id'] ?? 0);
|
||||
if ($companyId === 0) { header('Location: /admin/bot-config?msg=error'); exit; }
|
||||
|
||||
// Load existing DB config so keys not in the form (per_type, welcome_menu, etc.) are preserved
|
||||
$existingCompany = CompanyRepository::findById($companyId);
|
||||
$existingConfig = json_decode($existingCompany['config_json'] ?? '{}', true) ?: [];
|
||||
|
||||
$config = [];
|
||||
// Commands
|
||||
$keywords = $_POST['cmd_keyword'] ?? [];
|
||||
@@ -536,20 +540,10 @@ $routes = [
|
||||
$geminiModel = trim($_POST['gemini_model'] ?? '');
|
||||
if ($geminiModel !== '') $config['gemini_model'] = $geminiModel;
|
||||
|
||||
// Per-category menus — merge sobre la config existente para no perder flows/commands por categoría
|
||||
$perTypeMenus = $_POST['per_type_menu'] ?? [];
|
||||
$perType = $config['per_type'] ?? [];
|
||||
foreach ([1, 2, 3] as $cat) {
|
||||
$mk = trim($perTypeMenus[$cat] ?? '');
|
||||
$key = (string)$cat;
|
||||
if ($mk !== '') {
|
||||
$perType[$key] = array_merge($perType[$key] ?? [], ['greeting_menu' => $mk]);
|
||||
} elseif (isset($perType[$key]['greeting_menu'])) {
|
||||
unset($perType[$key]['greeting_menu']);
|
||||
if (empty($perType[$key])) unset($perType[$key]);
|
||||
}
|
||||
// per_type is managed exclusively via /admin/bot-config/save-per-type — copy as-is from DB
|
||||
if (isset($existingConfig['per_type'])) {
|
||||
$config['per_type'] = $existingConfig['per_type'];
|
||||
}
|
||||
$config['per_type'] = $perType;
|
||||
|
||||
// General
|
||||
$greeting = trim($_POST['greeting'] ?? '');
|
||||
@@ -565,11 +559,60 @@ $routes = [
|
||||
$welcomeMenu = trim($_POST['welcome_menu'] ?? '');
|
||||
if ($welcomeMenu !== '') $config['welcome_menu'] = $welcomeMenu;
|
||||
|
||||
// Preserve any DB keys the form doesn't explicitly manage (welcome_menu, per_type, etc.)
|
||||
$formManagedKeys = ['commands', 'menus', 'flows', 'ai_prompt', 'ai_model', 'ai_temperature',
|
||||
'ai_provider', 'openai_api_key', 'gemini_api_key', 'gemini_model',
|
||||
'ai_for_media', 'greeting', 'fallback', 'approval_webhook',
|
||||
'ignore_prefixes', 'welcome_menu', 'per_type'];
|
||||
foreach ($existingConfig as $k => $v) {
|
||||
if (!in_array($k, $formManagedKeys, true) && !isset($config[$k])) {
|
||||
$config[$k] = $v;
|
||||
}
|
||||
// Also preserve managed keys that POST didn't provide (e.g. truncated by max_input_vars)
|
||||
if (in_array($k, ['welcome_menu', 'per_type'], true) && !isset($config[$k])) {
|
||||
$config[$k] = $v;
|
||||
}
|
||||
}
|
||||
|
||||
CompanyRepository::save(['id' => $companyId, 'config_json' => json_encode($config, JSON_UNESCAPED_UNICODE)]);
|
||||
header('Location: /admin/bot-config?id=' . $companyId . '&msg=saved');
|
||||
exit;
|
||||
})()],
|
||||
|
||||
// ─── Admin: guardar per_type por categoría (endpoint separado) ───────────
|
||||
['POST', '/admin/bot-config/save-per-type', fn() => (function () {
|
||||
SessionAuth::require();
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
$companyId = (int)($_POST['company_id'] ?? 0);
|
||||
if ($companyId === 0) { echo json_encode(['ok' => false]); exit; }
|
||||
|
||||
$company = CompanyRepository::findById($companyId);
|
||||
if (!$company) { echo json_encode(['ok' => false]); exit; }
|
||||
|
||||
$config = json_decode($company['config_json'] ?? '{}', true) ?: [];
|
||||
|
||||
$rawPt = $config['per_type'] ?? [];
|
||||
$perType = (is_array($rawPt) && count(array_filter(array_keys($rawPt), 'is_string')) > 0)
|
||||
? $rawPt : [];
|
||||
|
||||
$perTypeMenus = $_POST['per_type_menu'] ?? [];
|
||||
foreach ([1, 2, 3] as $cat) {
|
||||
$mk = trim($perTypeMenus[$cat] ?? '');
|
||||
$key = (string)$cat;
|
||||
if ($mk !== '') {
|
||||
$perType[$key] = array_merge($perType[$key] ?? [], ['greeting_menu' => $mk]);
|
||||
} else {
|
||||
unset($perType[$key]['greeting_menu']);
|
||||
if (empty($perType[$key])) unset($perType[$key]);
|
||||
}
|
||||
}
|
||||
$config['per_type'] = empty($perType) ? new stdClass() : $perType;
|
||||
|
||||
CompanyRepository::save(['id' => $companyId, 'config_json' => json_encode($config, JSON_UNESCAPED_UNICODE)]);
|
||||
echo json_encode(['ok' => true]);
|
||||
exit;
|
||||
})()],
|
||||
|
||||
// ─── Admin: configuración general ──────────────────────────────────────
|
||||
['GET', '/admin/settings', fn() => DashboardController::settings()],
|
||||
|
||||
|
||||
+17
-4
@@ -136,6 +136,14 @@ class NormalBot
|
||||
$group = $col['meta_group'] ?? '';
|
||||
$key = $col['meta_key'] ?? '';
|
||||
$nextNode = $col['next_node'] ?? null;
|
||||
$validIds = $col['__valid_ids'] ?? null;
|
||||
|
||||
if ($validIds !== null && !in_array(trim($input), $validIds, true)) {
|
||||
return self::sendText(
|
||||
'⚠️ Por favor selecciona una opción válida de la lista.',
|
||||
$context['from'], $company
|
||||
);
|
||||
}
|
||||
|
||||
$formData = $meta[$group] ?? [];
|
||||
$formData[$key] = trim($input);
|
||||
@@ -182,10 +190,12 @@ class NormalBot
|
||||
);
|
||||
}
|
||||
|
||||
$valueField = $flow['value_field'] ?? 'id';
|
||||
$meta['collecting'] = [
|
||||
'meta_group' => $flow['meta_group'] ?? '',
|
||||
'meta_key' => $flow['meta_key'] ?? '',
|
||||
'next_node' => $flow['next_node'] ?? null,
|
||||
'meta_group' => $flow['meta_group'] ?? '',
|
||||
'meta_key' => $flow['meta_key'] ?? '',
|
||||
'next_node' => $flow['next_node'] ?? null,
|
||||
'__valid_ids' => array_column($items, $valueField),
|
||||
];
|
||||
ConversationContext::updateMetadata($ctxId, $meta);
|
||||
ConversationContext::updateNode($ctxId, 'collecting');
|
||||
@@ -783,6 +793,9 @@ class NormalBot
|
||||
} elseif ($dateMode === 'current_month') {
|
||||
$extraQuery['fecha_desde'] = date('Y-m-01');
|
||||
$extraQuery['fecha_hasta'] = date('Y-m-d');
|
||||
} elseif ($dateMode === 'last_month') {
|
||||
$extraQuery['fecha_desde'] = date('Y-m-01', strtotime('first day of last month'));
|
||||
$extraQuery['fecha_hasta'] = date('Y-m-t', strtotime('last day of last month'));
|
||||
}
|
||||
$extraQuery['telefono'] = $context['from'];
|
||||
$extraQuery['nombre'] = $context['name'] ?? '';
|
||||
@@ -871,7 +884,7 @@ class NormalBot
|
||||
WhatsAppSender::sendDocument($context['from'], $upload['media_id'], $phoneNumberId, $caption, $reportName);
|
||||
ConversationContext::reset($ctxId);
|
||||
|
||||
return self::sendText('✅ Reporte enviado.' . $nav, $context['from'], $company);
|
||||
return self::sendText('✅ Reporte generado.' . $nav, $context['from'], $company);
|
||||
}
|
||||
|
||||
// ── Public helpers ───────────────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user