diff --git a/services/NormalBot.php b/services/NormalBot.php index c258e04..ae5be14 100644 --- a/services/NormalBot.php +++ b/services/NormalBot.php @@ -593,6 +593,14 @@ class NormalBot $meta = ConversationContext::getMetadata($ctxId); $items = self::fetchDynamicList($flow['source_endpoint_key'] ?? '', $company, $meta); + // exclude_values descarta opciones que no son ítems reales, como el + // "🌐 Todas las fincas" (id 0) que el catálogo antepone para los informes. + $excluir = array_map('strval', $flow['exclude_values'] ?? []); + if ($excluir && is_array($items)) { + $vf = $flow['value_field'] ?? 'id'; + $items = array_values(array_filter($items, fn($i) => !in_array((string)($i[$vf] ?? ''), $excluir, true))); + } + if (empty($items)) { ConversationContext::reset($ctxId); return self::sendText( @@ -674,18 +682,70 @@ class NormalBot 'header' => $flow['header'] ?? '📋 Ingreso de datos', 'endpoint_key'=> $flow['endpoint_key'] ?? '', 'success_text'=> $flow['success_text'] ?? '✅ Datos registrados correctamente.', + 'items_key' => $flow['items_key'] ?? 'fincas', + 'ask_date' => (bool)($flow['ask_date'] ?? false), + 'fecha' => null, ]; ConversationContext::updateMetadata($ctxId, $meta); + // El dato se registra contra una fecha; se pregunta antes del bucle + // para no repetirla en cada ítem. + if ($meta['__foreach']['ask_date']) { + return self::foreachAskDate($flow, $context['from'], $company); + } + return self::foreachAskNext($meta['__foreach'], $context['from'], $company); } + /** Hoy / Ayer / Anteayer / Otra — mismo criterio que el campo date_quick. */ + private static function foreachAskDate(array $flow, string $to, array $company): ?array + { + $hoy = date('Y-m-d'); + return self::enqueueInteractive($to, [ + 'type' => 'list', + 'header' => ['type' => 'text', 'text' => mb_substr($flow['header'] ?? '📋 Registro', 0, 60)], + 'body' => ['text' => $flow['date_question'] ?? '📅 ¿De qué fecha es el registro?'], + 'footer' => ['text' => ''], + 'action' => ['button' => 'Ver fechas', 'sections' => [[ + 'title' => 'Fecha', + 'rows' => [ + ['id' => $hoy, 'title' => '📅 Hoy', 'description' => $hoy], + ['id' => date('Y-m-d', strtotime('-1 day')), 'title' => '📅 Ayer', 'description' => date('Y-m-d', strtotime('-1 day'))], + ['id' => date('Y-m-d', strtotime('-2 days')), 'title' => '📅 Anteayer', 'description' => date('Y-m-d', strtotime('-2 days'))], + ['id' => '__fe_other', 'title' => '✏️ Otra fecha', 'description' => 'Escribe la fecha'], + ], + ]]], + ], $company); + } + private static function handleForeachInput(array $meta, string $input, array $context, array $company, int $ctxId, array $flows): ?array { $fe = $meta['__foreach']; $items = $fe['items']; $idx = (int)$fe['index']; + // ── Fecha del registro, antes de recorrer los ítems ────────────────── + if (!empty($fe['ask_date']) && ($fe['fecha'] ?? null) === null) { + $v = trim($input); + + if ($v === '__fe_other') { + $fe['awaiting_date'] = true; + $meta['__foreach'] = $fe; + ConversationContext::updateMetadata($ctxId, $meta); + return self::sendText('✏️ Escribe la fecha (YYYY-MM-DD, ej: ' . date('Y-m-d') . '):', $context['from'], $company); + } + + if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $v) || !strtotime($v)) { + return self::sendText('⚠️ Formato inválido. Escribe la fecha como YYYY-MM-DD (ej: ' . date('Y-m-d') . '):', $context['from'], $company); + } + + $fe['fecha'] = $v; + unset($fe['awaiting_date']); + $meta['__foreach'] = $fe; + ConversationContext::updateMetadata($ctxId, $meta); + return self::foreachAskNext($fe, $context['from'], $company); + } + // Validate: must be a number if (!is_numeric(str_replace(',', '.', $input))) { $item = $items[$idx]; @@ -712,7 +772,9 @@ class NormalBot } // All collected — show summary and confirm/cancel buttons - $summary = "*📊 Resumen — {$fe['header']}*\n\n"; + $summary = "*📊 Resumen — {$fe['header']}*\n"; + if (!empty($fe['fecha'])) $summary .= "📅 {$fe['fecha']}\n"; + $summary .= "\n"; foreach ($fe['collected'] as $row) { $summary .= "• {$row['label']}: *{$row['valor']}*\n"; } @@ -748,11 +810,20 @@ class NormalBot $endpointKey = $fe['endpoint_key'] ?? ''; $nav = "\n\nEscribe *menú* para volver al inicio, *atrás* para subir un nivel o *salir* para terminar."; - // Build POST body: array of {id, valor} objects - $body = []; + // El ERP espera {fecha, : [{id, label, valor}]}; telefono y + // nombre los lee el controller para dejar trazabilidad en bot_entrada. + // (string) explícito: PHP convierte las claves numéricas del array en int + // y el JSON saldría con tipos distintos según el id de la finca. + $lecturas = []; foreach ($fe['collected'] as $id => $row) { - $body[] = ['id' => $id, 'label' => $row['label'], 'valor' => $row['valor']]; + $lecturas[] = ['id' => (string)$id, 'label' => $row['label'], 'valor' => $row['valor']]; } + $body = [ + 'fecha' => $fe['fecha'] ?? date('Y-m-d'), + 'telefono' => $context['from'], + 'nombre' => $context['name'] ?? '', + ($fe['items_key'] ?? 'fincas') => $lecturas, + ]; unset($meta['__foreach']); ConversationContext::updateMetadata($ctxId, $meta); diff --git a/setup/seed_palmas.php b/setup/seed_palmas.php index 3da3eb2..66df8fc 100644 --- a/setup/seed_palmas.php +++ b/setup/seed_palmas.php @@ -230,6 +230,7 @@ $configJson = [ 'rows' => [ ['id' => 'subir_ciclo_cosecha', 'title' => '🌾 Ciclo cosecha', 'description' => 'Subir ciclo de cosecha'], ['id' => 'registrar_ausentismo', 'title' => '👥 Ausentismo', 'description' => 'Registrar falta, incapacidad o permiso'], + ['id' => 'registrar_pluviometria','title' => '🌧️ Pluviometría', 'description' => 'Milímetros de lluvia por finca'], ], ], ], @@ -448,6 +449,26 @@ $configJson = [ ], ], + // Pluviometría: un valor de mm por finca, todas contra la misma fecha. + // exclude_values saca el "🌐 Todas las fincas" (id 0) que el catálogo + // antepone para los informes y que acá no es una finca real. + 'registrar_pluviometria' => [ + 'type' => 'collect_for_each', + 'source_endpoint_key' => 'fincas_dn', + 'endpoint_key' => 'pluvio_upload', + 'value_field' => 'id', + 'label_field' => 'label', + 'exclude_values' => ['0'], + 'ask_date' => true, + 'date_question' => '📅 ¿De qué fecha es la lectura?', + 'question' => '🌧️ ¿Cuántos milímetros?', + 'header' => '🌧️ Pluviometría', + 'items_key' => 'fincas', + 'success_text' => '✅ Pluviometría enviada a Palmas360.', + 'back' => 'enviar_informacion', + 'nlu_description' => 'Registrar pluviometría, lluvia o milímetros por finca', + ], + // Upload 'enviar_informacion' => ['type' => 'menu', 'menu' => 'submenu_subir_ciclos', 'nlu_skip' => true, 'back' => 'show_main_menu'], 'subir_ciclo_cosecha' => ['type' => 'function', 'function' => 'upload_ciclo', 'nlu_description' => 'Subir o enviar un ciclo de cosecha', 'params' => ['ciclo' => 'cosecha']], @@ -480,6 +501,7 @@ $configJson = [ 'rows' => [ ['id' => 'subir_ciclo_cosecha', 'title' => '🌾 Subir cosecha', 'description' => 'Enviar ciclo de cosecha'], ['id' => 'registrar_ausentismo', 'title' => '👥 Ausentismo', 'description' => 'Registrar falta, incapacidad o permiso'], + ['id' => 'registrar_pluviometria','title' => '🌧️ Pluviometría', 'description' => 'Milímetros de lluvia por finca'], ], ], ], @@ -572,6 +594,7 @@ $endpoints = [ ['key' => 'empleados_buscar_dn', 'dir' => 'download', 'url' => $BASE . '?peticion=empleados&estado=1&items=10'], ['key' => 'novedades_ausentismo_dn', 'dir' => 'download', 'url' => $BASE . '?peticion=novedades_ausentismo_dn'], ['key' => 'ausentismos_up', 'dir' => 'upload', 'url' => $BASE . '?peticion=ausentismos_up'], + ['key' => 'pluvio_upload', 'dir' => 'upload', 'url' => $BASE . '?peticion=pluvio_upload'], // Todos los lotes (PDF) por ciclo ['key' => 'cosecha_todos_dn', 'dir' => 'download', 'url' => $BASE . '?peticion=ciclos_cosecha_dn&finca_id={finca_id}'], ['key' => 'polinizacion_todos_dn', 'dir' => 'download', 'url' => $BASE . '?peticion=ciclos_polinizacion_dn&finca_id={finca_id}'], diff --git a/setup/test_navegacion.php b/setup/test_navegacion.php index 0284316..e0f93e8 100644 --- a/setup/test_navegacion.php +++ b/setup/test_navegacion.php @@ -296,6 +296,59 @@ $enMenu = function (array $config, string $cat, string $id): bool { check('cat 1 lo ve en su menú', $enMenu($config, '1', 'registrar_ausentismo'), true); check('cat 3 lo ve en enviar información', $enMenu($config, '3', 'registrar_ausentismo'), true); +echo "\nPluviometría (POST)\n"; + +$plu = $flows['registrar_pluviometria'] ?? []; +check('pide la fecha antes del bucle', $plu['ask_date'] ?? false, true); +check('excluye "Todas las fincas"', $plu['exclude_values'] ?? [], ['0']); +check('postea a pluvio_upload', $plu['endpoint_key'] ?? null, 'pluvio_upload'); +check('endpoint pluvio_upload registrado', isset($eps['pluvio_upload']), true); + +// Espejo de exclude_values: el catálogo antepone id 0 = "🌐 Todas las fincas" +$catalogo = [ + ['id' => '0', 'label' => '🌐 Todas las fincas'], + ['id' => '4', 'label' => 'REPOSO'], + ['id' => '7', 'label' => 'ROSA BLANCA'], +]; +$reales = array_values(array_filter($catalogo, + fn($i) => !in_array((string)$i['id'], array_map('strval', $plu['exclude_values']), true))); +check('solo pregunta por fincas reales', array_column($reales, 'label'), ['REPOSO', 'ROSA BLANCA']); + +// Espejo de submitForeach(): forma que exige BotEntradaProcesador::pluviosidad() +$armarBody = function (array $fe, string $from, string $nombre): array { + $lecturas = []; + foreach ($fe['collected'] as $id => $row) { + $lecturas[] = ['id' => (string)$id, 'label' => $row['label'], 'valor' => $row['valor']]; + } + return [ + 'fecha' => $fe['fecha'] ?? date('Y-m-d'), + 'telefono' => $from, + 'nombre' => $nombre, + ($fe['items_key'] ?? 'fincas') => $lecturas, + ]; +}; + +$body = $armarBody([ + 'fecha' => '2026-08-01', + 'items_key' => $plu['items_key'] ?? 'fincas', + 'collected' => [ + '4' => ['label' => 'REPOSO', 'valor' => '35'], + '7' => ['label' => 'ROSA BLANCA', 'valor' => '12'], + ], +], '573001565293', 'Usite'); + +// El procesador lee $d['fincas'] y $d['fecha']; el controller, telefono y nombre +check('la clave es "fincas", no "lecturas"', array_key_exists('fincas', $body), true); +check('manda la fecha elegida', $body['fecha'], '2026-08-01'); +check('manda trazabilidad', [$body['telefono'], $body['nombre']], ['573001565293', 'Usite']); +check('cada lectura lleva id, label y valor', + array_keys($body['fincas'][0]), ['id', 'label', 'valor']); +check('los ids son los de finca', array_column($body['fincas'], 'id'), ['4', '7']); + +// Sin fecha elegida cae a hoy, no a null +check('sin fecha usa hoy', + $armarBody(['collected' => []], 'x', 'y')['fecha'], date('Y-m-d')); + echo "\nask_finca\n"; $af = $flows['ask_finca']; check('no repregunta si ya hay finca', $af['skip_if_set'] ?? false, true);