From 52dbd1e203469455ed2ab7006630039b78168451 Mon Sep 17 00:00:00 2001 From: Lizandro Guarnizo <77708265+lizandrogd@users.noreply.github.com> Date: Thu, 20 Aug 2026 22:59:25 -0500 Subject: [PATCH] feat: editar desde el resumen sin rehacer el formulario MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cancelar obligaba a empezar de cero por una fecha mal puesta. El resumen suma un tercer boton —WhatsApp admite tres y usaba dos—: Editar lista los campos ya cargados con su valor, se elige uno, se corrige y se vuelve derecho al resumen. Los campos que alimentan a otros declaran 'invalida' en el seed: cambiar el tipo de ausentismo borra el motivo y lo vuelve a preguntar, acotado al tipo nuevo. Sin eso quedaba un motivo de Incapacidad dentro de un Permiso y el ERP recibia datos incoherentes sin que nadie lo notara. El motor engancha en un solo lugar, capAskNext: mientras queden campos sin valor los pregunta en cadena, y cuando no queda ninguno vuelve al resumen en vez de seguir el orden del formulario. Aplica a los cinco flujos de carga. Co-Authored-By: Claude Sonnet 4.6 --- services/NormalBot.php | 79 +++++++++++++++++++++++++++++++++++++ setup/seed_palmas.php | 9 +++++ setup/tests/test_flujos.php | 50 +++++++++++++++++++++++ 3 files changed, 138 insertions(+) diff --git a/services/NormalBot.php b/services/NormalBot.php index 1ad2181..cfcc9b5 100644 --- a/services/NormalBot.php +++ b/services/NormalBot.php @@ -1374,6 +1374,7 @@ class NormalBot 'body' => ['text' => $summary], 'action' => ['buttons' => [ ['type' => 'reply', 'reply' => ['id' => '__cap_confirm', 'title' => '✅ Confirmar']], + ['type' => 'reply', 'reply' => ['id' => '__cap_edit', 'title' => '✏️ Editar']], ['type' => 'reply', 'reply' => ['id' => '__cap_cancel', 'title' => '❌ Cancelar']], ]], ], $company); @@ -1422,6 +1423,32 @@ class NormalBot private static function capAskNext(array $cap, string $to, array $company, int $ctxId = 0): ?array { + // Editando desde el resumen: se pregunta el primer campo que quedo sin + // valor —el que se eligio, mas los que dependian de el— y al completarse + // todos se vuelve al resumen en vez de seguir el orden del formulario. + if (!empty($cap['__editando'])) { + $pendiente = null; + foreach ($cap['fields'] as $i => $f) { + $k = $f['key'] ?? ''; + if ($k === '' || isset($cap['collected'][$k])) continue; + if (self::capShouldSkip($f, $cap['collected'])) continue; + $pendiente = $i; + break; + } + unset($cap['__editando']); + if ($pendiente !== null) { + $cap['__editando'] = true; + $cap['index'] = $pendiente; + } + $m = $ctxId > 0 ? ConversationContext::getMetadata($ctxId) : []; + $m['__cap'] = $cap; + if ($ctxId > 0) ConversationContext::updateMetadata($ctxId, $m); + + if ($pendiente === null) { + return self::capShowConfirmOrSubmit($cap, $m, ['from' => $to], $company, $ctxId); + } + } + // Saltar campos ya pre-llenados (no contiguos) $prefilled = $cap['prefilled'] ?? []; while ((int)$cap['index'] < count($cap['fields'])) { @@ -2163,6 +2190,58 @@ class NormalBot if ($input === '__cap_confirm') { return self::submitCapPost($meta, $context, $company, $ctxId); } + // Elegir que corregir sin rehacer todo el formulario + if ($input === '__cap_edit') { + $cap = $meta['__cap']; + $rows = []; + foreach ($cap['fields'] as $f) { + $k = $f['key'] ?? ''; + if ($k === '' || !isset($cap['collected'][$k])) continue; + $valor = $cap['collected_labels'][$k] ?? $cap['collected'][$k]; + if (is_array($valor)) $valor = implode(', ', $valor); + $rows[] = [ + 'id' => '__cap_ed:' . $k, + 'title' => mb_substr(self::capFieldLabel($cap['fields'], $k), 0, 24), + 'description' => mb_substr((string)$valor, 0, 72), + ]; + } + $rows[] = ['id' => '__cap_ed_volver', 'title' => '↩️ Volver', 'description' => 'Sin cambios']; + return self::enqueueInteractive($context['from'], [ + 'type' => 'list', + 'header' => ['type' => 'text', 'text' => mb_substr($cap['header'], 0, 60)], + 'body' => ['text' => '¿Qué quieres corregir?'], + 'footer' => ['text' => ''], + 'action' => ['button' => 'Ver campos', 'sections' => [[ + 'title' => 'Campos', + 'rows' => array_slice($rows, 0, self::LISTA_MAX_FILAS), + ]]], + ], $company); + } + if ($input === '__cap_ed_volver') { + return self::capShowConfirmOrSubmit($meta['__cap'], $meta, $context, $company, $ctxId); + } + if (str_starts_with($input, '__cap_ed:')) { + $clave = substr($input, strlen('__cap_ed:')); + $cap = $meta['__cap']; + + // Un campo alimenta a otros: cambiar el tipo invalida el motivo, + // cambiar el lote invalida el producto. Se borran y se vuelven a + // preguntar en cadena, si no el ERP recibiria datos incoherentes. + $borrar = [$clave]; + foreach ($cap['fields'] as $f) { + if (($f['key'] ?? '') === $clave) { + $borrar = array_merge($borrar, (array)($f['invalida'] ?? [])); + } + } + foreach ($borrar as $k) { + unset($cap['collected'][$k], $cap['collected_labels'][$k]); + } + + $cap['__editando'] = true; + $meta['__cap'] = $cap; + ConversationContext::updateMetadata($ctxId, $meta); + return self::capAskNext($cap, $context['from'], $company, $ctxId); + } if ($input === '__cap_cancel') { $m = ConversationContext::getMetadata($ctxId); unset($m['__cap']); diff --git a/setup/seed_palmas.php b/setup/seed_palmas.php index 110a965..976596e 100644 --- a/setup/seed_palmas.php +++ b/setup/seed_palmas.php @@ -448,6 +448,8 @@ $configJson = [ // Primero el grupo general: 17 motivos en una sola lista // llegaban con el nombre cortado y costaban de entender 'key' => 'grupo_novedad', + // Al cambiarlo se vuelve a preguntar: el motivo pertenece al tipo elegido + 'invalida' => ['novedad_id'], 'label' => 'Tipo', 'type' => 'select', 'prompt' => '¿Qué tipo de ausentismo?', @@ -592,6 +594,7 @@ $configJson = [ ], [ 'key' => 'lote_id', + 'label' => 'Lote', 'type' => 'select', 'prompt' => 'Elige el lote (entre paréntesis va lo que falta):', @@ -662,6 +665,7 @@ $configJson = [ ], [ 'key' => 'lote_id', + 'label' => 'Lote', 'type' => 'select', 'prompt' => '¿En qué lote?', @@ -704,6 +708,9 @@ $configJson = [ 'fields' => [ [ 'key' => 'tipo', + // Al cambiarlo se vuelve a preguntar: el catálogo de labores + // y el producto dependen del tipo elegido + 'invalida' => ['novedad_id', 'producto_id'], 'label' => 'Tipo', 'type' => 'static_select', 'prompt' => '¿Qué vas a registrar?', @@ -732,6 +739,8 @@ $configJson = [ ], [ 'key' => 'lote_id', + // Al cambiarlo se vuelve a preguntar: la dosis sale de la fertilizacion de ese lote + 'invalida' => ['producto_id'], 'label' => 'Lote', 'type' => 'select', 'prompt' => '¿En qué lote?', diff --git a/setup/tests/test_flujos.php b/setup/tests/test_flujos.php index faecca6..a087232 100644 --- a/setup/tests/test_flujos.php +++ b/setup/tests/test_flujos.php @@ -270,6 +270,56 @@ $r = NormalBot::process($co, $ctx, 'subir labores diarias'); check('"subir labores diarias" arranca el flujo directo', str_contains(textoDe($r), 'De qué fecha es la labor')); capturas(); +// ════ 9. Editar desde el resumen ══════════════════════════════════════════════ +echo "\nEditar — corregir sin rehacer el formulario\n"; + +$ctx = contexto('57300EDITAR'); +NormalBot::processInteractive($co, $ctx, 'registrar_ausentismo'); +NormalBot::process($co, $ctx, 'julio'); +NormalBot::processInteractive($co, $ctx, '412'); +NormalBot::processInteractive($co, $ctx, 'Incapacidad'); +NormalBot::processInteractive($co, $ctx, '46'); +NormalBot::processInteractive($co, $ctx, $hoy); +$r = NormalBot::processInteractive($co, $ctx, $manana); +check('el resumen ofrece editar', isset(filasDe($r)['__cap_edit'])); + +$r = NormalBot::processInteractive($co, $ctx, '__cap_edit'); +$campos = filasDe($r); +check('lista los campos ya cargados', + array_keys($campos), + ['__cap_ed:empleado_id', '__cap_ed:grupo_novedad', '__cap_ed:novedad_id', + '__cap_ed:fecha_inicial', '__cap_ed:fecha_final', '__cap_ed_volver']); + +// Un campo hoja: se corrige y vuelve derecho al resumen +$r = NormalBot::processInteractive($co, $ctx, '__cap_ed:fecha_final'); +check('pregunta solo ese campo', str_contains(textoDe($r), 'Hasta qué fecha')); +$pasado = date('Y-m-d', strtotime('+2 days')); +$r = NormalBot::processInteractive($co, $ctx, $pasado); +check('y vuelve al resumen, no al siguiente campo', isset(filasDe($r)['__cap_confirm'])); +check('con el valor corregido', str_contains(textoDe($r), $pasado)); + +// Un campo del que otro depende: se re-pregunta en cadena +$r = NormalBot::processInteractive($co, $ctx, '__cap_edit'); +$r = NormalBot::processInteractive($co, $ctx, '__cap_ed:grupo_novedad'); +check('editar el tipo vuelve a preguntar el tipo', str_contains(textoDe($r), 'tipo de ausentismo')); +$r = NormalBot::processInteractive($co, $ctx, 'Permiso'); +check('y encadena el motivo, que quedó inválido', str_contains(textoDe($r), 'Cuál es el motivo')); +check('acotado al tipo nuevo', array_map('strval', array_keys(filasDe($r))), ['50']); +$r = NormalBot::processInteractive($co, $ctx, '50'); +check('recién ahí vuelve al resumen', isset(filasDe($r)['__cap_confirm'])); + +// Salir sin tocar nada +$r = NormalBot::processInteractive($co, $ctx, '__cap_edit'); +$r = NormalBot::processInteractive($co, $ctx, '__cap_ed_volver'); +check('"Volver" deja todo como estaba', isset(filasDe($r)['__cap_confirm'])); + +capturas(); +NormalBot::processInteractive($co, $ctx, '__cap_confirm'); +$post = array_values(array_filter(capturas(), fn($c) => ($c['peticion'] ?? '') === 'ausentismos_up'))[0] ?? null; +check('el POST lleva lo editado, sin restos del valor viejo', + [$post['body']['novedad_id'] ?? null, $post['body']['fecha_final'] ?? null], + ['50', $pasado]); + // ════ Resultado ═══════════════════════════════════════════════════════════════ echo "\n" . ($GLOBALS['fallas'] ? "{$GLOBALS['fallas']} falla(s)\n" : "Todos los flujos ejecutan de punta a punta\n"); exit($GLOBALS['fallas'] ? 1 : 0);