feat: Fase 3 con campos condicionales; fix del monitor del ERP

Cosecha, polinizacion y fertilizacion en un solo flujo en vez de tres: skip_if
muestra el producto solo a las dos que lo llevan y el pase solo a polinizacion.
El lote se pregunta antes que el producto porque la dosis depende de los dos —
sale de la fertilizacion que se autorizo en su modulo, no la elige el usuario.

ErpMonitor construia api_base_url + /health, que no existe en el ERP: la ruta
correcta es ?peticion=ping y la URL sale de company_endpoints como el resto. Por
eso el panel mostraba el ERP caido. Tampoco mandaba el Bearer.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Lizandro Guarnizo
2026-08-19 21:01:38 -05:00
co-authored by Claude Sonnet 4.6
parent 8399a86972
commit 42adeb64d0
3 changed files with 156 additions and 3 deletions
+19 -3
View File
@@ -17,9 +17,24 @@ class ErpMonitor
public static function check(array $company): array
{
$baseUrl = rtrim($company['api_base_url'] ?? '', '/');
$healthUrl = $baseUrl !== '' ? $baseUrl . '/health' : '';
// El ERP no expone /health sino ?peticion=ping, y la URL sale de
// company_endpoints como el resto de las llamadas: api_base_url no
// sirve para esto. Sin esto el panel mostraba el ERP siempre caído.
$apiKey = $company['api_key'] ?? '';
$healthUrl = '';
try {
$stmt = db()->prepare(
"SELECT url FROM company_endpoints WHERE company_id = ? AND endpoint_key = ? AND is_active = 1 LIMIT 1"
);
$stmt->execute([(int)$company['id'], 'numeros_dn']);
$url = (string)($stmt->fetchColumn() ?: '');
if ($url !== '') {
// Cualquier endpoint sirve de referencia; se reemplaza por ping
$healthUrl = preg_replace('/peticion=[^&]*/', 'peticion=ping', $url);
}
} catch (\PDOException $e) {
$healthUrl = '';
}
if ($healthUrl === '') {
return [
@@ -27,7 +42,7 @@ class ErpMonitor
'company_name' => $company['name'] ?? '',
'status' => 'unknown',
'latency_ms' => null,
'error' => 'Sin URL configurada',
'error' => 'Sin endpoint registrado para esta empresa',
'last_check' => date('Y-m-d H:i:s'),
];
}
@@ -40,6 +55,7 @@ class ErpMonitor
CURLOPT_TIMEOUT => 10,
CURLOPT_CONNECTTIMEOUT => 5,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . $apiKey,
'X-API-Key: ' . $apiKey,
'User-Agent: bot-palmas360-monitor/1.0',
],
+95
View File
@@ -251,6 +251,7 @@ $configJson = [
['id' => 'registrar_ciclo', 'modulo' => 'ciclos', 'title' => '🔄 Ciclos', 'description' => 'Abrir o cerrar lotes'],
['id' => 'registrar_mantenimiento','modulo' => 'mantenimiento', 'title' => '🔧 Mantenimiento', 'description' => 'Avance sobre lotes pendientes'],
['id' => 'registrar_labor', 'modulo' => 'labores', 'title' => '💪 Labor diaria', 'description' => 'Tractorista, transporte, horas extra'],
['id' => 'registrar_labor_validada','modulo' => 'labores', 'title' => '🌾 Cosecha y otros','description' => 'Cosecha, polinización, fertilización'],
],
],
],
@@ -652,6 +653,96 @@ $configJson = [
],
],
// Fase 3: labores con validacion previa. Un solo flujo con campos
// condicionales en vez de tres: cosecha no lleva producto, y el pase
// es solo de polinizacion. El ERP valida inventario y autorizaciones,
// y si rechaza algo lo dice en la respuesta.
'registrar_labor_validada' => [
'type' => 'collect_and_post',
'endpoint_key' => 'labores_up',
'header' => '🌾 Cosecha, polinización y fertilización',
'confirm' => true,
'success_text' => '✅ Enviado a Palmas360.',
'back' => 'enviar_informacion',
'nlu_description' => 'Registrar labor de cosecha, polinización o fertilización',
'fields' => [
[
'key' => 'tipo',
'label' => 'Tipo',
'type' => 'static_select',
'prompt' => '¿Qué vas a registrar?',
'options' => [
['id' => 'cosecha', 'label' => '🌾 Cosecha'],
['id' => 'polinizacion', 'label' => '🌸 Polinización'],
['id' => 'fertilizacion','label' => '🧪 Fertilización'],
],
],
[
'key' => 'novedad_id',
'label' => 'Labor',
'type' => 'select',
'prompt' => '¿Qué labor se hizo?',
'source_endpoint_key' => 'novedades_labor3_dn',
'value_field' => 'id',
'label_field' => 'nombre',
],
[
'key' => 'fecha',
'label' => 'Fecha',
'type' => 'date_quick',
'prompt' => '📅 ¿De qué fecha?',
'other_validate' => 'date',
'other_prompt' => 'Escribe la fecha (YYYY-MM-DD):',
],
[
'key' => 'lote_id',
'label' => 'Lote',
'type' => 'select',
'prompt' => '¿En qué lote?',
'source_endpoint_key' => 'lotes_finca_dn',
'value_field' => 'id',
'label_field' => 'label',
],
[
// Cosecha no lleva producto. La dosis sale de la
// fertilizacion autorizada para ese lote, por eso se
// pregunta despues del lote y no antes.
'key' => 'producto_id',
'label' => 'Producto',
'type' => 'select',
'prompt' => '¿Qué producto se aplicó?',
'source_endpoint_key' => 'productos_fertilizacion_dn',
'value_field' => 'id',
'label_field' => 'nombre',
'skip_if' => ['field' => 'tipo', 'not_in' => ['polinizacion', 'fertilizacion']],
'empty_text' => '⚠️ Ese lote no tiene fertilización autorizada pendiente.',
],
[
'key' => 'empleados',
'label' => 'Trabajadores',
'type' => 'multi_select',
'prompt' => '¿Quiénes la hicieron? (puedes marcar varios)',
'source_endpoint_key' => 'empleados_labor_dn',
'value_field' => 'id',
'label_field' => 'nombre',
'from_phone' => 'tercero_id',
],
[
'key' => 'cantidad',
'label' => 'Cantidad',
'type' => 'text',
'prompt' => '¿Qué cantidad?',
],
[
'key' => 'fase_polinizacion',
'label' => 'Pase',
'type' => 'text',
'prompt' => '¿Qué pase de polinización?',
'skip_if' => ['field' => 'tipo', 'equals' => 'polinizacion'],
],
],
],
// Upload
'enviar_informacion' => ['type' => 'menu', 'menu' => 'submenu_subir_ciclos', 'nlu_skip' => true, 'back' => 'show_main_menu'],
@@ -686,6 +777,7 @@ $configJson = [
['id' => 'registrar_ciclo', 'modulo' => 'ciclos', 'title' => '🔄 Ciclos', 'description' => 'Abrir o cerrar lotes'],
['id' => 'registrar_mantenimiento','modulo' => 'mantenimiento', 'title' => '🔧 Mantenimiento', 'description' => 'Avance sobre lotes pendientes'],
['id' => 'registrar_labor', 'modulo' => 'labores', 'title' => '💪 Labor diaria', 'description' => 'Tractorista, transporte, horas extra'],
['id' => 'registrar_labor_validada','modulo' => 'labores', 'title' => '🌾 Cosecha y otros','description' => 'Cosecha, polinización, fertilización'],
],
],
],
@@ -808,6 +900,9 @@ $endpoints = [
['key' => 'lotes_finca_dn', 'dir' => 'download', 'url' => $BASE . '?peticion=lotes_x_finca&finca_id={finca_id}'],
['key' => 'empleados_labor_dn', 'dir' => 'download', 'url' => $BASE . '?peticion=empleados&estado=1&items=200'],
['key' => 'labores_up', 'dir' => 'upload', 'url' => $BASE . '?peticion=labores_up'],
// Fase 3: las que arrastran inventario o autorizacion previa
['key' => 'novedades_labor3_dn', 'dir' => 'download', 'url' => $BASE . '?peticion=novedades_labor_dn&fase=3'],
['key' => 'productos_fertilizacion_dn', 'dir' => 'download', 'url' => $BASE . '?peticion=productos_fertilizacion_dn&lote_id={lote_id}'],
['key' => 'lotes_mantenimiento_dn', 'dir' => 'download', 'url' => $BASE . '?peticion=lotes_mantenimiento_dn&grupo={grupo_id}&finca_id={finca_id}'],
['key' => 'lotes_mantenimiento_todos_dn', 'dir' => 'download', 'url' => $BASE . '?peticion=lotes_mantenimiento_dn&grupo={grupo_id}&finca_id={finca_id}&todos=1'],
// Todos los lotes (PDF) por ciclo
+42
View File
@@ -580,6 +580,48 @@ foreach (['novedades_labor_dn', 'lotes_finca_dn', 'empleados_labor_dn', 'labores
check("endpoint {$k} registrado", isset($eps[$k]), true);
}
echo "\nFase 3 — campos según el tipo\n";
$f3 = $flows['registrar_labor_validada'] ?? [];
$c3 = array_column($f3['fields'] ?? [], null, 'key');
check('ofrece los tres tipos',
array_column($c3['tipo']['options'], 'id'), ['cosecha', 'polinizacion', 'fertilizacion']);
check('el producto sale de la fertilización autorizada del lote',
$c3['producto_id']['source_endpoint_key'] ?? null, 'productos_fertilizacion_dn');
/** Espejo de capShouldSkip() */
$saltear = function (array $campo, array $col): bool {
$si = $campo['skip_if'] ?? null;
if (!$si) return false;
$v = (string)($col[$si['field']] ?? '');
if (isset($si['not_in'])) return !in_array($v, array_map('strval', $si['not_in']), true);
if (isset($si['equals'])) return $v !== (string)$si['equals'];
return false;
};
$pedidos = function (array $campos, string $tipo) use ($saltear): array {
$col = ['tipo' => $tipo];
return array_values(array_filter(array_keys($campos),
fn($k) => !$saltear($campos[$k], $col)));
};
// Los tres formularios del pedido, sin escribir tres flujos
check('cosecha: sin producto ni pase',
$pedidos($c3, 'cosecha'), ['tipo', 'novedad_id', 'fecha', 'lote_id', 'empleados', 'cantidad']);
check('fertilización: con producto, sin pase',
$pedidos($c3, 'fertilizacion'), ['tipo', 'novedad_id', 'fecha', 'lote_id', 'producto_id', 'empleados', 'cantidad']);
check('polinización: con producto y pase',
$pedidos($c3, 'polinizacion'), ['tipo', 'novedad_id', 'fecha', 'lote_id', 'producto_id', 'empleados', 'cantidad', 'fase_polinizacion']);
// El producto se pregunta despues del lote: la dosis depende de los dos
$orden = array_keys($c3);
check('el lote se pregunta antes que el producto',
array_search('lote_id', $orden) < array_search('producto_id', $orden), true);
foreach (['novedades_labor3_dn', 'productos_fertilizacion_dn'] as $k) {
check("endpoint {$k} registrado", isset($eps[$k]), true);
}
echo "\nAlcance de fincas por número\n";
/**