feat: auto-detect collect_and_post fields from endpoint body_fields
Instead of defining fields inline per flow, the admin now defines them once on the endpoint (direction=upload). The bot reads body_fields from company_endpoints at runtime, so the same endpoint can be reused by multiple collect_and_post flows without duplicating field config. DB: ALTER TABLE company_endpoints ADD COLUMN body_fields TEXT DEFAULT NULL Changes: - NormalBot: falls back to endpoint.body_fields when flow.fields is empty - DashboardController: endpoint rows show a body_fields section for upload endpoints (key / label / prompt per field, add/remove rows) - companyEndpointSave: reads + validates body_fields JSON and persists it Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
3c7ea9fdf9
commit
21d30a0895
@@ -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'] ? '<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>';
|
||||
$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>' : '';
|
||||
$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 .= "<div class=\"bf-row\" style=\"display:grid;grid-template-columns:1fr 1fr 2fr auto;gap:4px;margin-bottom:3px\">
|
||||
<input type=\"text\" class=\"bf-key\" value=\"{$bk}\" placeholder=\"clave\" style=\"font-family:monospace;font-size:11px;padding:2px 5px;border:1px solid #e5e7eb;border-radius:3px\">
|
||||
<input type=\"text\" class=\"bf-lbl\" value=\"{$bl}\" placeholder=\"etiqueta\" style=\"font-size:11px;padding:2px 5px;border:1px solid #e5e7eb;border-radius:3px\">
|
||||
<input type=\"text\" class=\"bf-prm\" value=\"{$bp}\" placeholder=\"Pregunta al usuario\" style=\"font-size:11px;padding:2px 5px;border:1px solid #e5e7eb;border-radius:3px\">
|
||||
<button type=\"button\" onclick=\"this.closest('.bf-row').remove()\" style=\"background:#fee2e2;border:none;border-radius:3px;padding:2px 6px;cursor:pointer;font-size:11px\">✕</button>
|
||||
</div>";
|
||||
}
|
||||
$bfSection = $epDir === 'upload' ? '' : 'display:none';
|
||||
$epRows .= <<<ROW
|
||||
<tr id="ep-row-{$epId}">
|
||||
<td style="min-width:120px">
|
||||
@@ -1138,7 +1151,7 @@ HTML;
|
||||
<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">
|
||||
<select id="epd_{$epId}" onchange="toggleBfSection({$epId},this.value)" 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>
|
||||
@@ -1160,6 +1173,13 @@ HTML;
|
||||
<button class="btn-danger-sm" onclick="deleteEpRow({$id},{$epId})">✕</button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr id="ep-bf-{$epId}" style="{$bfSection}">
|
||||
<td colspan="7" style="padding:8px 12px;background:#f0f7ff;border-bottom:1px solid #e5e7eb">
|
||||
<div style="font-size:11px;font-weight:600;color:#3b5bdb;margin-bottom:6px">📋 Campos del body (para "Pedir campos y enviar")</div>
|
||||
<div id="bf-rows-{$epId}">{$bfRowsHtml}</div>
|
||||
<button type="button" onclick="addBfRow({$epId})" style="font-size:11px;background:#e7efff;border:1px solid #c7d7ff;border-radius:3px;padding:3px 8px;cursor:pointer;margin-top:4px">+ Campo</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;
|
||||
}
|
||||
@@ -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 =
|
||||
'<input type="text" class="bf-key" placeholder="clave" style="font-family:monospace;font-size:11px;padding:2px 5px;border:1px solid #e5e7eb;border-radius:3px">' +
|
||||
'<input type="text" class="bf-lbl" placeholder="etiqueta" style="font-size:11px;padding:2px 5px;border:1px solid #e5e7eb;border-radius:3px">' +
|
||||
'<input type="text" class="bf-prm" placeholder="Pregunta al usuario" style="font-size:11px;padding:2px 5px;border:1px solid #e5e7eb;border-radius:3px">' +
|
||||
'<button type="button" onclick="this.closest(\'.bf-row\').remove()" style="background:#fee2e2;border:none;border-radius:3px;padding:2px 6px;cursor:pointer;font-size:11px">✕</button>';
|
||||
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) {
|
||||
|
||||
Reference in New Issue
Block a user