feat: menu navegable con submenus y descarga de reportes via api_report

- NormalBot: nueva funcion api_report para descargar PDF/Excel del ERP y enviarlo por WhatsApp
- NormalBot: resolveMenu() para referenciar menus por nombre en los flujos
- WhatsAppSender: uploadMedia() y sendDocument() para envio de documentos
- BotRouter: guarda respuestas en conversations para visibilidad en el chat
- Script apply-menus.php con estructura completa de menus y endpoints
This commit is contained in:
Lizandro Guarnizo
2026-06-27 11:30:08 -05:00
parent 454ee297e2
commit 51c187e47d
4 changed files with 316 additions and 2 deletions
+100 -2
View File
@@ -62,6 +62,16 @@ class NormalBot
return null;
}
private static function resolveMenu($menuRef, array $company): array
{
if (is_string($menuRef)) {
$config = self::getConfig($company);
$menus = $config['menus'] ?? [];
return $menus[$menuRef] ?? [];
}
return is_array($menuRef) ? $menuRef : [];
}
private static function handleFlow(array $flow, array $context, array $company, int $ctxId): ?array
{
$type = $flow['type'] ?? 'text';
@@ -69,7 +79,7 @@ class NormalBot
return match ($type) {
'text' => self::sendText($flow['message'] ?? '', $context['from'], $company),
'image' => self::sendImage($flow['media_id'] ?? '', $flow['caption'] ?? null, $context['from'], $company),
'menu' => self::buildMenuResponse($flow['menu'] ?? [], $context['from'], $company),
'menu' => self::buildMenuResponse(self::resolveMenu($flow['menu'] ?? [], $company), $context['from'], $company),
'function' => self::executeFunction($flow['function'] ?? '', $flow['params'] ?? [], $context, $company, $ctxId),
default => null,
};
@@ -83,10 +93,98 @@ class NormalBot
return self::sendText('¿En qué más puedo ayudarte?', $context['from'], $company);
})(),
'forward_to_ai' => null,
'api_report' => self::executeApiReport($params, $context, $company, $ctxId),
default => null,
};
}
private static function executeApiReport(array $params, array $context, array $company, int $ctxId): ?array
{
$endpoint = $params['endpoint'] ?? '';
if ($endpoint === '') {
return self::sendText('Error: endpoint no configurado.', $context['from'], $company);
}
$baseUrl = rtrim($company['api_base_url'] ?? '', '/');
$url = $baseUrl . '/' . ltrim($endpoint, '/');
$apiKey = $company['api_key'] ?? '';
$query = $params['query'] ?? [];
if (!empty($query)) {
$url .= '?' . http_build_query($query);
}
$url .= (str_contains($url, '?') ? '&' : '?') . 'telefono=' . urlencode($context['from']) . '&nombre=' . urlencode($context['name'] ?? '');
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_TIMEOUT => 60,
CURLOPT_HTTPHEADER => [
'X-API-Key: ' . $apiKey,
'User-Agent: bot-palmas360/1.0',
],
]);
$content = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$contentType = curl_getinfo($ch, CURLINFO_CONTENT_TYPE);
$error = curl_error($ch);
curl_close($ch);
if ($error) {
self::log("API report error [{$endpoint}]: {$error}");
return self::sendText('Error al obtener el reporte. Intenta de nuevo.', $context['from'], $company);
}
if ($httpCode >= 400) {
self::log("API report HTTP {$httpCode} [{$endpoint}]: " . mb_substr($content, 0, 200));
return self::sendText('Error al obtener el reporte. Intenta de nuevo.', $context['from'], $company);
}
$mimeMap = [
'application/pdf' => ['pdf', 'pdf'],
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' => ['xlsx', 'xlsx'],
'application/vnd.ms-excel' => ['xls', 'xls'],
'text/csv' => ['csv', 'csv'],
'application/vnd.oasis.opendocument.spreadsheet' => ['ods', 'ods'],
];
$ext = 'pdf';
$mime = 'application/pdf';
foreach ($mimeMap as $m => $info) {
if (str_starts_with($contentType ?? '', $m)) {
$ext = $info[0];
$mime = $info[1] ?? $m;
break;
}
}
$phoneNumberId = $company['phone_number_id'] ?? env('WHATSAPP_PHONE_NUMBER_ID', '');
if ($phoneNumberId === '') {
return self::sendText('Error: canal de envío no configurado.', $context['from'], $company);
}
$tmpFile = sys_get_temp_dir() . '/report_' . bin2hex(random_bytes(8)) . '.' . $ext;
file_put_contents($tmpFile, $content);
$upload = WhatsAppSender::uploadMedia($tmpFile, $mime, $phoneNumberId);
if (!$upload['success'] || !$upload['media_id']) {
self::log("API report upload failed [{$endpoint}]: " . ($upload['error'] ?? 'unknown'));
return self::sendText('Error al enviar el reporte. Intenta de nuevo.', $context['from'], $company);
}
$reportName = $params['filename'] ?? ('reporte.' . $ext);
$caption = $params['caption'] ?? 'Aquí tienes el reporte solicitado.';
$result = WhatsAppSender::sendDocument($context['from'], $upload['media_id'], $phoneNumberId, $caption, $reportName);
ConversationContext::reset($ctxId);
return null;
}
private static function buildMenuResponse(array $menu, string $to, array $company): array
{
$menuType = $menu['type'] ?? 'list';
@@ -211,7 +309,7 @@ class NormalBot
foreach ($flows as $flowId => $flow) {
if (($flow['type'] ?? '') === 'menu') {
$menu = $flow['menu'] ?? [];
$menu = self::resolveMenu($flow['menu'] ?? [], $company);
foreach ($menu['sections'] ?? [] as $section) {
foreach ($section['rows'] ?? [] as $row) {
if (($row['id'] ?? '') === $input) {