diff --git a/admin/DashboardController.php b/admin/DashboardController.php index 1d31f78..a7fd7f2 100644 --- a/admin/DashboardController.php +++ b/admin/DashboardController.php @@ -1109,26 +1109,39 @@ HTML; HTML; // 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 = db()->prepare("SELECT id,endpoint_key,name,params,direction,url,method,is_active,last_called_at,body_fields FROM company_endpoints WHERE company_id=? ORDER BY direction,endpoint_key"); $epStmt->execute([$id]); $allEps = $epStmt->fetchAll(\PDO::FETCH_ASSOC); $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'] ? '' . self::h($ep['last_called_at']) . '' : ''; - $dirBadge = $epDir === 'upload' - ? '↑ upload' - : '↓ download'; + $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'] ? '' . self::h($ep['last_called_at']) . '' : ''; $mSel = fn($v) => $epMeth === $v ? 'selected' : ''; $dSel = fn($v) => $epDir === $v ? 'selected' : ''; - $epId = (int)$ep['id']; + $epId = (int)$ep['id']; + + // body_fields for collect_and_post + $bfData = json_decode($ep['body_fields'] ?? '', true) ?: []; + $bfRowsHtml = ''; + foreach ($bfData as $bf) { + $bk = self::h($bf['key'] ?? ''); + $bl = self::h($bf['label'] ?? ''); + $bp = self::h($bf['prompt'] ?? ''); + $bfRowsHtml .= "
+ + + + +
"; + } + $bfSection = $epDir === 'upload' ? '' : 'display:none'; $epRows .= << @@ -1138,7 +1151,7 @@ HTML; - @@ -1160,6 +1173,13 @@ HTML; + + +
📋 Campos del body (para "Pedir campos y enviar")
+
{$bfRowsHtml}
+ + + ROW; } @@ -1450,6 +1470,40 @@ async function deletePhone(pid, btn) { else { alert(j.error); btn.disabled=false; } } +function toggleBfSection(epId, dir) { + const row = document.getElementById('ep-bf-'+epId); + if (row) row.style.display = (dir === 'upload') ? '' : 'none'; +} + +function addBfRow(epId) { + const c = document.getElementById('bf-rows-'+epId); + if (!c) return; + const d = document.createElement('div'); + d.className = 'bf-row'; + d.style.cssText = 'display:grid;grid-template-columns:1fr 1fr 2fr auto;gap:4px;margin-bottom:3px'; + d.innerHTML = + '' + + '' + + '' + + ''; + c.appendChild(d); +} + +function collectBfFields(epId) { + const rows = document.querySelectorAll('#bf-rows-'+epId+' .bf-row'); + const fields = []; + rows.forEach(r => { + const key = r.querySelector('.bf-key')?.value.trim(); + if (!key) return; + fields.push({ + key, + label: r.querySelector('.bf-lbl')?.value.trim() || key, + prompt: r.querySelector('.bf-prm')?.value.trim() || '', + }); + }); + return JSON.stringify(fields); +} + async function saveEpRow(cid, epId) { const fd=new FormData(); fd.append('company_id', cid); @@ -1460,6 +1514,7 @@ async function saveEpRow(cid, epId) { 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'); + fd.append('body_fields', collectBfFields(epId)); const msg=document.getElementById('epMsg'); const r=await fetch('/admin/company/endpoint/save',{method:'POST',body:fd}); const j=await r.json(); @@ -1717,8 +1772,17 @@ HTML; $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'] ?? ''); + $isActive = ($_POST['is_active'] ?? '0') === '1' ? 1 : 0; + $params = trim($_POST['params'] ?? ''); + $bodyFieldsRaw = trim($_POST['body_fields'] ?? ''); + // Validate JSON or null + $bodyFields = null; + if ($bodyFieldsRaw !== '' && $bodyFieldsRaw !== '[]') { + $decoded = json_decode($bodyFieldsRaw, true); + if (is_array($decoded) && !empty($decoded)) { + $bodyFields = json_encode($decoded, JSON_UNESCAPED_UNICODE); + } + } if ($companyId <= 0 || $key === '') { echo json_encode(['ok' => false, 'error' => 'Datos inválidos']); exit; @@ -1726,11 +1790,11 @@ HTML; try { 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]); + db()->prepare("UPDATE company_endpoints SET endpoint_key=?,name=?,direction=?,url=?,method=?,is_active=?,params=?,body_fields=? WHERE id=? AND company_id=?") + ->execute([$key, $name, $direction, $url, $method, $isActive, $params ?: null, $bodyFields, $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]); + db()->prepare("INSERT INTO company_endpoints (company_id,endpoint_key,name,direction,url,method,is_active,params,body_fields) 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),body_fields=VALUES(body_fields)") + ->execute([$companyId, $key, $name, $direction, $url, $method, $isActive, $params ?: null, $bodyFields]); } echo json_encode(['ok' => true]); } catch (\PDOException $e) { diff --git a/services/NormalBot.php b/services/NormalBot.php index 51ec638..243b365 100644 --- a/services/NormalBot.php +++ b/services/NormalBot.php @@ -395,9 +395,23 @@ class NormalBot private static function handleCollectAndPost(array $flow, array $context, array $company, int $ctxId): ?array { $fields = $flow['fields'] ?? []; + + // If no inline fields, auto-detect from endpoint's body_fields definition + if (empty($fields)) { + $epKey = $flow['endpoint_key'] ?? ''; + if ($epKey !== '') { + $stmt = db()->prepare("SELECT body_fields FROM company_endpoints WHERE company_id=? AND endpoint_key=? AND is_active=1 LIMIT 1"); + $stmt->execute([(int)$company['id'], $epKey]); + $row = $stmt->fetch(); + if ($row && !empty($row['body_fields'])) { + $fields = json_decode($row['body_fields'], true) ?: []; + } + } + } + if (empty($fields)) { ConversationContext::reset($ctxId); - return self::sendText('⚠️ Flujo sin campos configurados. Contacta al administrador.', $context['from'], $company); + return self::sendText('⚠️ Flujo sin campos configurados. Define los campos en el endpoint. Escribe *menu* para volver.', $context['from'], $company); } $meta = ConversationContext::getMetadata($ctxId);