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:
Lizandro Guarnizo
2026-06-28 17:04:05 -05:00
co-authored by Claude Sonnet 4.6
parent eeda883bd9
commit 2c0ac04993
3 changed files with 346 additions and 84 deletions
+25 -10
View File
@@ -41,14 +41,15 @@ class NormalBot
}
// 3. Active node — resume conversation
if ($currentNode !== null && $currentNode !== 'collecting' && isset($flows[$currentNode])) {
$sentinels = ['collecting', '__greeted'];
if ($currentNode !== null && !in_array($currentNode, $sentinels, true) && isset($flows[$currentNode])) {
return self::handleFlow($flows[$currentNode], $context, $company, $ctxId, $menus);
}
// 4. Per-type greeting menu (new user / no active context)
// 4. Per-type greeting menu — only for brand-new sessions (currentNode === null)
$greetingMenuKey = $perType['greeting_menu'] ?? null;
if ($greetingMenuKey !== null && $currentNode === null && isset($menus[$greetingMenuKey])) {
ConversationContext::updateNode($ctxId, null);
ConversationContext::updateNode($ctxId, '__greeted'); // sentinel: evita re-envío del menú
return self::buildMenuResponse($menus[$greetingMenuKey], $context['from'], $company);
}
@@ -72,9 +73,9 @@ class NormalBot
return self::handleFlow($flows[$fallbackFlowId], $context, $company, $ctxId, $menus);
}
// 7. Greeting menu as fallback
// 7. Greeting menu as fallback (texto no reconocido después de estar en __greeted)
if ($greetingMenuKey !== null && isset($menus[$greetingMenuKey])) {
ConversationContext::updateNode($ctxId, null);
ConversationContext::updateNode($ctxId, '__greeted');
return self::buildMenuResponse($menus[$greetingMenuKey], $context['from'], $company);
}
@@ -133,7 +134,8 @@ class NormalBot
private static function handleDynamicList(array $flow, array $context, array $company, int $ctxId): ?array
{
$items = self::fetchDynamicList($flow['source_endpoint_key'] ?? '', $company);
$meta = ConversationContext::getMetadata($ctxId);
$items = self::fetchDynamicList($flow['source_endpoint_key'] ?? '', $company, $meta);
if ($items === null || count($items) === 0) {
ConversationContext::reset($ctxId);
@@ -143,7 +145,6 @@ class NormalBot
);
}
$meta = ConversationContext::getMetadata($ctxId);
$meta['collecting'] = [
'meta_group' => $flow['meta_group'] ?? '',
'meta_key' => $flow['meta_key'] ?? '',
@@ -155,7 +156,19 @@ class NormalBot
return self::buildDynamicListResponse($items, $flow, $context['from'], $company);
}
private static function fetchDynamicList(string $endpointKey, array $company): ?array
// Reemplaza {variable} en la URL con valores del metadata acumulado
private static function substituteUrlVars(string $url, array $meta): string
{
foreach ($meta as $group => $fields) {
if (!is_array($fields)) continue;
foreach ($fields as $key => $value) {
$url = str_replace('{' . $key . '}', urlencode((string)$value), $url);
}
}
return $url;
}
private static function fetchDynamicList(string $endpointKey, array $company, array $meta = []): ?array
{
if ($endpointKey === '') return null;
@@ -167,7 +180,8 @@ class NormalBot
if (!$ep || empty($ep['url'])) return null;
$apiKey = $company['api_key'] ?? '';
$ch = curl_init($ep['url']);
$url = self::substituteUrlVars($ep['url'], $meta);
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 10,
@@ -307,7 +321,8 @@ class NormalBot
return self::sendText('El informe solicitado no está configurado. Contacta al administrador.' . $nav, $context['from'], $company);
}
$url = $ep['url'];
$meta = ConversationContext::getMetadata($ctxId);
$url = self::substituteUrlVars($ep['url'], $meta); // reemplaza {var} con valores del formulario
$method = strtoupper($ep['method'] ?? 'GET');
$apiKey = $company['api_key'] ?? '';