feat: flow form editor shows all types with proper selects

PHP form for flows tab now renders:
- Type select grouped: Respuesta / Navegar / Acción directa / Formulario
- 💬 Texto fijo: textarea
- 📑 Mostrar menú: dropdown of company menus
-  Función especial: goodbye / api_report / IA / Agente
  - api_report expands: endpoint dropdown, period, filename, caption
- ✏️ Pedir dato (collect_input): prompt, meta_group, meta_key, next-node select
- 📋 Lista dinámica (dynamic_list): endpoint select, value/label fields, header, body, next-node
- 🚀 Enviar formulario (submit_form): endpoint, meta_group, filename, caption
Save handler in index.php reads all new field arrays and builds correct flow JSON.
flowTabChange()/flowFnTabChange() toggle sections on type/function change.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Lizandro Guarnizo
2026-06-28 14:03:18 -05:00
co-authored by Claude Sonnet 4.6
parent 316d9ce73e
commit eeda883bd9
2 changed files with 270 additions and 66 deletions
+195 -56
View File
@@ -2537,47 +2537,186 @@ HTML;
<!-- ─── 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>
<p style="font-size:12px;color:#7a8291;margin-bottom:14px">Acciones que se ejecutan cuando un comando o botón de menú es seleccionado.</p>
<div id="flowContainer">
HTML;
// Pre-build select options for menus and endpoints
$menuOptHtml = '<option value="">— Selecciona menú —</option>';
foreach (array_keys($menus) as $mk) {
$menuOptHtml .= '<option value="' . self::h($mk) . '">📑 ' . self::h($mk) . '</option>';
}
$epOptHtml = '<option value="">— Selecciona endpoint —</option>';
foreach ($companyEndpoints as $ep) {
$epOptHtml .= '<option value="' . self::h($ep['endpoint_key']) . '">' . self::h($ep['endpoint_key']) . '</option>';
}
$flowKeys = array_keys($flows);
$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>";
$ft = $flow['type'] ?? 'text';
$fMsg = self::h($flow['message'] ?? '');
$fFn = $flow['function'] ?? 'forward_to_ai';
$fMenu = $flow['menu'] ?? '';
$params = $flow['params'] ?? [];
$epKey = self::h($params['endpoint_key'] ?? $flow['endpoint_key'] ?? '');
$dMode = self::h($params['date_mode'] ?? '');
$cap = self::h($params['caption'] ?? $flow['caption'] ?? '');
$fname = self::h($params['filename'] ?? $flow['filename'] ?? '');
$prompt = self::h($flow['prompt'] ?? '');
$mGroup = self::h($flow['meta_group'] ?? '');
$mKey = self::h($flow['meta_key'] ?? '');
$nNode = $flow['next_node'] ?? '';
$srcEp = self::h($flow['source_endpoint_key'] ?? '');
$valF = self::h($flow['value_field'] ?? 'id');
$lblF = self::h($flow['label_field'] ?? 'nombre');
$header = self::h($flow['header'] ?? '');
$body = self::h($flow['body'] ?? '');
$secTit = self::h($flow['section_title'] ?? '');
// Build menu select with current selection
$menuSelHtml = '<option value="">— Selecciona menú —</option>';
foreach (array_keys($menus) as $mk) {
$sel = $fMenu === $mk ? ' selected' : '';
$menuSelHtml .= '<option value="' . self::h($mk) . '"' . $sel . '>📑 ' . self::h($mk) . '</option>';
}
// Build endpoint select with current selection
$epSelHtml = '<option value="">— Selecciona endpoint —</option>';
foreach ($companyEndpoints as $ep) {
$sel = $epKey === self::h($ep['endpoint_key']) ? ' selected' : '';
$epSelHtml .= '<option value="' . self::h($ep['endpoint_key']) . '"' . $sel . '>' . self::h($ep['endpoint_key']) . '</option>';
}
// For dynamic_list source endpoint
$dlEpSelHtml = '<option value="">— Selecciona endpoint —</option>';
foreach ($companyEndpoints as $ep) {
$sel = $srcEp === self::h($ep['endpoint_key']) ? ' selected' : '';
$dlEpSelHtml .= '<option value="' . self::h($ep['endpoint_key']) . '"' . $sel . '>' . self::h($ep['endpoint_key']) . '</option>';
}
// For submit_form endpoint
$sfEpSelHtml = '<option value="">— Selecciona endpoint —</option>';
foreach ($companyEndpoints as $ep) {
$sel = $epKey === self::h($ep['endpoint_key']) ? ' selected' : '';
$sfEpSelHtml .= '<option value="' . self::h($ep['endpoint_key']) . '"' . $sel . '>' . self::h($ep['endpoint_key']) . '</option>';
}
// Next-node select
$nnSelHtml = '<option value="">— Ninguno —</option>';
foreach ($flowKeys as $ofk) {
if ($ofk === $fk) continue;
$sel = $nNode === $ofk ? ' selected' : '';
$nnSelHtml .= '<option value="' . self::h($ofk) . '"' . $sel . '>' . self::h($ofk) . '</option>';
}
$showText = $ft === 'text' ? '' : 'display:none';
$showMenu = $ft === 'menu' ? '' : 'display:none';
$showFn = $ft === 'function' ? '' : 'display:none';
$showEp = ($ft === 'function' && $fFn === 'api_report') ? '' : 'display:none';
$showCollect = $ft === 'collect_input' ? '' : 'display:none';
$showDynList = $ft === 'dynamic_list' ? '' : 'display:none';
$showSubmit = $ft === 'submit_form' ? '' : 'display:none';
$fnOpts = '';
foreach (['goodbye' => '↩️ Salir (goodbye)', 'api_report' => '📄 Descargar informe', 'forward_to_ai' => '🤖 Escalar a IA', 'forward_to_agent' => '👤 Escalar a agente'] as $fv => $fl) {
$fnOpts .= '<option value="' . $fv . '"' . ($fFn === $fv ? ' selected' : '') . '>' . $fl . '</option>';
}
$dmOpts = '';
foreach (['' => 'Sin fecha', 'today' => 'Hoy', 'last_30' => 'Últimos 30 días'] as $dv => $dl) {
$dmOpts .= '<option value="' . $dv . '"' . ($dMode === $dv ? ' selected' : '') . '>' . $dl . '</option>';
}
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:180px;font-size:13px\"></span>
<button type=\"button\" class=\"del\" onclick=\"this.closest('.item-card').remove()\">✕</button>
</div>
<div class=\"form-row\" style=\"align-items:flex-start;gap:10px\">
<div class=\"form-group\" style=\"min-width:200px\">
<label>¿Qué hace?</label>
<select name=\"flow_type[]\" onchange=\"flowTabChange(this)\">
<optgroup label=\"Respuesta\">
<option value=\"text\"" . ($ft==='text'?' selected':'') . ">💬 Texto fijo</option>
</optgroup>
<optgroup label=\"Navegar\">
<option value=\"menu\"" . ($ft==='menu'?' selected':'') . ">📑 Mostrar menú</option>
</optgroup>
<optgroup label=\"Acción directa\">
<option value=\"function\"" . ($ft==='function'?' selected':'') . ">⚡ Función especial</option>
</optgroup>
<optgroup label=\"Formulario\">
<option value=\"collect_input\"" . ($ft==='collect_input'?' selected':'') . ">✏️ Pedir dato</option>
<option value=\"dynamic_list\"" . ($ft==='dynamic_list'?' selected':'') . ">📋 Lista dinámica</option>
<option value=\"submit_form\"" . ($ft==='submit_form'?' selected':'') . ">🚀 Enviar formulario</option>
</optgroup>
</select>
</div>
<div style=\"flex:1;display:flex;flex-direction:column;gap:10px\">
<!-- texto fijo -->
<div class=\"ftg-text\" style=\"{$showText}\">
<textarea name=\"flow_message[]\" rows=\"2\" placeholder=\"Texto que recibirá el usuario\">{$fMsg}</textarea>
</div>
<!-- menú -->
<div class=\"ftg-menu\" style=\"{$showMenu}\">
<label>Menú a mostrar</label>
<select name=\"flow_menu[]\">{$menuSelHtml}</select>
</div>
<!-- función especial -->
<div class=\"ftg-fn\" style=\"{$showFn}\">
<label>Función</label>
<select name=\"flow_function[]\" onchange=\"flowFnTabChange(this)\">{$fnOpts}</select>
<div class=\"ftg-ep\" style=\"{$showEp};margin-top:8px;display:flex;flex-direction:column;gap:8px\">
<div><label>Endpoint</label><select name=\"flow_ep_key[]\">{$epSelHtml}</select></div>
<div style=\"display:grid;grid-template-columns:1fr 1fr;gap:8px\">
<div><label>Periodo</label><select name=\"flow_date_mode[]\">{$dmOpts}</select></div>
<div><label>Nombre archivo</label><input type=\"text\" name=\"flow_filename[]\" value=\"{$fname}\" placeholder=\"reporte.pdf\"></div>
</div>
<div><label>Mensaje al enviar</label><input type=\"text\" name=\"flow_caption[]\" value=\"{$cap}\" placeholder=\"Aquí tienes el informe.\"></div>
</div>
</div>
<!-- collect_input -->
<div class=\"ftg-collect\" style=\"{$showCollect};display:flex;flex-direction:column;gap:8px\">
<div><label>Pregunta al usuario</label><input type=\"text\" name=\"flow_prompt[]\" value=\"{$prompt}\" placeholder=\"¿Desde qué fecha? (YYYY-MM-DD)\"></div>
<div style=\"display:grid;grid-template-columns:1fr 1fr;gap:8px\">
<div><label>Grupo del formulario</label><input type=\"text\" name=\"flow_meta_group[]\" value=\"{$mGroup}\" placeholder=\"form_produccion_lotes\"></div>
<div><label>Campo</label><input type=\"text\" name=\"flow_meta_key[]\" value=\"{$mKey}\" placeholder=\"desde\"></div>
</div>
<div><label>Flujo siguiente</label><select name=\"flow_next_node[]\">{$nnSelHtml}</select></div>
</div>
<!-- dynamic_list -->
<div class=\"ftg-dynlist\" style=\"{$showDynList};display:flex;flex-direction:column;gap:8px\">
<div><label>Endpoint que devuelve la lista</label><select name=\"flow_dl_ep[]\">{$dlEpSelHtml}</select></div>
<div style=\"display:grid;grid-template-columns:1fr 1fr;gap:8px\">
<div><label>Campo de valor (ID)</label><input type=\"text\" name=\"flow_val_field[]\" value=\"{$valF}\" placeholder=\"id\"></div>
<div><label>Campo etiqueta</label><input type=\"text\" name=\"flow_lbl_field[]\" value=\"{$lblF}\" placeholder=\"nombre\"></div>
</div>
<div style=\"display:grid;grid-template-columns:1fr 1fr;gap:8px\">
<div><label>Encabezado</label><input type=\"text\" name=\"flow_header[]\" value=\"{$header}\" placeholder=\"Selecciona la finca\"></div>
<div><label>Título sección</label><input type=\"text\" name=\"flow_sec_title[]\" value=\"{$secTit}\" placeholder=\"Fincas\"></div>
</div>
<div><label>Cuerpo del mensaje</label><input type=\"text\" name=\"flow_body[]\" value=\"{$body}\" placeholder=\"¿De qué finca necesitas el informe?\"></div>
<div style=\"display:grid;grid-template-columns:1fr 1fr;gap:8px\">
<div><label>Grupo formulario</label><input type=\"text\" name=\"flow_dl_meta_group[]\" value=\"{$mGroup}\" placeholder=\"form_prod_finca\"></div>
<div><label>Campo</label><input type=\"text\" name=\"flow_dl_meta_key[]\" value=\"{$mKey}\" placeholder=\"finca_id\"></div>
</div>
<div><label>Flujo siguiente</label><select name=\"flow_dl_next[]\">{$nnSelHtml}</select></div>
</div>
<!-- submit_form -->
<div class=\"ftg-submit\" style=\"{$showSubmit};display:flex;flex-direction:column;gap:8px\">
<div><label>Endpoint destino</label><select name=\"flow_sf_ep[]\">{$sfEpSelHtml}</select></div>
<div><label>Grupo del formulario</label><input type=\"text\" name=\"flow_sf_meta_group[]\" value=\"{$mGroup}\" placeholder=\"form_produccion_lotes\"></div>
<div style=\"display:grid;grid-template-columns:1fr 1fr;gap:8px\">
<div><label>Nombre archivo</label><input type=\"text\" name=\"flow_sf_filename[]\" value=\"{$fname}\" placeholder=\"reporte.pdf\"></div>
<div><label>Mensaje al enviar</label><input type=\"text\" name=\"flow_sf_caption[]\" value=\"{$cap}\" placeholder=\"Aquí tienes el informe.\"></div>
</div>
</div>
</div>
</div>
</div>";
$flowIdx++;
}
echo <<<HTML
@@ -2792,28 +2931,28 @@ function addRow(btn, mi, si) {
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);
// Flows — mostrar/ocultar secciones según tipo seleccionado
function flowTabChange(sel) {
const card = sel.closest('.item-card');
const v = sel.value;
const map = {
'ftg-text': v==='text', 'ftg-menu': v==='menu', 'ftg-fn': v==='function',
'ftg-collect': v==='collect_input', 'ftg-dynlist': v==='dynamic_list', 'ftg-submit': v==='submit_form'
};
for (const [cls, show] of Object.entries(map)) {
const el = card.querySelector('.' + cls);
if (el) el.style.display = show ? 'flex' : 'none';
}
if (v !== 'function') {
const ep = card.querySelector('.ftg-ep');
if (ep) ep.style.display = 'none';
}
}
function flowTypeChange(sel) {
function flowFnTabChange(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';
const ep = card.querySelector('.ftg-ep');
if (ep) ep.style.display = sel.value === 'api_report' ? 'flex' : 'none';
}
function updatePreview() {