fix(flows): CSS display conflict, addFlowCard, endpoint CRUD, menu re-send, URL vars
- Fix display:none vs display:flex conflict in PHP flow card rendering (sections were always visible)
- Add addFlowCard() JS function that properly inserts new flow cards into the PHP form
- Fix flowTabChange() to use block vs flex display correctly per section type
- Rewrite endpoints tab as full CRUD table (add/edit/delete) with name, direction, active toggle
- Add companyEndpointDelete() handler and route
- Update companyEndpointSave() to handle ep_id, name, params, is_active fields
- Add company_endpoints columns: name, params
- Add saveEpRow/testEpRow/deleteEpRow/addNewEp JS functions
- NormalBot: set __greeted sentinel after showing greeting menu to prevent re-send on every message
- NormalBot: add substituteUrlVars() to replace {param} tokens in endpoint URLs with collected metadata
- DB: submenu_ciclos_sanidad (4 items) → list; prod_kilos_finca endpoint linked; all >3-button menus → list
- DB: fincas_list endpoint activated
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
eeda883bd9
commit
2c0ac04993
+318
-72
@@ -1094,42 +1094,117 @@ HTML;
|
||||
</div>
|
||||
HTML;
|
||||
|
||||
// Endpoints
|
||||
$epRows = db()->prepare("SELECT endpoint_key, url, method, last_response, last_called_at FROM company_endpoints WHERE company_id=?");
|
||||
$epRows->execute([$id]);
|
||||
$epMap = [];
|
||||
foreach ($epRows->fetchAll(\PDO::FETCH_ASSOC) as $r) $epMap[$r['endpoint_key']] = $r;
|
||||
// Endpoints — CRUD dinámico
|
||||
$epStmt = db()->prepare("SELECT id,endpoint_key,name,params,direction,url,method,is_active,last_called_at FROM company_endpoints WHERE company_id=? ORDER BY direction,endpoint_key");
|
||||
$epStmt->execute([$id]);
|
||||
$allEps = $epStmt->fetchAll(\PDO::FETCH_ASSOC);
|
||||
|
||||
$catalog = self::endpointCatalog();
|
||||
$renderEpSection = function(string $dir, string $dirLabel, string $badge) use ($catalog, $epMap, $id): string {
|
||||
$html = "<h3 style='font-size:13px;font-weight:600;color:#111827;margin:0 0 12px'>{$badge} {$dirLabel}</h3>";
|
||||
foreach ($catalog[$dir] as $key => $label) {
|
||||
$saved = $epMap[$key] ?? [];
|
||||
$url = self::h($saved['url'] ?? '');
|
||||
$meth = $saved['method'] ?? 'GET';
|
||||
$lastAt = $saved['last_called_at'] ? '<span style="font-size:11px;color:#9ca3af">' . self::h($saved['last_called_at']) . '</span>' : '';
|
||||
$mSel = fn($v) => $meth === $v ? 'selected' : '';
|
||||
$html .= <<<EP
|
||||
<div style="border:1px solid #e5e7eb;border-radius:8px;padding:14px;margin-bottom:10px">
|
||||
<div style="font-size:13px;font-weight:500;color:#111827;margin-bottom:8px">{$label} {$lastAt}</div>
|
||||
<div style="display:flex;gap:8px;flex-wrap:wrap;align-items:flex-end">
|
||||
<select id="m_{$key}" style="width:90px;padding:7px 8px;border:1px solid #e5e7eb;border-radius:6px;font-size:13px">
|
||||
$epRows = '';
|
||||
foreach ($allEps as $ep) {
|
||||
$epKey = self::h($ep['endpoint_key']);
|
||||
$epName = self::h($ep['name'] ?? '');
|
||||
$epUrl = self::h($ep['url'] ?? '');
|
||||
$epMeth = $ep['method'] ?? 'GET';
|
||||
$epDir = $ep['direction'] ?? 'download';
|
||||
$epParams= self::h($ep['params'] ?? '');
|
||||
$epAct = $ep['is_active'] ? 'checked' : '';
|
||||
$lastAt = $ep['last_called_at'] ? '<span style="font-size:11px;color:#9ca3af">' . self::h($ep['last_called_at']) . '</span>' : '';
|
||||
$dirBadge = $epDir === 'upload'
|
||||
? '<span class="badge badge-blue" style="font-size:10px">↑ upload</span>'
|
||||
: '<span class="badge badge-green" style="font-size:10px">↓ download</span>';
|
||||
$mSel = fn($v) => $epMeth === $v ? 'selected' : '';
|
||||
$dSel = fn($v) => $epDir === $v ? 'selected' : '';
|
||||
$epId = (int)$ep['id'];
|
||||
$epRows .= <<<ROW
|
||||
<tr id="ep-row-{$epId}">
|
||||
<td style="min-width:120px">
|
||||
<input type="text" value="{$epKey}" id="epk_{$epId}" style="width:100%;font-family:monospace;font-size:12px;padding:4px 6px;border:1px solid #e5e7eb;border-radius:4px">
|
||||
</td>
|
||||
<td>
|
||||
<input type="text" value="{$epName}" id="epn_{$epId}" placeholder="Nombre legible" style="width:100%;font-size:12px;padding:4px 6px;border:1px solid #e5e7eb;border-radius:4px">
|
||||
</td>
|
||||
<td>
|
||||
<select id="epd_{$epId}" style="font-size:12px;padding:4px;border:1px solid #e5e7eb;border-radius:4px">
|
||||
<option value="download" {$dSel('download')}>↓ Download</option>
|
||||
<option value="upload" {$dSel('upload')}>↑ Upload</option>
|
||||
<option value="list" {$dSel('list')}>📋 List</option>
|
||||
</select>
|
||||
</td>
|
||||
<td>
|
||||
<select id="epm_{$epId}" style="font-size:12px;padding:4px;border:1px solid #e5e7eb;border-radius:4px">
|
||||
<option {$mSel('GET')}>GET</option><option {$mSel('POST')}>POST</option>
|
||||
</select>
|
||||
<input type="text" id="u_{$key}" value="{$url}" placeholder="https://..." style="flex:1;min-width:200px;padding:7px 10px;border:1px solid #e5e7eb;border-radius:6px;font-size:13px">
|
||||
<button class="btn-secondary" onclick="saveEp({$id},'{$key}','{$dir}')">Guardar</button>
|
||||
<button class="btn-sm" onclick="testEp({$id},'{$key}')">Probar</button>
|
||||
</td>
|
||||
<td style="min-width:200px">
|
||||
<input type="text" value="{$epUrl}" id="epu_{$epId}" placeholder="https://..." style="width:100%;font-size:12px;padding:4px 6px;border:1px solid #e5e7eb;border-radius:4px">
|
||||
<div style="font-size:10px;color:#9ca3af;margin-top:2px">Usa {'{var}'} para variables. {$lastAt}</div>
|
||||
</td>
|
||||
<td><input type="checkbox" {$epAct} id="epa_{$epId}" title="Activo"></td>
|
||||
<td style="white-space:nowrap">
|
||||
<button class="btn-sm" onclick="saveEpRow({$id},{$epId})">💾</button>
|
||||
<button class="btn-sm" onclick="testEpRow({$id},{$epId})">▶</button>
|
||||
<button class="btn-danger-sm" onclick="deleteEpRow({$id},{$epId})">✕</button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr id="ep-resp-{$epId}" style="display:none"><td colspan="7" style="padding:8px;font-size:11px;color:#374151;background:#f9fafb;border-radius:6px"></td></tr>
|
||||
ROW;
|
||||
}
|
||||
if (!$epRows) $epRows = '<tr><td colspan="7" class="empty">Sin endpoints. Agrega uno abajo.</td></tr>';
|
||||
|
||||
$epHtml = <<<EP
|
||||
<div class="card-h" style="margin-bottom:12px">🔌 Endpoints API</div>
|
||||
<p style="font-size:12px;color:#7a8291;margin-bottom:14px">
|
||||
Define los endpoints del ERP. Usa <code>{'{variable}'}</code> en la URL para variables que el bot pedirá al usuario.<br>
|
||||
Ejemplo: <code>?peticion=produccion&desde={'{desde}'}&hasta={'{hasta}'}</code>
|
||||
</p>
|
||||
<div class="card" style="overflow-x:auto">
|
||||
<table style="width:100%;min-width:700px">
|
||||
<thead>
|
||||
<tr style="font-size:12px;text-align:left">
|
||||
<th>Key</th><th>Nombre</th><th>Dirección</th><th>Método</th><th>URL</th><th>Activo</th><th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="epTableBody">{$epRows}</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div id="epMsg" style="margin:8px 0"></div>
|
||||
|
||||
<div class="card" style="margin-top:16px">
|
||||
<div class="card-h">+ Agregar endpoint</div>
|
||||
<div class="card-b">
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label>Key <span style="font-size:10px;color:#9ca3af">(sin espacios, ej: fincas_list)</span></label>
|
||||
<input type="text" id="newEpKey" placeholder="mi_endpoint" style="font-family:monospace">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Nombre</label>
|
||||
<input type="text" id="newEpName" placeholder="Lista de fincas">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label>Dirección</label>
|
||||
<select id="newEpDir">
|
||||
<option value="download">↓ Download (ERP → WA)</option>
|
||||
<option value="upload">↑ Upload (WA → ERP)</option>
|
||||
<option value="list">📋 Lista dinámica</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Método</label>
|
||||
<select id="newEpMethod"><option>GET</option><option>POST</option></select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>URL</label>
|
||||
<input type="text" id="newEpUrl" placeholder="https://app.palmas360.com/...?peticion=xxx">
|
||||
</div>
|
||||
<div id="newEpMsg" style="margin-bottom:8px"></div>
|
||||
<button class="btn-primary" onclick="addNewEp({$id})">Agregar</button>
|
||||
</div>
|
||||
<div id="ep_resp_{$key}" style="margin-top:8px;display:none"></div>
|
||||
</div>
|
||||
EP;
|
||||
}
|
||||
return $html;
|
||||
};
|
||||
|
||||
$epHtml = $renderEpSection('upload', 'Subir información (WhatsApp → ERP)', '<span class="badge badge-blue">↑ Upload</span>')
|
||||
. '<div style="margin:20px 0;border-top:1px solid #e5e7eb"></div>'
|
||||
. $renderEpSection('download', 'Bajar información (ERP → WhatsApp)', '<span class="badge badge-green">↓ Download</span>');
|
||||
}
|
||||
|
||||
$tabsHtml = $isEdit ? <<<HTML
|
||||
@@ -1290,35 +1365,73 @@ async function deletePhone(pid, btn) {
|
||||
else { alert(j.error); btn.disabled=false; }
|
||||
}
|
||||
|
||||
async function saveEp(cid, key, dir) {
|
||||
const url = document.getElementById('u_'+key).value.trim();
|
||||
const meth = document.getElementById('m_'+key).value;
|
||||
async function saveEpRow(cid, epId) {
|
||||
const fd=new FormData();
|
||||
fd.append('company_id',cid); fd.append('endpoint_key',key);
|
||||
fd.append('direction',dir); fd.append('url',url); fd.append('method',meth);
|
||||
fd.append('company_id', cid);
|
||||
fd.append('ep_id', epId);
|
||||
fd.append('endpoint_key', document.getElementById('epk_'+epId).value.trim());
|
||||
fd.append('name', document.getElementById('epn_'+epId).value.trim());
|
||||
fd.append('direction', document.getElementById('epd_'+epId).value);
|
||||
fd.append('method', document.getElementById('epm_'+epId).value);
|
||||
fd.append('url', document.getElementById('epu_'+epId).value.trim());
|
||||
fd.append('is_active', document.getElementById('epa_'+epId).checked ? '1' : '0');
|
||||
const msg=document.getElementById('epMsg');
|
||||
const r=await fetch('/admin/company/endpoint/save',{method:'POST',body:fd});
|
||||
const j=await r.json();
|
||||
const box=document.getElementById('ep_resp_'+key);
|
||||
box.style.display='';
|
||||
box.innerHTML=j.ok ? '<span style="color:#166534;font-size:12px">✓ Guardado</span>' : '<span style="color:#991b1b;font-size:12px">'+j.error+'</span>';
|
||||
setTimeout(()=>box.style.display='none', 2000);
|
||||
msg.innerHTML=j.ok
|
||||
? '<div class="toast toast-success">✓ Guardado</div>'
|
||||
: '<div class="toast toast-error">'+j.error+'</div>';
|
||||
setTimeout(()=>msg.innerHTML='', 2000);
|
||||
}
|
||||
|
||||
async function testEp(cid, key) {
|
||||
const url=document.getElementById('u_'+key).value.trim();
|
||||
const meth=document.getElementById('m_'+key).value;
|
||||
const box=document.getElementById('ep_resp_'+key);
|
||||
box.style.display=''; box.innerHTML='<span style="color:#6b7280;font-size:12px">Probando...</span>';
|
||||
async function testEpRow(cid, epId) {
|
||||
const url=document.getElementById('epu_'+epId).value.trim();
|
||||
const meth=document.getElementById('epm_'+epId).value;
|
||||
const respRow=document.getElementById('ep-resp-'+epId);
|
||||
const respCell=respRow.querySelector('td');
|
||||
respRow.style.display=''; respCell.textContent='Probando...';
|
||||
const fd=new FormData();
|
||||
fd.append('company_id',cid); fd.append('endpoint_key',key);
|
||||
fd.append('company_id',cid); fd.append('ep_id',epId);
|
||||
fd.append('endpoint_key', document.getElementById('epk_'+epId).value.trim());
|
||||
fd.append('url',url); fd.append('method',meth);
|
||||
const r=await fetch('/admin/company/endpoint/test',{method:'POST',body:fd});
|
||||
const j=await r.json();
|
||||
if (j.ok) {
|
||||
const preview=JSON.stringify(j.response,null,2).substring(0,800);
|
||||
box.innerHTML='<pre style="font-size:11px;max-height:200px;overflow-y:auto;margin:0">'+preview+'</pre>';
|
||||
respCell.innerHTML='<pre style="font-size:11px;max-height:180px;overflow-y:auto;margin:0">'+JSON.stringify(j.response,null,2).substring(0,1000)+'</pre>';
|
||||
} else {
|
||||
box.innerHTML='<div class="toast toast-error" style="margin:0;font-size:12px">'+j.error+'</div>';
|
||||
respCell.innerHTML='<span style="color:#991b1b">'+j.error+'</span>';
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteEpRow(cid, epId) {
|
||||
if (!confirm('¿Eliminar este endpoint?')) return;
|
||||
const fd=new FormData(); fd.append('company_id',cid); fd.append('ep_id',epId);
|
||||
const r=await fetch('/admin/company/endpoint/delete',{method:'POST',body:fd});
|
||||
const j=await r.json();
|
||||
if (j.ok) {
|
||||
document.getElementById('ep-row-'+epId)?.remove();
|
||||
document.getElementById('ep-resp-'+epId)?.remove();
|
||||
} else alert(j.error);
|
||||
}
|
||||
|
||||
async function addNewEp(cid) {
|
||||
const key = document.getElementById('newEpKey').value.trim();
|
||||
const name = document.getElementById('newEpName').value.trim();
|
||||
const dir = document.getElementById('newEpDir').value;
|
||||
const meth = document.getElementById('newEpMethod').value;
|
||||
const url = document.getElementById('newEpUrl').value.trim();
|
||||
const msg = document.getElementById('newEpMsg');
|
||||
if (!key || !url) { msg.innerHTML='<div class="toast toast-error">Key y URL son requeridos</div>'; return; }
|
||||
const fd=new FormData();
|
||||
fd.append('company_id',cid); fd.append('endpoint_key',key); fd.append('name',name);
|
||||
fd.append('direction',dir); fd.append('method',meth); fd.append('url',url); fd.append('is_active','1');
|
||||
const r=await fetch('/admin/company/endpoint/save',{method:'POST',body:fd});
|
||||
const j=await r.json();
|
||||
if (j.ok) {
|
||||
msg.innerHTML='<div class="toast toast-success">✓ Agregado</div>';
|
||||
setTimeout(()=>location.reload(), 800);
|
||||
} else {
|
||||
msg.innerHTML='<div class="toast toast-error">'+j.error+'</div>';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -1512,20 +1625,45 @@ HTML;
|
||||
SessionAuth::require();
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
$companyId = (int)($_POST['company_id'] ?? 0);
|
||||
$key = preg_replace('/[^a-z0-9_]/', '', $_POST['endpoint_key'] ?? '');
|
||||
$direction = in_array($_POST['direction'] ?? '', ['upload','download']) ? $_POST['direction'] : null;
|
||||
$url = trim($_POST['url'] ?? '');
|
||||
$method = in_array(strtoupper($_POST['method'] ?? 'GET'), ['GET','POST']) ? strtoupper($_POST['method']) : 'GET';
|
||||
$companyId = (int)($_POST['company_id'] ?? 0);
|
||||
$epId = (int)($_POST['ep_id'] ?? 0);
|
||||
$key = preg_replace('/[^a-z0-9_]/', '', strtolower($_POST['endpoint_key'] ?? ''));
|
||||
$name = trim($_POST['name'] ?? '');
|
||||
$direction = in_array($_POST['direction'] ?? '', ['upload','download','list']) ? $_POST['direction'] : 'download';
|
||||
$url = trim($_POST['url'] ?? '');
|
||||
$method = in_array(strtoupper($_POST['method'] ?? 'GET'), ['GET','POST']) ? strtoupper($_POST['method']) : 'GET';
|
||||
$isActive = ($_POST['is_active'] ?? '0') === '1' ? 1 : 0;
|
||||
$params = trim($_POST['params'] ?? '');
|
||||
|
||||
if ($companyId <= 0 || $key === '' || $direction === null) {
|
||||
echo json_encode(['ok' => false, 'error' => 'Datos inválidos']);
|
||||
exit;
|
||||
if ($companyId <= 0 || $key === '') {
|
||||
echo json_encode(['ok' => false, 'error' => 'Datos inválidos']); exit;
|
||||
}
|
||||
|
||||
try {
|
||||
db()->prepare("INSERT INTO company_endpoints (company_id, endpoint_key, direction, url, method) VALUES (?,?,?,?,?) ON DUPLICATE KEY UPDATE url=VALUES(url), method=VALUES(method)")
|
||||
->execute([$companyId, $key, $direction, $url, $method]);
|
||||
if ($epId > 0) {
|
||||
db()->prepare("UPDATE company_endpoints SET endpoint_key=?,name=?,direction=?,url=?,method=?,is_active=?,params=? WHERE id=? AND company_id=?")
|
||||
->execute([$key, $name, $direction, $url, $method, $isActive, $params ?: null, $epId, $companyId]);
|
||||
} else {
|
||||
db()->prepare("INSERT INTO company_endpoints (company_id,endpoint_key,name,direction,url,method,is_active,params) VALUES (?,?,?,?,?,?,?,?) ON DUPLICATE KEY UPDATE name=VALUES(name),direction=VALUES(direction),url=VALUES(url),method=VALUES(method),is_active=VALUES(is_active),params=VALUES(params)")
|
||||
->execute([$companyId, $key, $name, $direction, $url, $method, $isActive, $params ?: null]);
|
||||
}
|
||||
echo json_encode(['ok' => true]);
|
||||
} catch (\PDOException $e) {
|
||||
echo json_encode(['ok' => false, 'error' => $e->getMessage()]);
|
||||
}
|
||||
exit;
|
||||
}
|
||||
|
||||
// ─── POST /admin/company/endpoint/delete ────────────────────────────────
|
||||
public static function companyEndpointDelete(): void
|
||||
{
|
||||
SessionAuth::require();
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
$companyId = (int)($_POST['company_id'] ?? 0);
|
||||
$epId = (int)($_POST['ep_id'] ?? 0);
|
||||
if ($companyId <= 0 || $epId <= 0) { echo json_encode(['ok'=>false,'error'=>'Datos inválidos']); exit; }
|
||||
try {
|
||||
db()->prepare("DELETE FROM company_endpoints WHERE id=? AND company_id=?")->execute([$epId, $companyId]);
|
||||
echo json_encode(['ok' => true]);
|
||||
} catch (\PDOException $e) {
|
||||
echo json_encode(['ok' => false, 'error' => $e->getMessage()]);
|
||||
@@ -2608,10 +2746,18 @@ HTML;
|
||||
$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';
|
||||
$showEp = ($ft === 'function' && $fFn === 'api_report')
|
||||
? 'display:flex;flex-direction:column;gap:8px;margin-top:8px'
|
||||
: 'display:none';
|
||||
$showCollect = $ft === 'collect_input'
|
||||
? 'display:flex;flex-direction:column;gap:8px'
|
||||
: 'display:none';
|
||||
$showDynList = $ft === 'dynamic_list'
|
||||
? 'display:flex;flex-direction:column;gap:8px'
|
||||
: 'display:none';
|
||||
$showSubmit = $ft === 'submit_form'
|
||||
? 'display:flex;flex-direction:column;gap:8px'
|
||||
: '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) {
|
||||
@@ -2665,7 +2811,7 @@ HTML;
|
||||
<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 class=\"ftg-ep\" style=\"{$showEp}\">
|
||||
<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>
|
||||
@@ -2676,7 +2822,7 @@ HTML;
|
||||
</div>
|
||||
|
||||
<!-- collect_input -->
|
||||
<div class=\"ftg-collect\" style=\"{$showCollect};display:flex;flex-direction:column;gap:8px\">
|
||||
<div class=\"ftg-collect\" style=\"{$showCollect}\">
|
||||
<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>
|
||||
@@ -2686,7 +2832,7 @@ HTML;
|
||||
</div>
|
||||
|
||||
<!-- dynamic_list -->
|
||||
<div class=\"ftg-dynlist\" style=\"{$showDynList};display:flex;flex-direction:column;gap:8px\">
|
||||
<div class=\"ftg-dynlist\" style=\"{$showDynList}\">
|
||||
<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>
|
||||
@@ -2705,7 +2851,7 @@ HTML;
|
||||
</div>
|
||||
|
||||
<!-- submit_form -->
|
||||
<div class=\"ftg-submit\" style=\"{$showSubmit};display:flex;flex-direction:column;gap:8px\">
|
||||
<div class=\"ftg-submit\" style=\"{$showSubmit}\">
|
||||
<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\">
|
||||
@@ -2721,7 +2867,7 @@ HTML;
|
||||
}
|
||||
echo <<<HTML
|
||||
</div>
|
||||
<button type="button" class="add-btn" onclick="addNewFlow()">+ Agregar flujo</button>
|
||||
<button type="button" class="add-btn" onclick="addFlowCard()">+ Agregar flujo</button>
|
||||
</div>
|
||||
|
||||
<!-- ─── POR CATEGORÍA ────────────────────────────────────────────────── -->
|
||||
@@ -2935,13 +3081,18 @@ function addRow(btn, mi, si) {
|
||||
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'
|
||||
// flex sections need display:flex; block sections use '' (default block)
|
||||
const sections = {
|
||||
'ftg-text': {show: v==='text', flex: false},
|
||||
'ftg-menu': {show: v==='menu', flex: false},
|
||||
'ftg-fn': {show: v==='function', flex: false},
|
||||
'ftg-collect': {show: v==='collect_input', flex: true},
|
||||
'ftg-dynlist': {show: v==='dynamic_list', flex: true},
|
||||
'ftg-submit': {show: v==='submit_form', flex: true},
|
||||
};
|
||||
for (const [cls, show] of Object.entries(map)) {
|
||||
for (const [cls, cfg] of Object.entries(sections)) {
|
||||
const el = card.querySelector('.' + cls);
|
||||
if (el) el.style.display = show ? 'flex' : 'none';
|
||||
if (el) el.style.display = cfg.show ? (cfg.flex ? 'flex' : '') : 'none';
|
||||
}
|
||||
if (v !== 'function') {
|
||||
const ep = card.querySelector('.ftg-ep');
|
||||
@@ -2955,6 +3106,101 @@ function flowFnTabChange(sel) {
|
||||
if (ep) ep.style.display = sel.value === 'api_report' ? 'flex' : 'none';
|
||||
}
|
||||
|
||||
function addFlowCard() {
|
||||
const container = document.getElementById('flowContainer');
|
||||
if (!container) return;
|
||||
|
||||
const menus = CONFIG?.menus || {};
|
||||
let menuOpts = '<option value="">— Selecciona menú —</option>';
|
||||
for (const k of Object.keys(menus)) menuOpts += `<option value="${k}">📑 ${k}</option>`;
|
||||
|
||||
const eps = ENDPOINTS || {};
|
||||
let epOpts = '<option value="">— Ninguno —</option>';
|
||||
for (const [k, ep] of Object.entries(eps)) epOpts += `<option value="${k}">${ep.name || k}</option>`;
|
||||
|
||||
const flows = CONFIG?.flows || {};
|
||||
let nnOpts = '<option value="">— Ninguno —</option>';
|
||||
for (const k of Object.keys(flows)) nnOpts += `<option value="${k}">${k}</option>`;
|
||||
|
||||
container.insertAdjacentHTML('beforeend', `
|
||||
<div class="item-card" id="flow-card-${Date.now()}">
|
||||
<div class="item-head">
|
||||
<span>ID: <input type="text" name="flow_key[]" value="" style="border:none;background:transparent;font-weight:700;color:#0b3d91;width:180px;font-size:13px" placeholder="nuevo_flujo"></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" selected>💬 Texto fijo</option></optgroup>
|
||||
<optgroup label="Navegar"><option value="menu">📑 Mostrar menú</option></optgroup>
|
||||
<optgroup label="Acción directa"><option value="function">⚡ Función especial</option></optgroup>
|
||||
<optgroup label="Formulario">
|
||||
<option value="collect_input">✏️ Pedir dato</option>
|
||||
<option value="dynamic_list">📋 Lista dinámica</option>
|
||||
<option value="submit_form">🚀 Enviar formulario</option>
|
||||
</optgroup>
|
||||
</select>
|
||||
</div>
|
||||
<div style="flex:1;display:flex;flex-direction:column;gap:10px">
|
||||
<div class="ftg-text"><textarea name="flow_message[]" rows="2" placeholder="Texto que recibirá el usuario"></textarea></div>
|
||||
<div class="ftg-menu" style="display:none"><label>Menú a mostrar</label><select name="flow_menu[]">${menuOpts}</select></div>
|
||||
<div class="ftg-fn" style="display:none">
|
||||
<label>Función</label>
|
||||
<select name="flow_function[]" onchange="flowFnTabChange(this)">
|
||||
<option value="goodbye">↩️ Salir (goodbye)</option>
|
||||
<option value="api_report">📄 Descargar informe</option>
|
||||
<option value="forward_to_ai">🤖 Escalar a IA</option>
|
||||
<option value="forward_to_agent">👤 Escalar a agente</option>
|
||||
</select>
|
||||
<div class="ftg-ep" style="display:none;flex-direction:column;gap:8px;margin-top:8px">
|
||||
<div><label>Endpoint</label><select name="flow_ep_key[]">${epOpts}</select></div>
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px">
|
||||
<div><label>Periodo</label><select name="flow_date_mode[]"><option value="">Sin fecha</option><option value="today">Hoy</option><option value="last_30">Últimos 30 días</option></select></div>
|
||||
<div><label>Nombre archivo</label><input type="text" name="flow_filename[]" placeholder="reporte.pdf"></div>
|
||||
</div>
|
||||
<div><label>Mensaje al enviar</label><input type="text" name="flow_caption[]" placeholder="Aquí tienes el informe."></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ftg-collect" style="display:none;flex-direction:column;gap:8px">
|
||||
<div><label>Pregunta al usuario</label><input type="text" name="flow_prompt[]" placeholder="¿Desde qué fecha? (YYYY-MM-DD)"></div>
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px">
|
||||
<div><label>Grupo formulario</label><input type="text" name="flow_meta_group[]" placeholder="form_prod"></div>
|
||||
<div><label>Campo</label><input type="text" name="flow_meta_key[]" placeholder="desde"></div>
|
||||
</div>
|
||||
<div><label>Flujo siguiente</label><select name="flow_next_node[]">${nnOpts}</select></div>
|
||||
</div>
|
||||
<div class="ftg-dynlist" style="display:none;flex-direction:column;gap:8px">
|
||||
<div><label>Endpoint (lista)</label><select name="flow_dl_ep[]">${epOpts}</select></div>
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px">
|
||||
<div><label>Campo ID</label><input type="text" name="flow_val_field[]" placeholder="id"></div>
|
||||
<div><label>Campo etiqueta</label><input type="text" name="flow_lbl_field[]" 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[]" placeholder="Selecciona..."></div>
|
||||
<div><label>Título sección</label><input type="text" name="flow_sec_title[]" placeholder="Opciones"></div>
|
||||
</div>
|
||||
<div><label>Cuerpo</label><input type="text" name="flow_body[]" placeholder="¿Cuál deseas?"></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[]" placeholder="form_prod"></div>
|
||||
<div><label>Campo</label><input type="text" name="flow_dl_meta_key[]" placeholder="finca_id"></div>
|
||||
</div>
|
||||
<div><label>Flujo siguiente</label><select name="flow_dl_next[]">${nnOpts}</select></div>
|
||||
</div>
|
||||
<div class="ftg-submit" style="display:none;flex-direction:column;gap:8px">
|
||||
<div><label>Endpoint destino</label><select name="flow_sf_ep[]">${epOpts}</select></div>
|
||||
<div><label>Grupo formulario</label><input type="text" name="flow_sf_meta_group[]" placeholder="form_prod"></div>
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px">
|
||||
<div><label>Nombre archivo</label><input type="text" name="flow_sf_filename[]" placeholder="reporte.pdf"></div>
|
||||
<div><label>Mensaje</label><input type="text" name="flow_sf_caption[]" placeholder="Aquí tienes el informe."></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>`);
|
||||
container.lastElementChild.scrollIntoView({behavior:'smooth'});
|
||||
}
|
||||
|
||||
function updatePreview() {
|
||||
const g = document.getElementById('prevGreeting')?.value || '';
|
||||
const f = document.getElementById('prevFallback')?.value || '';
|
||||
|
||||
Reference in New Issue
Block a user