100 Commits
Author SHA1 Message Date
Lizandro GuarnizoandClaude Sonnet 4.6 673c15dae3 fix: los lotes para abrir ciclo salian en orden alfabetico
El pedido es mostrar los 10 con los ciclos mas largos —los que toca abrir— y
una opcion para ver el resto. La apertura ofrecia todos los lotes de la finca
por nombre, sin señal de cual estaba mas atrasado.

Ahora apertura y cierre traen 10 ordenados por dias, con los dias en la
etiqueta, y "Otro lote" cambia al listado completo sin perder lo ya marcado.
Si ningun lote tiene historial la consulta cae sola en alfabetico, porque el
DATEDIFF queda nulo y desempata por nombre.

Solo toca el flujo de carga; los informes quedan como estaban.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-08-11 21:09:51 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 d6bedfba54 chore: el seed crea las columnas del perfil por numero
tercero_id y modulos_json se crean desde el seed en vez de con un ALTER a mano:
ya se corre en cada despliegue y es idempotente, asi que no hay un paso manual
que alguien pueda saltearse.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-08-11 20:27:09 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 724179a3c6 feat: perfil por numero — trabajador vinculado y modulos permitidos
Dos perfiles, sin agregar ningun campo para distinguirlos:

- Con tercero vinculado en el ERP, el numero es de un trabajador: from_phone
  pre-llena empleado_id y el flujo se saltea el buscador. Sale gratis porque
  empleados y terceros son la misma tabla, asi que el id sirve tal cual.
- Sin vinculo es supervisor y elige de quien es el registro.

Los modulos habilitados (ciclos, ausentismos, produccion, pluviometria) llegan
del ERP y filtran las filas de menu. Las filas sin modulo declarado no se tocan,
asi la navegacion queda intacta.

Se mantiene la convencion de las fincas: nada marcado significa todo. Los
numeros que ya existen siguen igual sin migrar nada.

Requiere en la base del bot:
  ALTER TABLE company_phones
    ADD COLUMN tercero_id INT NULL, ADD COLUMN modulos_json TEXT NULL;
El codigo tolera que no existan todavia.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-08-11 20:21:32 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 94d4dcd26a refactor: el alcance de fincas lo resuelve el ERP, no el bot
Los catalogos ahora mandan ?wa= igual que los informes, y el endpoint fincas
devuelve solo las asignadas a ese numero. El "Todas las fincas" se antepone
unicamente a quien no tiene restriccion: con dos de cinco asignadas no
significa nada claro.

Con eso sale sobrando todo el filtrado del lado del bot —scope_from,
alcanceDelNumero y fincas_json en PhoneSync— y queda una sola fuente de verdad,
del lado donde estan los datos.

Efecto util: con una sola finca asignada el catalogo trae un unico item y
handleDynamicList auto-selecciona, asi que el usuario nunca ve la pregunta.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-08-05 23:59:35 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 478ba3ea73 feat: cada numero ve solo las fincas que el ERP le asigno
PhoneSync guarda el alcance que llega en whatsapp_numeros, y ask_finca acota la
lista con scope_from. Lista vacia = sin restriccion, para que quien tiene todas
vea tambien las fincas que se creen despues.

"Todas las fincas" sobrevive al filtro y pasa a significar "todas las mias"; de
acotarla se encarga el ERP, que es donde estan los datos.

Tolera que la columna todavia no exista: mientras tanto nadie queda restringido.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-08-05 23:53:34 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 eb0ae1f208 feat: los informes mandan el numero de quien pregunta
La API autentica por token de empresa, asi que el ERP veia "Rosa Blanca pide
produccion" y no podia acotar la respuesta a las fincas asignadas a esa
persona. Ahora cada consulta lleva ?wa= con el numero.

Va en executeApiReport y no en las URLs del seed porque los treinta informes
pasan por ahi: una linea los cubre a todos.

Sin efecto hasta que el ERP resuelva el alcance; mientras tanto es un
parametro que nadie lee.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-08-05 23:39:05 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 6a6f2ef44a fix: decir una categoria a secas devolvia el menu principal
Los submenus estaban marcados nlu_skip para que la IA no ruteara a navegacion,
pero eso dejo solo las hojas en el catalogo: "ausentismo" tenia cuatro
candidatos y ninguno se llamaba asi, "sanidad" ninguno, "produccion" dos. Como
no podia elegir, caia al fallback y aparecia el menu principal, que se siente
como que ignoro el mensaje.

Las cuatro categorias vuelven a ser ruteables. Para que el filtro de permisos
las alcance —por tipo un menu no es ni subida ni descarga— se agrega nlu_dir,
con lo que cat 1 solo ve registrar y cat 2 solo consultar, y la ambiguedad se
resuelve sola. Cat 3 ve las dos, asi que tiene un menu que pregunta cual.

Cuando aun asi no rutea, ahora avisa antes de mostrar el menu.

Se quita el aprendizaje por usuario: el orden por uso lo resuelve Palmas360
sobre los registros reales, y dos criterios compitiendo sobraba.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-08-05 23:20:25 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 2d92867431 feat: los motivos de ausentismo mas usados aparecen primero
El catalogo llega ordenado alfabeticamente y son 17 opciones, asi que el motivo
habitual quedaba en cualquier lado y a veces en la segunda pagina.

El bot cuenta que elige cada usuario y los sube. Es por usuario y no global a
proposito: quien registra incapacidades registra sobre todo incapacidades, y
eso le sirve mas que un promedio de toda la operacion.

El orden de PHP 8 es estable, asi que los empates conservan el alfabetico. El
contador se limita a 20 entradas y sobrevive al reset que corre tras cada
registro. Va como opcion por campo (frecuentes) para no reordenar listas donde
el alfabetico importa, como los lotes.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-08-05 23:12:49 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 6ff2914162 fix: el saludo de cat 1 nombra los tres flujos que puede usar
Al sacar subir_ciclo_cosecha quedo nombrando solo ausentismo, cuando cat 1
tambien abre y cierra ciclos y carga pluviometria: eso tambien es reportar.

La prueba buscaba la frase vieja "subir ciclo"; ahora verifica los tres
flujos por separado.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-08-05 23:05:41 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 18c759e80d fix: dejar en el menu de envio solo los tres flujos de Fase 1
subir_ciclo_cosecha no correspondia. El audio menciona la cosecha una sola vez,
en la lista de Fase 3 —"amarrado al tiquete y hay que ver detalle de tiquete",
lo mas empinado y lo que deja de ultimo— y nunca detallo su flujo. Los tres
audios con paso a paso son pluviometria, ausentismo y ciclos.

Ademas el flow no hacia nada: upload_ciclo nunca se implemento.

El saludo de cat 1 tambien lo ponia de ejemplo; ahora nombra ausentismo.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-08-05 23:05:05 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 25a6ce7b1e fix: subir_ciclo_cosecha no respondia nada; marcar los que seran formulario
upload_ciclo estaba declarado en el seed y clasificado en el catalogo del NLU,
pero executeFunction nunca lo implemento: caia en default => null y el bot se
quedaba mudo al tocarlo. Pasa a ser un texto que avisa que esta en
construccion, hasta que exista el flujo de produccion.

Tampoco encajaba conceptualmente: abrir el ciclo de un lote ya lo hace
"Ciclos", y registrar la cosecha del dia es produccion, que va contra el
tiquete de bascula y es Fase 3.

Pluviometria y Ciclos avisan en su descripcion que van a pasar a formulario.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-08-05 23:01:26 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 d4325ce037 docs: dejar en FLOWS-META.txt solo los dos JSON
Las instrucciones y notas de verificacion salen del archivo para que el admin
copie y pegue directo. Quedan en el historial de git si hacen falta.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-08-05 09:13:39 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 8b9029a017 docs: verificar los Flows contra la documentacion de Meta
Componentes, propiedades y la accion complete confirmados contra los docs
oficiales. Lo unico sin confirmar es el numero de version: las paginas del
changelog no estaban accesibles. Queda anotado que hacer si el editor la
rechaza.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-08-05 08:47:50 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 598c3b4fda docs: especificacion de los dos Flows de WhatsApp para el admin de Meta
JSON completo de Ciclos y Pluviometria, con la configuracion que hay que poner
en WhatsApp Manager y las dos verificaciones previas a publicar.

Va versionado porque una vez publicado un Flow su estructura no se edita, y
conviene tener registro de que se publico.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-08-05 08:45:44 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 e5326bda8c feat: carga de ciclos — apertura y cierre de lotes con seleccion multiple
Ultimo flujo de Fase 1. Pregunta ciclo, accion, fecha y lotes, y postea a
ciclos_{ciclo}_up con lote_ids[], que es el contrato que quedo en el ERP.

Pieza nueva del motor, multi_select: la lista acumula, cada toque marca o
desmarca con un check en la etiqueta, y "Listo" cierra. WhatsApp no tiene
casillas, asi que el estado se muestra redibujando. De las 10 filas, dos van
para "Ver mas" y "Listo".

Tambien resolverPorCampos(), que reemplaza {campo} con lo ya elegido tanto en
la clave del endpoint como en la URL del POST. Con eso los seis tipos de ciclo
y las dos acciones comparten tres entradas de endpoint en vez de dieciocho:
apertura ofrece los lotes de la finca y cierre solo los que estan abiertos.

Mantenimiento no aparece: no tiene tabla de ciclos ni apertura/cierre, es una
labor y va con Fase 2.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-08-05 07:56:04 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 576ea7de40 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 <noreply@anthropic.com>
2026-08-04 22:16:17 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 f06a400d16 fix: pluviometria se quedaba muda tras elegir la fecha
El selector de fecha que agregue a collect_for_each responde por lista, pero el
bloque __foreach de processInteractive solo contemplaba confirmar y cancelar:
la fecha caia en la resolucion de menus estaticos, no coincidia con ninguno y
el flujo moria sin mensaje.

Hasta ahora collect_for_each solo recibia numeros escritos, por eso nunca hizo
falta ese ruteo. Solo se desvia mientras se espera la fecha, para no secuestrar
la navegacion por menus durante el resto del bucle.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-08-04 20:38:40 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 c0fb2dc089 fix: rechazar rangos de fecha invertidos en ausentismo
El analisis de Gemini en Palmas360 marco un ausentismo con fecha final
anterior a la inicial: el bot lo dejaba pasar y el error recien aparecia
frente al revisor.

min_field valida entre campos al guardar, por los dos caminos (opcion de
lista y fecha escrita a mano). El aviso se envia aparte para poder volver a
mostrar el selector debajo.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-08-04 20:33:00 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 76ee42b96e 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>
2026-08-04 20:22:03 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 0e72aa3d5a feat: flujo de registro de ausentismo (Fase 1, carga de datos)
Primer flujo de POST. El bot recolecta trabajador, motivo y rango de fechas, y
postea a ausentismos_up; Palmas360 decide si auto-aprueba o encola para
revision, como ya hace bot_entrada.

El campo lookup asumia que el endpoint devolvia un solo objeto, pero /empleados
devuelve una lista paginada: con eso el trabajador salia como "Desconocido" y
sin id. Ahora distingue lista de objeto, y si hay varias coincidencias las
ofrece para elegir en vez de quedarse con la primera.

Campos opcionales del contrato (eps, diagnostico_id) quedan fuera hasta
confirmar con Palmas360 que novedades son incapacidad.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-08-04 19:37:16 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 0bb1d3447e feat: bienvenida diaria; fix: el NLU filtraba JSON crudo al usuario
Bienvenida una vez por dia y por empresa, distinta por categoria (cat 1 no ve
ejemplos de descarga). Se manda aparte del retorno de process(), asi el usuario
recibe el saludo y acto seguido lo que pidio. __saludo sobrevive a
preservingReset para no repetirse tras cada informe.

Fix del leak: cuando la respuesta del modelo no parseaba como JSON se enviaba
cruda, y el usuario veia {"action":"chat","text":"...  Ahora se rescata solo el
texto, incluso de JSON truncado, y si no hay nada legible se devuelve vacio
para caer al menu de fallback.

Ademas el prompt le prohibe preguntar por datos que una opcion ya pide: ante
"descargar informe de mantenimiento" debe rutear al selector de grupo en vez de
preguntar por chat.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-08-01 11:06:35 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 8825f4e821 fix: el selector de empresas se comia los comandos de navegacion
isResetKeyword aceptaba menu, salir, atras, volver, inicio y regresar, o sea el
vocabulario completo de navegacion del bot. Ese chequeo corre en el webhook,
antes de NormalBot, asi que a un usuario que pertenece a varias empresas
cualquiera de esas palabras le borraba la sesion y lo devolvia al selector en
vez de navegar.

Ahora solo responde a palabras que signifiquen cambiar de empresa.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-08-01 11:01:30 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 a64421c527 feat: finca visible en cada menu, entities por campo y aviso de comandos
Entities:
- Se conserva el mapa {campo: valor} en vez de aplanarlo. El pre-llenado de
  formularios y el auto-registro de valores comparan la clave contra el nombre
  del campo, asi que con la lista plana nunca se disparaban.
- entity_key acota cada dynamic_list a su propio campo: antes una finca llamada
  "reposo" se comparaba contra grupos de mantenimiento y se descartaba ahi.
- Cada lista consume solo lo suyo, asi "finca reposo, grupo plateo" resuelve
  las dos cosas en una frase.

Finca visible:
- Se guarda la etiqueta ademas del id al elegir una opcion.
- El footer de cada menu muestra la finca activa; sin finca queda el nombre de
  la empresa, asi cat 1 no cambia. Los menus de botones ahora emiten footer.
- {finca} disponible en header, body y footer.

Aviso de comandos: los textos tras cada informe nombran menu, atras y salir.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-08-01 10:55:55 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 693623e10c fix: el seed ya no borra la configuracion de IA del admin
config_json guarda tanto la estructura del bot (menus, flujos, comandos) como
los ajustes que se editan desde el panel (ai_model, gemini_model, claude_model,
temperatura, prompt, whisper). El seed reescribia el JSON entero, asi que cada
corrida borraba lo segundo.

Ahora conserva toda clave de primer nivel que el seed no declara, e imprime
cuales conservo.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-08-01 10:46:07 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 c8909e75db fix(nlu): no adivinar cual informe de mantenimiento quiere el usuario
"informe de mantenimiento de plateo" no dice si quiere los mas atrasados o
todos los lotes, asi que entregar un PDF era adivinar. Ahora resuelve el grupo
por entity y muestra el submenu; solo va directo al informe cuando el usuario
lo nombra ("todos los lotes", "mas atrasados").

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-08-01 10:44:17 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 ce5d4ef2b2 fix: el NLU nunca atendia texto libre tras un informe; api_base_url apunta al ERP
preservingReset dejaba current_node en NULL, que process() no distingue de una
sesion nueva: se redisparaba el flujo de bienvenida (ask_finca), skip_if_set lo
saltaba al menu principal y el paso de NLU nunca corria. Por eso "elegir otra
finca" o "informe de mantenimiento de plateo" devolvian el menu. Ahora queda el
centinela __greeted, que marca sesion iniciada.

api_base_url guardaba el Graph API de WhatsApp, pero WhatsAppSender lo tiene
hardcodeado y todos los demas consumidores esperan el ERP.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-08-01 10:39:10 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 1b797df2f0 fix: PhoneSync usa company_endpoints en vez de api_base_url
api_base_url guarda el Graph API de WhatsApp (lo fija el propio seed), asi que
el cron armaba https://graph.facebook.com/v22.0/php/controller/... y recibia
401 cada 5 minutos desde siempre.

El endpoint numeros_dn ya estaba registrado con la URL absoluta correcta; ahora
la sincronizacion lo usa como el resto de llamadas al ERP.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-08-01 10:36:32 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 0e89258f67 feat: navegacion por niveles, intencion pendiente y NLU que aterriza en el informe
- "atras"/"volver"/"regresar" suben un nivel via mapa 'back' declarativo en el
  seed, en vez de saltar siempre al menu principal.
- "finca"/"cambiar finca" como comandos directos a reset_finca.
- requires/resolver: si el NLU rutea a un informe sin el dato que necesita, el
  bot lo pide y vuelve al informe original, no al submenu. Asi "informe de
  mantenimiento de plateo" entrega el PDF sin pasos intermedios.
- ciclo_mantenimiento consulta el catalogo completo cuando hay entities: los
  grupos fuera del top 3 tambien matchean.
- ask_finca con skip_if_set: deja de repreguntar la finca despues de cada
  informe; para cambiarla esta reset_finca.
- Boton "Otro grupo" en el submenu de mantenimiento.
- Fix: el auto-select por entities no mergeaba per_type y mandaba a cat 3 al
  menu equivocado al nombrar una finca.

validate_config.php verifica el grafo (back/resolver/botones/endpoints) y
test_navegacion.php recorre en seco los escenarios de ruteo.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-08-01 09:59:30 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 671fccd6b2 fix: mantenimiento PDF vacio — conservar grupo_mant y detectar placeholders
preservingReset borraba grupo_mant despues de cada informe, dejando
{grupo_id} sin resolver en la siguiente llamada del mismo submenu.
La API recibia grupo=0 y devolvia cero filas sin error visible.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-08-01 09:42:39 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 09db466fa6 fix: navegación — atras/cancelar/regresar + NLU desde menús activos
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-30 23:50:09 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 79d2399ddf fix: agregar comando 'salir' y limpiar __api_vars en reset de comandos
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-30 23:47:12 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 fe048f53a3 refactor: simplificar Por Categoría — sin toggle, greeting_flow directo
- Eliminar checkbox 'Pedir finca al inicio': innecesario porque el
  dynamic_list ya auto-selecciona cuando solo hay 1 opción (finca, empresa, etc.)
- greeting_flow controla si hay flujo de bienvenida; si está vacío → greeting_menu
- Si greeting_flow tiene valor → greeting='' (dispara el flow) y se oculta greeting_menu
- Hint text explica que con 1 sola opción se auto-selecciona sin preguntar
- save-per-type deriva greeting/greeting_menu del greeting_flow, no de un toggle

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-30 23:19:50 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 909c6be32f feat: expandir tab Por Categoría en bot-config
- Toggle "Pedir finca al inicio" por categoría → maneja greeting/greeting_flow
- Campo editable de greeting_flow (pre-llena ask_finca al activar el toggle)
- Selector de menú inicial (greeting_menu, visible cuando NO pide finca)
- Selector de flujo de respaldo (fallback_flow) por categoría
- Datalist con flows y menús disponibles para autocompletar
- save-per-type actualiza greeting, greeting_flow, greeting_menu y fallback_flow
  preservando menus/flows/commands del seed en cada categoría
- Cat 1: sin finca, configura greeting_menu desde admin
- Cat 2/3: pide finca activado, greeting_flow = ask_finca

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-30 23:14:49 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 0ddab6adcf feat: NLU routing + fix greeting para cat2/cat3 + nlu_descriptions
- Cat 2 y cat 3: greeting '' (no null) para que greeting_flow: ask_finca
  se dispare en el primer mensaje y el usuario seleccione su finca
- nlu_enabled: true en config root activa el routing inteligente
- NormalBot: step 6 llama AiBot::routeOrChat() antes del fallback_flow;
  si retorna 'route' ejecuta el flow; si 'chat' responde en texto libre
- reset_finca: nuevo flow+función que limpia el meta de finca y relanza
  ask_finca; el NLU puede enrutar "quiero cambiar de finca" aquí
- nlu_description en todos los flows consultables por el NLU; nlu_skip
  en menus de navegación interna para no exponerlos al modelo
- AiBot::buildFlowCatalog: agrega upload_ciclo a isUpload para que cat 2
  no vea flujos de subida en el catálogo del NLU

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-30 23:03:25 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 7fc36f1ebf fix: renombrar Rincofurus → Palmarum en menú sanidad vegetal
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-30 22:28:29 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 e223438f36 fix: timezone America/Bogota en punto de entrada del bot
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-30 22:24:10 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 93a33de819 fix: restaurar finca_id en mantenimiento; filtro correcto por finca
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-30 22:02:25 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 fbd48b2781 fix: mantenimiento no filtra por finca (grupos son de toda la empresa)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-30 21:58:05 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 855fe25159 feat: mantenimiento top muestra texto; quita opción histórico 30d
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-30 21:24:47 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 fd907c8cc0 fix: __valid_ids usa strval para evitar mismatch int vs string
buildDynamicListResponse castea IDs a string al armar la lista de
WhatsApp. La comparación estricta in_array fallaba si el JSON devolvía
el id como int. Ahora __valid_ids siempre contiene strings.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-30 21:03:05 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 620ae3a1b3 fix: dynamic_list next_node busca en menus si no está en flows
Cuando el next_node de un collecting está en menus (no flows),
handleCollectingInput retornaba null sin respuesta. Ahora intenta
buildMenuResponse como fallback, lo que corrige el flujo de
selección de grupo en mantenimiento.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-30 20:55:40 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 db46ced033 feat: PDF producción cambia caption y filename a 'año'
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-30 20:45:37 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 b0462a7675 fix: seed no sobreescribe api_key en companies existentes
api_key contiene el token ERP de PALMAS360 configurado manualmente.
Se retira del ON DUPLICATE KEY UPDATE para que cada seed run no lo pise.
Solo aplica al INSERT inicial de una empresa nueva.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-30 20:38:12 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 591b29b966 feat: menú producción cambia 'Total del mes' a 'Totales facturados'
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-30 20:17:58 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 5dda66d27f fix: ciclo_*_todos usa date_mode=today en vez de current_month
La fecha que pasan estos nodos al endpoint es un punto de corte (hoy),
no un rango mensual. current_month enviaba fecha_desde=primer-día-mes
que el controller pasaba al modelo como referencia de días.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-30 20:07:16 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 fc23448509 chore: limpiar seed — eliminar clave y endpoints huérfanos
- Eliminada entrada duplicada 'ciclo_mantenimiento' tipo menu (reemplazada
  por el dynamic_list de abajo en el mismo array)
- Eliminado endpoint 'mantenimiento_top_texto_bot_dn' (nunca referenciado
  por ningún nodo de flujo)
- Eliminado endpoint duplicado 'mantenimiento_todos_dn' (URL idéntica a
  mantenimiento_todos_bot_dn, sin usos)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-30 18:09:29 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 f23283124a fix: preservar finca_id al resetear contexto tras generar reporte
El reset post-reporte borraba meta['finca']['finca_id'] causando que
todos los informes siguientes se generaran sin filtro de finca.

Nuevo helper preservingReset() restaura el grupo 'finca' después del
reset para que el usuario solo seleccione su finca una vez por sesión.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-30 17:58:10 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 5b0bd3b8b8 fix: al seleccionar id=0 en lista se activaba reset del multi-company
El id "0" de "Todas las fincas" coincidía con el keyword "0" en
isResetKeyword, borrando la sesión multi-empresa y mostrando el
pre-menú de empresa en vez de procesar la selección de finca.

Solución: isResetKeyword solo aplica para mensajes de texto libre;
las respuestas interactivas (list_reply/button_reply) nunca son
comandos de reinicio.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-30 16:43:07 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 c5955160bb feat(bot): mantenimiento con selección dinámica de grupo
- ciclo_mantenimiento → dynamic_list top 3 grupos más usados + "Otro grupo"
- "Otro grupo" → lista grupos 4-13 (append_options redirect)
- Tras seleccionar grupo → menú 3 botones: más largos, todos, histórico 30d
- Endpoints con {grupo_id} dinámico en URL

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-29 20:22:32 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 bc4b589d93 fix(bot): reordenar sanidad vegetal y renombrar Palmarum→Rincofurus
Orden correcto: Censo, Plagas, Rincofurus, Tratamiento.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-29 16:57:18 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 7120bd9675 feat(bot): sub-menú de 3 opciones por cada ciclo
Cada ciclo (cosecha, polinización, plagas, censo, palmarum,
tratamientos, mantenimiento) ahora presenta un menú de botones con:
  1. Más largos — texto top 10 con fecha actual
  2. Todos los lotes — PDF filtrado por finca
  3. Histórico 30d — PDF matriz (excepto mantenimiento)

Se agregan endpoints *_todos_dn apuntando a ciclos_*_dn (PDF).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-29 16:55:47 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 6572b9fcf6 feat(menus): reorganizar ciclos con sub-menú de sanidad
Estructura: Cosecha | Polinización | Sanidad (Plagas/Censo/Palmarum/Tratamientos) | Mantenimiento

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-29 16:45:04 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 2d98a9e7e0 fix(bot): enviar texto via WhatsAppSender antes de retornar PDF encadenado
El return de sendText() se descartaba; ahora se llama WhatsAppSender::sendText()
directamente para que el texto llegue antes del PDF/Excel.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-29 16:36:09 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 bd8544fce4 fix(seed): corregir flows y URLs de endpoints
- ask_finca como dynamic_list con meta_group=finca
- ciclos usan api_report con next_endpoint_key para texto+PDF
- produccion_total encadena texto → PDF
- ausentismos usan date_mode en vez de funcion inexistente
- URLs de endpoints sin {fecha_desde}/{fecha_hasta} (date_mode los agrega)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-29 16:23:00 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 2329df6ac9 feat(seed): agregar endpoint numeros_dn para sincronización de teléfonos WhatsApp
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-29 16:09:56 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 eb296ee078 fix(migrate): agregar columnas params y body_fields en company_endpoints
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-29 16:05:10 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 26b11d99e2 fix(migrate): agregar columna name en company_endpoints
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-29 15:58:30 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 0c82348641 fix(migrate): agregar tabla sessions faltante para DbSessionHandler
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-29 15:35:54 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 f378d02768 feat(setup): seed completo de companies, endpoints y menus restaurados
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-29 15:14:04 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 4b710ef33b test: script de validación endpoints ROSABLANCA
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-28 22:21:27 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 fe4e638667 feat(sync): cron de sincronización automática de números WhatsApp desde PALMAS360
- PhoneSync::syncAll() itera todas las empresas activas, llama a
  whatsapp_numeros de cada ERP y hace UPSERT en company_phones
- Números eliminados en PALMAS360 quedan is_active=0 automáticamente
- cron/sync_phones.php es el entry point; configurar cada 5 min en crontab

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-28 22:10:04 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 e3d9aac26d feat(bot): add last_7 date_mode y next_endpoint_key para texto+PDF en cadena
- last_7: fecha_desde = hoy-7 días, fecha_hasta = hoy (para ausentismos semana)
- next_endpoint_key en executeApiReport: si la primera llamada retorna JSON texto
  con status=1, envía el texto al usuario y luego ejecuta el endpoint siguiente
  (ej: produccion_total → texto inmediato + PDF a continuación)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-28 21:59:16 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 0df08bf953 feat: append_options en dynamic_list con redirect a otro flow
Permite añadir opciones fijas al final de una lista dinámica. Si el
usuario elige una de esas opciones, el bot redirige al flow con ese ID
en vez de guardar el valor como campo del formulario. Usado para el
botón "Ver más grupos" en el flujo de mantenimiento.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-28 20:29:51 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 91dd84cc8d feat: auto-select finca cuando la lista tiene un solo ítem
Si el endpoint de fincas devuelve exactamente 1 opción, el bot la
selecciona automáticamente sin mostrar la lista al usuario, y navega
directo al menú principal usando los flows del per_type activo.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-28 20:12:35 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 060041c855 fix: apply-menus hace merge en vez de reemplazar config completo
Antes sobreescribía todo el config_json con solo los flows base,
borrando nlu, whisper_*, gemini_*, flows custom y menus custom.
Ahora solo actualiza los flows/menus definidos en el script y
preserva todo lo demás que tenga la empresa en la BD.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-14 15:36:20 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 976f61b682 fix: match fuzzy de label en dynamic_list y collecting mid-flow
- handleDynamicList: usa str_contains bilateral para que "mantenimiento plateo"
  coincida con label "Plateo" (el NLU suele incluir palabras extra)
- handleCollectingInput: si el texto transcrito no es un ID válido, intenta
  match fuzzy contra __label_map guardado en el estado collecting
- handleDynamicList: guarda __label_map en el estado collecting para
  que mid-flow audio pueda auto-seleccionar sin ID exacto

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-05 22:03:57 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 3d04be14bc feat: auto-selección en dynamic_list cuando NLU extrae el grupo/filtro
- handleDynamicList: si __nlu_entities contiene un valor que coincide
  con el label de un ítem, lo auto-selecciona y navega al next_node
  sin mostrar la lista interactiva
- AiBot: NLU prompt ahora pide explícitamente grupos y filtros en entities

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-05 21:57:22 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 54a93adea9 feat: one-shot audio entity extraction y pre-fill de formularios
- AiBot: NLU ahora extrae entities (finca, valor, etc.) del audio
- BotRouter: guarda __nlu_entities en metadata antes de enrutar
- NormalBot: handleCollectAndPost pre-rellena campos desde entities
  y capAskNext salta los ya completados
- NormalBot: handleCollectForEach registra ítem directo si entities
  trae finca+valor coincidente, sin iterar todo el listado
- BotRouter: mid-flow audio va a NormalBot directamente (no NLU)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-05 21:48:48 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 d0c1b1d3be feat: mid-flow audio — transcribed text goes to NormalBot, not NLU
When a user sends audio while in an active flow (__cap, __foreach,
collecting, __api_vars), the Whisper transcription is passed directly
to NormalBot as the step answer instead of going through NLU routing.

Bot still shows '🎤 Escuché: _texto_' confirmation before processing.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-05 21:38:25 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 1114c52df4 fix: wrap collected values in __ep_vars_combined for URL substitution
substituteUrlVars requires flat key-value pairs under __ep_vars_combined;
passing collected directly as meta skipped substitution since values are
strings not arrays, so {finca_id} was never replaced in the lotes URL.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-05 21:30:50 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 eed8116c66 feat: support nlu_skip flag to exclude intermediate flows from NLU catalog
Flows marked with nlu_skip:true are never offered to the NLU router,
preventing the AI from jumping directly to steps that require prior
data collection (e.g. submenu_mantenimiento_fecha needs grupo selected first).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-05 21:13:34 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 09342fa786 feat: pass collected values to fetchDynamicList for dependent selects
Enables select fields to filter their list based on previously collected
values via URL variable substitution (e.g. lotes filtered by finca_id).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-05 20:57:18 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 2bdbde50f5 fix: add response_format=json to self-hosted Whisper request
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-05 20:39:13 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 79eda71c5a fix: whisper_url takes priority over ai_provider for audio transcription
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-05 20:19:28 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 816c6d27c8 feat: show whisper-own in ai-logs filter and badge
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-05 20:16:18 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 fc54ec992b feat: self-hosted Whisper server support
- MediaTranscriber: if whisper_url set, POSTs to own server with
  Basic auth (audio_file field); falls back to OpenAI cloud Whisper
- Admin AI tab: new section for whisper_url / whisper_user / whisper_pass
- save-ai endpoint: saves the three whisper fields to config_json

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-05 20:07:49 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 9509c3b4fc feat: add skip_if support to collect_and_post flow fields
Allows fields to declare a skip_if condition so the bot skips them
based on previously collected values — used for eps/diagnostico fields
that only apply when novedad is an incapacidad type.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-05 20:07:44 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 497743ceb1 feat: NLU fallback — route unrecognized text to AI in hybrid+NLU mode
NormalBot.process() gains suppressFallback param. When true, steps 7-8
(greeting menu fallback, static fallback text) return null instead of
responding, letting BotRouter hand off to NLU/AI.

BotRouter.runHybridBot() passes suppressFallback=$nluEnabled so that:
- NLU on: unrecognized text → NLU → route to flow or AI chat
- NLU off: existing fallback behavior unchanged
- Commands, active flows, first-session greeting: always unaffected

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-05 09:09:49 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 54d778783e fix: use Layout::open/close in aiLogs() — page() does not exist
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-05 09:06:50 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 9852a749e6 fix: remove broken duplicate query in aiLogs()
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-05 09:05:05 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 5506ebd2af feat: AI usage logging (AiLogger, ai_logs table, admin view)
- AiLogger.php: new service, inserts into ai_logs on every AI call
- ai_logs table: added to migrate.php (provider, model, call_type, tokens, duration_ms)
- AiBot: logs chat, media_response, nlu calls with timing and token counts
- MediaTranscriber: logs whisper, geminiAudio, openaiVision, geminiVision, claudeVision
- /admin/ai-logs: paginated table with provider/company filter
- Layout.php: added IA Logs nav link
- index.php: require AiLogger before AiBot; added GET /admin/ai-logs route

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-05 09:03:21 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 1d9484a1e0 Fix NLU flow: media_id, upload perm, catalog filter, nlu_description field
- WpWebhook: inject media_id into context before calling BotRouter so
  MediaTranscriber can download audio/image (was passing caption='')
- BotRouter: use context['media_id'] for transcription (non-breaking fallback)
- AiBot::processMedia: fix canUpload to include permType 1 (solo reporta)
- AiBot::buildFlowCatalog: include text flows that have nlu_description set
- DashboardController + index.php: add nlu_description field per flow card

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-05 08:51:13 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 bb4b5dab10 Fix AI tab save: dedicated AJAX endpoint /save-ai
Instead of relying on the native HTML form (which couldn't reliably
isolate AI fields), the IA tab now uses an AJAX call identical to
save-per-type. The new /admin/bot-config/save-ai endpoint only
reads and merges AI fields into config_json, preserving menus/flows.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-05 08:28:41 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 5afa846b4e Add per-tab save buttons in bot-config page
Each tab (Comandos, Menús, Flujos, IA, General) now has its own
submit button so the user doesn't have to scroll to the bottom.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-04 22:16:04 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 eb436638fb fix: AI tab settings not saving — read from DOM in bot-config save JS
The JS FormData was built from the CONFIG object (DB state), missing
all AI inputs the user types: api_key, models, checkboxes (nlu,
ai_for_media). Now reads them directly from DOM before the fetch.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-04 22:04:52 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 9e36a965c9 feat: NLU routing + audio/image transcription via AI
- NluRouter in AiBot: builds permission-filtered flow catalog, asks AI
  to route user intent to exact flow key or return chat response
- MediaTranscriber: downloads WA media, transcribes audio (Whisper/Gemini),
  extracts image intent (OpenAI/Gemini/Claude vision)
- BotRouter hybrid: text → NormalBot → NLU fallback if no match;
  audio/image → transcribe → NLU route; sends transcript preview first
- Admin IA tab: NLU checkbox with description
- Per-company nlu flag saved in config_json

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-04 21:50:01 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 2b859e3dc0 feat: multi-provider AI with per-company override and Claude support
- Global settings: custom AI panel with OpenAI/Gemini/Claude each with
  API key, model selector, and live test-connection button (no save needed)
- Per-company IA tab: same three providers + fallback to global when empty
- AiBot: reads per-company config_json first, falls back to env() global
- Added callClaude() for Anthropic Messages API
- More OpenAI models: gpt-4.1, gpt-4.1-mini
- test-ai endpoint: accepts provider/api_key/model from POST for live test

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-04 19:06:51 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 b82fcf6aaa feat: add Gemini 2.5 Flash as default model option
Added gemini-2.5-flash to the admin panel selector (first/default option)
and updated the fallback default in AiBot and migrate seed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-04 18:52:08 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 5883801989 fix: prepend api_base_url to relative endpoint URLs in NormalBot
After migration stripped base URLs from DB records, bot was calling
relative paths (e.g. /ruta) as-is without the base. buildUrl() helper
now prepends api_base_url for all four curl call sites.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-04 16:25:10 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 f06be3584f feat: two-message post-selection flow in multi-company pre-menu
After user selects a company, send short confirmation text then reset
bot_context and trigger BotRouter with empty message to fire the greeting.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-04 16:18:41 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 8f43ccf4c2 feat: pre-menú de selección de empresa para números multi-empresa
Cuando un número pertenece a más de una empresa, el bot muestra un
menú interactivo "¿Con cuál empresa deseas comunicarte?" antes de
cualquier flujo normal. La selección se cachea 8h en la tabla
multi_company_sessions. Keywords de reinicio (salir, inicio, menú...)
limpian la sesión y vuelven a mostrar el pre-menú.
Números de una sola empresa: flujo intacto.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-04 16:07:52 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 b349230362 chore: script de migración para quitar base URL de endpoints
Recorre company_endpoints y elimina el prefijo api_base_url de
las URLs absolutas, dejando solo la ruta relativa.
Dry-run por defecto (?run=1 para ejecutar).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-04 15:35:58 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 43933bfe3a feat: endpoints usan api_base_url como base, rutas relativas en UI
- Banner en tab Endpoints muestra la URL base de General
- Inputs de URL aceptan rutas relativas (?param=x o /ruta)
- Preview live de la URL completa (base+ruta) bajo cada input
- Backend prepend api_base_url en test y sync si URL no es absoluta
- Compatible hacia atrás: URLs absolutas existentes siguen funcionando

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-04 15:19:46 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 bfbc70451d feat: clonar endpoints desde otra empresa en pestaña Endpoints API
Agrega panel de clonación en la pestaña Endpoints API del editor de empresa.
Permite copiar los endpoints de otra empresa con dos modos: sobreescribir todo
o solo añadir los endpoints que aún no tienen URL configurada.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-04 13:50:39 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 34322ae3dc Change "Reporte enviado" to "Reporte generado"
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-30 20:10:09 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 97a48d0fe4 Validate dynamic_list input and fix cosecha button IDs
- dynamic_list now stores valid IDs in collecting state; rejects
  free-text input that doesn't match a list option
- Fixed submenu_ciclo_cosecha button IDs to match real flow names
  (ciclo_cosecha_dia / ciclo_cosecha_historico)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-30 20:07:52 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 0ce766a9eb feat: kilos por lotes con selección de período (mes actual/anterior/personalizado)
- Add last_month date_mode to executeApiReport
- prod_kilos_lotes now shows period menu instead of immediately asking dates
- lotes_mes_actual / lotes_mes_anterior use api_report with date_mode
- lotes_personalizado collects fecha_desde + fecha_hasta (2 messages only when needed)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-30 19:35:12 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 601169525e fix: dedicated save button for categories + preserve all extra DB keys
- Replace onchange+AJAX with explicit button '💾 Guardar categorías'
  that shows alert on success/error for clear feedback
- Get company_id from DOM (not COMPANY_ID JS const from another script block)
- Main form save now preserves welcome_menu and per_type from DB even if
  POST is truncated by max_input_vars
- Restore welcome_menu='welcome_msg' in DB (was wiped by a previous save)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-30 19:16:36 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 ae5b7266d4 fix: per_type is DB-only, main form save never touches it
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-30 19:11:54 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 3c79fac8d7 fix: preserve per_type from DB on main form save
The main save rebuilt config from scratch — per_type always started empty,
so clicking Guardar wiped whatever savePerType() had just stored.

Now: load existing DB config first, use its per_type as the base, apply
POST per_type_menu on top only when it is present. Same for welcome_menu.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-30 19:09:43 -05:00