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;
|
- |
+
+ |
+ 📋 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);