|
📋 Campos del body (para "Pedir campos y enviar")
@@ -1504,6 +1563,72 @@ function collectBfFields(epId) {
return JSON.stringify(fields);
}
+function toggleVarMode(sel) {
+ const row = sel.closest('.var-row');
+ const isAsk = sel.value === 'ask';
+ row.querySelector('.vr-ask-wrap').style.display = isAsk ? '' : 'none';
+ row.querySelector('.vr-fix-wrap').style.display = isAsk ? 'none' : '';
+}
+
+function toggleCustom(sel) {
+ const wrap = sel.closest('.vr-fix-wrap');
+ wrap.querySelector('.vr-custom').style.display = sel.value === '__custom' ? 'block' : 'none';
+}
+
+function collectVarConfigs(epId) {
+ const rows = document.querySelectorAll('#vr-rows-'+epId+' .var-row');
+ const configs = [];
+ rows.forEach(r => {
+ const key = r.dataset.key;
+ const mode = r.querySelector('.vr-mode').value;
+ if (mode === 'ask') {
+ configs.push({ key, mode, prompt: r.querySelector('.vr-prompt')?.value.trim() || '' });
+ } else {
+ const preset = r.querySelector('.vr-preset').value;
+ const value = preset === '__custom' ? (r.querySelector('.vr-custom')?.value.trim() || '') : preset;
+ configs.push({ key, mode, value });
+ }
+ });
+ return JSON.stringify(configs);
+}
+
+// When URL changes, refresh the vars section
+function onUrlChange(epId) {
+ const url = document.getElementById('epu_'+epId).value;
+ const matches = [...url.matchAll(/\{([^}]+)\}/g)].map(m => m[1]);
+ const vrRows = document.getElementById('vr-rows-'+epId);
+ const vrTr = document.getElementById('ep-vars-'+epId);
+ if (!vrRows) return;
+ // Add rows for new vars not yet present
+ const existing = [...vrRows.querySelectorAll('.var-row')].map(r => r.dataset.key);
+ matches.forEach(key => {
+ if (existing.includes(key)) return;
+ const div = document.createElement('div');
+ div.className = 'var-row';
+ div.dataset.key = key;
+ div.style.cssText = 'display:grid;grid-template-columns:80px 100px 1fr auto;gap:6px;align-items:center;margin-bottom:5px;font-size:12px';
+ div.innerHTML = '{'+key+'}' + `
+
+
+
+
+
+
+
+
+ `;
+ vrRows.appendChild(div);
+ });
+ if (vrTr) vrTr.style.display = matches.length ? '' : 'none';
+}
+
async function saveEpRow(cid, epId) {
const fd=new FormData();
fd.append('company_id', cid);
@@ -1515,6 +1640,7 @@ async function saveEpRow(cid, epId) {
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));
+ fd.append('params', collectVarConfigs(epId));
const msg=document.getElementById('epMsg');
const r=await fetch('/admin/company/endpoint/save',{method:'POST',body:fd});
const j=await r.json();
diff --git a/services/NormalBot.php b/services/NormalBot.php
index 4262911..0b7b71f 100644
--- a/services/NormalBot.php
+++ b/services/NormalBot.php
@@ -59,6 +59,11 @@ class NormalBot
return self::handleCapInput($meta, $input, $context, $company, $ctxId);
}
+ // 2d. api_vars: collecting URL variables before executing an api_report endpoint
+ if (!empty($meta['__api_vars'])) {
+ return self::handleApiVarsInput($meta, $input, $context, $company, $ctxId, $flows, $menus);
+ }
+
// 3. Active node — resume conversation
$sentinels = ['collecting', '__greeted'];
if ($currentNode !== null && !in_array($currentNode, $sentinels, true) && isset($flows[$currentNode])) {
@@ -178,8 +183,14 @@ class NormalBot
// Reemplaza {variable} en la URL con valores del metadata acumulado
private static function substituteUrlVars(string $url, array $meta): string
{
+ // Priority: __ep_vars_combined (vars collected/resolved for this endpoint call)
+ $epVars = $meta['__ep_vars_combined'] ?? [];
+ foreach ($epVars as $key => $value) {
+ $url = str_replace('{' . $key . '}', urlencode((string)$value), $url);
+ }
+ // Then the rest of meta groups
foreach ($meta as $group => $fields) {
- if (!is_array($fields)) continue;
+ if (!is_array($fields) || str_starts_with($group, '__')) continue;
foreach ($fields as $key => $value) {
$url = str_replace('{' . $key . '}', urlencode((string)$value), $url);
}
@@ -620,6 +631,57 @@ class NormalBot
};
}
+ // ── Resolves fixed-mode var values ───────────────────────────────────────
+ private static function resolveFixedVar(string $value): string
+ {
+ return match ($value) {
+ 'current_date' => date('Y-m-d'),
+ 'month_start' => date('Y-m-01'),
+ 'month_end' => date('Y-m-t'),
+ 'last_7_start' => date('Y-m-d', strtotime('-7 days')),
+ 'last_30_start' => date('Y-m-d', strtotime('-30 days')),
+ 'year_start' => date('Y-01-01'),
+ default => $value,
+ };
+ }
+
+ // ── Handles sequential collection of URL vars before calling endpoint ───
+ private static function handleApiVarsInput(array $meta, string $input, array $context, array $company, int $ctxId, array $flows, array $menus): ?array
+ {
+ $nav = "\n\nEscribe *menu* para ver opciones o *salir* para terminar.";
+ $state = $meta['__api_vars'];
+
+ $pending = $state['pending'] ?? [];
+ $collected = $state['collected'] ?? [];
+ $epKey = $state['endpoint_key'] ?? '';
+ $epParams = $state['ep_params'] ?? [];
+
+ // Save the user's answer for the current question
+ if (!empty($pending)) {
+ $current = array_shift($pending);
+ $collected[$current['key']] = trim($input);
+ $state['pending'] = $pending;
+ $state['collected'] = $collected;
+ }
+
+ // If more questions remain, ask the next one
+ if (!empty($state['pending'])) {
+ $next = $state['pending'][0];
+ $meta['__api_vars'] = $state;
+ ConversationContext::updateMetadata($ctxId, $meta);
+ return self::sendText($next['prompt'] . $nav, $context['from'], $company);
+ }
+
+ // All vars collected — inject into meta and execute
+ unset($meta['__api_vars']);
+ foreach ($collected as $k => $v) {
+ $meta['__ep_vars'][$k] = $v;
+ }
+ ConversationContext::updateMetadata($ctxId, $meta);
+
+ return self::executeApiReport(array_merge($epParams, ['endpoint_key' => $epKey]), $context, $company, $ctxId);
+ }
+
private static function executeApiReport(array $params, array $context, array $company, int $ctxId): ?array
{
$endpointKey = $params['endpoint_key'] ?? '';
@@ -630,7 +692,7 @@ class NormalBot
return self::sendText('Error: endpoint no configurado.' . $nav, $context['from'], $company);
}
- $stmt = db()->prepare("SELECT url, method FROM company_endpoints WHERE company_id = ? AND endpoint_key = ? AND is_active = 1 LIMIT 1");
+ $stmt = db()->prepare("SELECT url, method, params FROM company_endpoints WHERE company_id = ? AND endpoint_key = ? AND is_active = 1 LIMIT 1");
$stmt->execute([(int)$company['id'], $endpointKey]);
$ep = $stmt->fetch();
@@ -640,18 +702,72 @@ class NormalBot
return self::sendText('El informe solicitado no está configurado. Contacta al administrador.' . $nav, $context['from'], $company);
}
- $meta = ConversationContext::getMetadata($ctxId);
- $url = self::substituteUrlVars($ep['url'], $meta); // reemplaza {var} con valores del formulario
+ // ── Resolve URL vars from params config ──────────────────────────────
+ $varConfigs = json_decode($ep['params'] ?? '[]', true) ?: [];
+ $meta = ConversationContext::getMetadata($ctxId);
+
+ // Inject any previously collected vars into meta for substituteUrlVars
+ $epVars = $meta['__ep_vars'] ?? [];
+ if (!empty($epVars)) {
+ $meta['__ep_vars_flat'] = $epVars;
+ }
+
+ $askVars = [];
+ foreach ($varConfigs as $vc) {
+ $key = $vc['key'] ?? '';
+ $mode = $vc['mode'] ?? 'ask';
+ if ($key === '') continue;
+
+ if ($mode === 'fixed') {
+ // Inject fixed value directly into meta for substituteUrlVars
+ $meta['__ep_vars_flat'][$key] = self::resolveFixedVar($vc['value'] ?? '');
+ } elseif ($mode === 'ask') {
+ // Only ask if not already collected
+ if (!isset($epVars[$key])) {
+ $askVars[] = ['key' => $key, 'prompt' => $vc['prompt'] ?? "Ingresa el valor para {$key}:"];
+ } else {
+ $meta['__ep_vars_flat'][$key] = $epVars[$key];
+ }
+ }
+ }
+
+ // If there are vars to ask, start collection flow
+ if (!empty($askVars)) {
+ $first = array_shift($askVars);
+ $meta['__api_vars'] = [
+ 'endpoint_key' => $endpointKey,
+ 'ep_params' => $params,
+ 'pending' => $askVars,
+ 'collected' => [],
+ ];
+ // Clean collected vars so they don't bleed into the next call
+ unset($meta['__ep_vars']);
+ ConversationContext::updateMetadata($ctxId, $meta);
+ return self::sendText($first['prompt'] . $nav, $context['from'], $company);
+ }
+
+ // Flatten collected vars into meta for substituteUrlVars
+ if (!empty($meta['__ep_vars_flat'])) {
+ $meta['__ep_vars_combined'] = $meta['__ep_vars_flat'];
+ }
+ unset($meta['__ep_vars'], $meta['__ep_vars_flat']);
+ ConversationContext::updateMetadata($ctxId, $meta);
+
+ $url = self::substituteUrlVars($ep['url'], array_merge($meta, ['__ep_vars_combined' => $meta['__ep_vars_combined'] ?? []]));
$method = strtoupper($ep['method'] ?? 'GET');
$apiKey = $company['api_key'] ?? '';
$extraQuery = $params['query'] ?? [];
$dateMode = $params['date_mode'] ?? '';
if ($dateMode === 'today') {
- $extraQuery['fecha'] = date('Y-m-d');
+ $extraQuery['fecha_desde'] = date('Y-m-d');
+ $extraQuery['fecha_hasta'] = date('Y-m-d');
} elseif ($dateMode === 'last_30') {
- $extraQuery['fecha_inicio'] = date('Y-m-d', strtotime('-30 days'));
- $extraQuery['fecha_fin'] = date('Y-m-d');
+ $extraQuery['fecha_desde'] = date('Y-m-d', strtotime('-30 days'));
+ $extraQuery['fecha_hasta'] = date('Y-m-d');
+ } elseif ($dateMode === 'current_month') {
+ $extraQuery['fecha_desde'] = date('Y-m-01');
+ $extraQuery['fecha_hasta'] = date('Y-m-d');
}
$extraQuery['telefono'] = $context['from'];
$extraQuery['nombre'] = $context['name'] ?? '';
@@ -693,6 +809,14 @@ class NormalBot
return self::sendText('Error al obtener el reporte. Intenta de nuevo.' . $nav, $context['from'], $company);
}
+ // Si el ERP devuelve JSON (ej: sin_datos), informar al usuario en texto
+ if (str_starts_with($contentType ?? '', 'application/json')) {
+ $json = json_decode($content, true);
+ $msg = $json['mensaje'] ?? ($json['message'] ?? 'No hay datos disponibles para el período solicitado.');
+ ConversationContext::reset($ctxId);
+ return self::sendText('ℹ️ ' . $msg . $nav, $context['from'], $company);
+ }
+
$mimeMap = [
'application/pdf' => ['pdf', 'application/pdf'],
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' => ['xlsx', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'],
|