feat: expandir tab Por Categoría en bot-config

- Toggle "Pedir finca al inicio" por categoría → maneja greeting/greeting_flow
- Campo editable de greeting_flow (pre-llena ask_finca al activar el toggle)
- Selector de menú inicial (greeting_menu, visible cuando NO pide finca)
- Selector de flujo de respaldo (fallback_flow) por categoría
- Datalist con flows y menús disponibles para autocompletar
- save-per-type actualiza greeting, greeting_flow, greeting_menu y fallback_flow
  preservando menus/flows/commands del seed en cada categoría
- Cat 1: sin finca, configura greeting_menu desde admin
- Cat 2/3: pide finca activado, greeting_flow = ask_finca

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Lizandro Guarnizo
2026-07-30 23:14:49 -05:00
co-authored by Claude Sonnet 4.6
parent 0ddab6adcf
commit 909c6be32f
2 changed files with 138 additions and 27 deletions
+103 -18
View File
@@ -3193,28 +3193,87 @@ HTML;
// Per-category config HTML
$perTypeConfig = $config['per_type'] ?? [];
$menuKeysList = array_keys($menus);
$catLabels = ['1' => 'Categoría 1 — Solo reporta', '2' => 'Categoría 2 — Solo recibe informes', '3' => 'Categoría 3 — Reporta y recibe'];
$flowKeysList = array_keys($config['flows'] ?? []);
// Include per_type menus/flows in the option lists
foreach ($config['per_type'] ?? [] as $pt) {
foreach (array_keys($pt['menus'] ?? []) as $k) if (!in_array($k, $menuKeysList)) $menuKeysList[] = $k;
foreach (array_keys($pt['flows'] ?? []) as $k) if (!in_array($k, $flowKeysList)) $flowKeysList[] = $k;
}
$catMeta = [
'1' => ['label' => 'Categoría 1 — Solo reporta', 'icon' => '📤', 'bg' => '#f0fdf4', 'bd' => '#86efac', 'clr' => '#166534'],
'2' => ['label' => 'Categoría 2 — Solo recibe informes', 'icon' => '📥', 'bg' => '#eff6ff', 'bd' => '#93c5fd', 'clr' => '#1e40af'],
'3' => ['label' => 'Categoría 3 — Reporta y recibe', 'icon' => '🔄', 'bg' => '#fdf4ff', 'bd' => '#e879f9', 'clr' => '#701a75'],
];
$categoryTabHtml = '';
// Build shared option lists
$menuOptsBase = '<option value="">— Sin menú —</option>';
foreach ($menuKeysList as $mk) $menuOptsBase .= '<option value="' . self::h($mk) . '">' . self::h($mk) . '</option>';
$flowOptsBase = '<option value="">— Ninguno —</option>';
foreach ($menuKeysList as $mk) $flowOptsBase .= '<option value="' . self::h($mk) . '">📑 ' . self::h($mk) . '</option>';
foreach ($flowKeysList as $fk) $flowOptsBase .= '<option value="' . self::h($fk) . '">🔀 ' . self::h($fk) . '</option>';
foreach ([1, 2, 3] as $cat) {
$catCfg = $perTypeConfig[(string)$cat] ?? [];
$m = $catMeta[(string)$cat];
$greetingVal = array_key_exists('greeting', $catCfg) ? $catCfg['greeting'] : null;
$askFinca = $greetingVal !== null;
$chk = $askFinca ? ' checked' : '';
$greetFlow = self::h($catCfg['greeting_flow'] ?? ($askFinca ? 'ask_finca' : ''));
$fallback = $catCfg['fallback_flow'] ?? '';
$selMenu = $catCfg['greeting_menu'] ?? '';
$opts = '<option value="">— Usa configuración global —</option>';
foreach ($menuKeysList as $mk) {
$s = $selMenu === $mk ? ' selected' : '';
$opts .= '<option value="' . self::h($mk) . '"' . $s . '>' . self::h($mk) . '</option>';
}
$label = $catLabels[(string)$cat];
// greeting_menu selector (insert selected attr)
$menuOpts = str_replace(
'value="' . self::h($selMenu) . '"',
'value="' . self::h($selMenu) . '" selected',
$menuOptsBase
);
// fallback selector (insert selected attr)
$fbOpts = str_replace(
'value="' . self::h($fallback) . '"',
'value="' . self::h($fallback) . '" selected',
$flowOptsBase
);
$greetFlowStyle = $askFinca ? '' : 'display:none';
$greetMenuStyle = $askFinca ? 'display:none' : '';
$label = $m['label']; $icon = $m['icon']; $bg = $m['bg']; $bd = $m['bd']; $clr = $m['clr'];
$categoryTabHtml .= <<<CATHTML
<div style="background:#f8f9fd;border:1px solid #eef1f5;border-radius:10px;padding:18px;margin-bottom:16px">
<div style="font-weight:700;font-size:13px;color:#0b3d91;margin-bottom:12px">{$label}</div>
<div class="form-group" style="margin-bottom:0">
<label>Menú de bienvenida (greeting menu)</label>
<select name="per_type_menu[{$cat}]" style="width:100%;padding:9px 12px;border:1px solid #e2e5ea;border-radius:8px;font-size:13px">
{$opts}
</select>
<div style="font-size:11px;color:#7a8291;margin-top:4px">Menú que se muestra cuando el usuario escribe por primera vez o sin contexto activo</div>
</div>
</div>
<div style="background:{$bg};border:1px solid {$bd};border-radius:10px;padding:18px;margin-bottom:16px">
<div style="display:flex;align-items:center;gap:8px;margin-bottom:14px">
<span style="font-size:20px">{$icon}</span>
<span style="font-weight:700;font-size:13px;color:{$clr}">{$label}</span>
</div>
<div class="form-group" style="margin-bottom:12px">
<label style="display:flex;align-items:center;gap:8px;font-weight:500">
<input type="checkbox" name="per_type_ask_finca[{$cat}]" value="1"{$chk} onchange="toggleAskFinca(this,{$cat})">
Pedir selección de finca al inicio
</label>
<div style="font-size:11px;color:#7a8291;margin-top:3px">Al activarlo, el bot solicita la finca antes de mostrar el menú principal</div>
</div>
<div id="cat-greet-flow-{$cat}" style="{$greetFlowStyle}" class="form-group" style="margin-bottom:12px">
<label>Flujo de bienvenida</label>
<input type="text" name="per_type_greeting_flow[{$cat}]" value="{$greetFlow}"
list="ptFlowsDatalist" placeholder="ej: ask_finca"
style="width:100%;padding:8px 12px;border:1px solid #e2e5ea;border-radius:8px;font-size:13px">
<div style="font-size:11px;color:#7a8291;margin-top:3px">Flow que se ejecuta en el primer mensaje del usuario</div>
</div>
<div id="cat-greet-menu-{$cat}" style="{$greetMenuStyle}" class="form-group" style="margin-bottom:12px">
<label>Menú inicial</label>
<select name="per_type_menu[{$cat}]" style="width:100%;padding:9px 12px;border:1px solid #e2e5ea;border-radius:8px;font-size:13px">
{$menuOpts}
</select>
<div style="font-size:11px;color:#7a8291;margin-top:3px">Menú que se muestra cuando el usuario no tiene contexto activo</div>
</div>
<div class="form-group" style="margin-bottom:0">
<label>Flujo de respaldo (fallback)</label>
<select name="per_type_fallback[{$cat}]" style="width:100%;padding:9px 12px;border:1px solid #e2e5ea;border-radius:8px;font-size:13px">
{$fbOpts}
</select>
<div style="font-size:11px;color:#7a8291;margin-top:3px">Se muestra cuando el usuario envía algo que no corresponde a ninguna opción</div>
</div>
</div>
CATHTML;
}
$aiModelMini = $aiModelVal === 'gpt-4o-mini' ? ' selected' : '';
@@ -3643,7 +3702,13 @@ HTML;
<!-- ─── POR CATEGORÍA ────────────────────────────────────────────────── -->
<div class="tab-content" id="tab-categories">
<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>
<p style="font-size:12px;color:#7a8291;margin-bottom:16px">Define el comportamiento inicial de cada categoría de usuario: si pide finca, qué menú muestra y qué hacer ante texto no reconocido.</p>
<datalist id="ptFlowsDatalist">
HTML;
foreach ($menuKeysList as $mk) echo '<option value="' . self::h($mk) . '">📑 Menú</option>' . "\n";
foreach ($flowKeysList as $fk) echo '<option value="' . self::h($fk) . '">🔀 Flow</option>' . "\n";
echo <<<HTML
</datalist>
{$categoryTabHtml}
<button type="button" id="savePerTypeBtn" onclick="savePerType()" class="btn-primary" style="margin-top:8px">💾 Guardar categorías</button>
</div>
@@ -3846,13 +3911,33 @@ HTML;
const CONFIG = {$botConfigJson};
const ENDPOINTS = {$botEndpointsJson};
function toggleAskFinca(chk, cat) {
var flowDiv = document.getElementById('cat-greet-flow-' + cat);
var menuDiv = document.getElementById('cat-greet-menu-' + cat);
if (chk.checked) {
if (flowDiv) flowDiv.style.display = '';
if (menuDiv) menuDiv.style.display = 'none';
var gf = document.querySelector('[name="per_type_greeting_flow[' + cat + ']"]');
if (gf && gf.value === '') gf.value = 'ask_finca';
} else {
if (flowDiv) flowDiv.style.display = 'none';
if (menuDiv) menuDiv.style.display = '';
}
}
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 chk = document.querySelector('[name="per_type_ask_finca[' + cat + ']"]');
fd.append('per_type_ask_finca[' + cat + ']', chk && chk.checked ? '1' : '0');
var gf = document.querySelector('[name="per_type_greeting_flow[' + cat + ']"]');
if (gf) fd.append('per_type_greeting_flow[' + cat + ']', gf.value);
var sel = document.querySelector('[name="per_type_menu[' + cat + ']"]');
if (sel) fd.append('per_type_menu[' + cat + ']', sel.value);
var fb = document.querySelector('[name="per_type_fallback[' + cat + ']"]');
if (fb) fd.append('per_type_fallback[' + cat + ']', fb.value);
});
var btn = document.getElementById('savePerTypeBtn');
if (btn) { btn.textContent = 'Guardando...'; btn.disabled = true; }
+35 -9
View File
@@ -660,22 +660,48 @@ $routes = [
$config = json_decode($company['config_json'] ?? '{}', true) ?: [];
$rawPt = $config['per_type'] ?? [];
$rawPt = $config['per_type'] ?? [];
$perType = (is_array($rawPt) && count(array_filter(array_keys($rawPt), 'is_string')) > 0)
? $rawPt : [];
$perTypeMenus = $_POST['per_type_menu'] ?? [];
$ptAskFinca = $_POST['per_type_ask_finca'] ?? [];
$ptGreetFlow = $_POST['per_type_greeting_flow'] ?? [];
$ptMenus = $_POST['per_type_menu'] ?? [];
$ptFallback = $_POST['per_type_fallback'] ?? [];
foreach ([1, 2, 3] as $cat) {
$mk = trim($perTypeMenus[$cat] ?? '');
$key = (string)$cat;
if ($mk !== '') {
$perType[$key] = array_merge($perType[$key] ?? [], ['greeting_menu' => $mk]);
$key = (string)$cat;
$current = $perType[$key] ?? []; // preserve menus/flows/commands from seed
$askFinca = ($ptAskFinca[$cat] ?? '0') === '1';
if ($askFinca) {
$gf = trim($ptGreetFlow[$cat] ?? 'ask_finca');
$current['greeting'] = '';
$current['greeting_flow'] = $gf !== '' ? $gf : 'ask_finca';
unset($current['greeting_menu']);
} else {
unset($perType[$key]['greeting_menu']);
if (empty($perType[$key])) unset($perType[$key]);
$current['greeting'] = null;
unset($current['greeting_flow']);
$mk = trim($ptMenus[$cat] ?? '');
if ($mk !== '') {
$current['greeting_menu'] = $mk;
} else {
unset($current['greeting_menu']);
}
}
$fb = trim($ptFallback[$cat] ?? '');
if ($fb !== '') {
$current['fallback_flow'] = $fb;
} else {
unset($current['fallback_flow']);
}
$perType[$key] = $current;
}
$config['per_type'] = empty($perType) ? new stdClass() : $perType;
$config['per_type'] = $perType;
CompanyRepository::save(['id' => $companyId, 'config_json' => json_encode($config, JSON_UNESCAPED_UNICODE)]);
echo json_encode(['ok' => true]);