From 576ea7de40ab8167fe08249e428eca6c3a7d3378 Mon Sep 17 00:00:00 2001 From: Lizandro Guarnizo <77708265+lizandrogd@users.noreply.github.com> Date: Tue, 4 Aug 2026 22:16:17 -0500 Subject: [PATCH] fix: 7 de las 17 novedades de ausentismo eran inalcanzables buildDynamicListResponse cortaba en 10 filas con array_slice y descartaba el resto sin aviso, asi que el catalogo de motivos mostraba 10 de 17 y las otras 7 no habia forma de elegirlas. Ahora pagina: 9 opciones y "Ver mas" con cuantas faltan, hasta agotarlas. El 10 dejo de estar suelto en el codigo y quedo como LISTA_MAX_FILAS, que es el limite de WhatsApp de donde salio el problema. Solo cubre los campos select; dynamic_list lo va a necesitar para los lotes de Ciclos y va con ese trabajo. Co-Authored-By: Claude Sonnet 4.6 --- services/NormalBot.php | 30 +++++++++++++++++++++++++++++- setup/test_navegacion.php | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+), 1 deletion(-) diff --git a/services/NormalBot.php b/services/NormalBot.php index be73144..c8ce9a1 100644 --- a/services/NormalBot.php +++ b/services/NormalBot.php @@ -3,6 +3,9 @@ declare(strict_types=1); class NormalBot { + /** Filas máximas de una lista interactiva de WhatsApp. Estaba suelto como 10. */ + private const LISTA_MAX_FILAS = 10; + public static function process(array $company, array $context, string $input, bool $suppressFallback = false): ?array { $config = self::getConfig($company); @@ -549,12 +552,28 @@ class NormalBot $sections[] = ['title' => $secTitle, 'rows' => array_slice($rows, 0, 10)]; } } else { + // Con más opciones que filas disponibles se pagina: el último lugar + // queda para "Ver más". Sin esto las sobrantes eran inalcanzables. + $pagina = max(0, (int)($flow['__page'] ?? 0)); + $hayMas = count($items) > self::LISTA_MAX_FILAS; + $porPagina = $hayMas ? self::LISTA_MAX_FILAS - 1 : self::LISTA_MAX_FILAS; + $visibles = array_slice($items, $pagina * $porPagina, $porPagina); + $rows = []; - foreach (array_slice($items, 0, 10) as $item) { + foreach ($visibles as $item) { $id = (string)($item[$valueField] ?? ''); if ($id === '') continue; $rows[] = ['id' => $id, 'title' => mb_substr((string)($item[$labelField] ?? $id), 0, 24)]; } + + $restantes = count($items) - (($pagina + 1) * $porPagina); + if ($hayMas && $restantes > 0) { + $rows[] = [ + 'id' => '__mas', + 'title' => '➕ Ver más', + 'description' => "Faltan {$restantes}", + ]; + } $sections = [['title' => mb_substr($flow['section_title'] ?? 'Opciones', 0, 24), 'rows' => $rows]]; } @@ -963,6 +982,14 @@ class NormalBot return self::capShowConfirmOrSubmit($cap, $meta, $context, $company, $ctxId); } + // ── 1b. "Ver más": avanza la página y vuelve a mostrar la misma lista ─ + if ($value === '__mas') { + $cap['__page'][$field['key']] = (int)($cap['__page'][$field['key']] ?? 0) + 1; + $meta['__cap'] = $cap; + ConversationContext::updateMetadata($ctxId, $meta); + return self::capAskNext($cap, $context['from'], $company, $ctxId); + } + // ── 2. User chose "Otra opción" — ask for manual text ──────────────── if ($value === '__cap_other') { $cap['__awaiting_other'] = true; @@ -1172,6 +1199,7 @@ class NormalBot 'body' => $field['prompt'] ?? ('Selecciona ' . ($field['label'] ?? $field['key'])), 'button' => 'Ver opciones', 'section_title' => $field['label'] ?? $field['key'], + '__page' => (int)($cap['__page'][$field['key']] ?? 0), ], $to, $company); } } diff --git a/setup/test_navegacion.php b/setup/test_navegacion.php index 808d1e0..526e5bd 100644 --- a/setup/test_navegacion.php +++ b/setup/test_navegacion.php @@ -371,6 +371,41 @@ check('cancelar sigue cancelando', $rutaFe($esperandoFecha, '__foreach_cance check('con fecha ya elegida no interfiere', $rutaFe($conFecha, 'submenu_ciclos'), 'menus'); check('un foreach sin fecha tampoco', $rutaFe(['ask_date' => false], 'submenu_ciclos'), 'menus'); +echo "\nPaginado de listas largas\n"; + +/** Espejo del paginado de buildDynamicListResponse() */ +$paginar = function (array $items, int $pagina, int $max = 10): array { + $hayMas = count($items) > $max; + $porPagina = $hayMas ? $max - 1 : $max; + $filas = array_slice($items, $pagina * $porPagina, $porPagina); + $restantes = count($items) - (($pagina + 1) * $porPagina); + if ($hayMas && $restantes > 0) $filas[] = '__mas'; + return $filas; +}; + +// El caso real: 17 novedades de ausentismo, de las que 7 eran inalcanzables +$novedades = array_map(fn($n) => "N{$n}", range(1, 17)); + +$vistas = []; +$pagina = 0; +do { + $filas = $paginar($novedades, $pagina); + $hayMas = in_array('__mas', $filas, true); + foreach ($filas as $f) if ($f !== '__mas') $vistas[] = $f; + $pagina++; +} while ($hayMas && $pagina < 20); + +check('recorriendo las páginas se llega a todas', count($vistas), 17); +check('ninguna se repite', count(array_unique($vistas)), 17); +check('ninguna se pierde', array_diff($novedades, $vistas), []); +check('termina, no cicla', $pagina <= 3, true); +check('la última página no ofrece "ver más"', + in_array('__mas', $paginar($novedades, 1), true), false); + +// Con 10 o menos no debe aparecer el botón +check('10 opciones entran sin paginar', + $paginar(array_slice($novedades, 0, 10), 0), array_slice($novedades, 0, 10)); + echo "\nask_finca\n"; $af = $flows['ask_finca']; check('no repregunta si ya hay finca', $af['skip_if_set'] ?? false, true);