feat: flujo de registro de pluviometria (Fase 1, carga de datos)

Segundo flujo de POST: fecha una sola vez y luego los milimetros de cada finca.

collect_for_each gana tres piezas que este flujo necesitaba:
- ask_date pregunta la fecha antes del bucle (Hoy/Ayer/Anteayer/Otra), en vez
  de repetirla por item o asumir hoy.
- exclude_values descarta el "Todas las fincas" (id 0) que el catalogo antepone
  para los informes y que aca no es una finca real.
- El payload pasa de un array plano a {fecha, fincas:[{id,label,valor}]}, que es
  lo que BotEntradaProcesador::pluviosidad() espera; ademas manda telefono y
  nombre, que el controller usa para la trazabilidad en bot_entrada.

Nota: el procesador ACTUALIZA la fila de pluviosidad del dia y falla si no
existe. Queda pendiente confirmar con Palmas360 quien las crea.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Lizandro Guarnizo
2026-08-04 20:22:03 -05:00
co-authored by Claude Sonnet 4.6
parent 0e72aa3d5a
commit 76ee42b96e
3 changed files with 151 additions and 4 deletions
+75 -4
View File
@@ -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, <items_key>: [{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);
+23
View File
@@ -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}'],
+53
View File
@@ -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);