- "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>
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>
- 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>
- 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>
- 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>
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>
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>
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>
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>
- 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>
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>
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>
- 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>
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>
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>
- 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>
- 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>
- 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>
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>
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>
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>
- 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>
- 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>
- 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>
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>
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>
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>
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>
- 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>
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>
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>
- 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>
- 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>
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>
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>
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>
- 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>
- 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>
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>
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>
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>
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>
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>
- 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>
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>
- 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>
- 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>
- 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>
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>
The main form exceeded max_input_vars (27 flows × 37 fields each) so PHP
silently dropped per_type_menu before reaching the save handler.
- Add POST /admin/bot-config/save-per-type — reads existing DB config,
merges only the per_type_menu values, saves back
- Per-type selects fire savePerType() onchange → AJAX → toast on success
- Add public/.user.ini with max_input_vars=5000 as belt+suspenders
- Remove array_is_list() (PHP 8.1) — replaced with PHP 7.4 compatible check
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Store PHP sessions in the `sessions` MySQL table (30-day TTL) instead of
server files, which disappear on container restart / redeploy.
Cookie lifetime also extended to 30 days.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Show welcome_msg (button with "Ver menú") for all new sessions before category menu
- Fix saveConfig() AJAX to include per_type_menu DOM values so they survive individual saves
- Add welcome_menu field to General tab in admin (render + save)
- Add 'atras' command alias (→ show_main_menu)
- Add ciclos_plagas, ciclos_palm, ciclos_tratamientos flows to DB config
- Fix per_type in DB from [] to {} (proper JSON object)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When user says "menu/inicio/volver", bot now shows the menu configured
for their category (per_type.greeting_menu) instead of the global
show_main_menu. Same redirect applies to interactive button taps.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
WhatsApp rejects list messages with empty body.text. Using ?? missed
empty string from config; ?: correctly falls back to default.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
API returns {status,datos:[...]} but bot was looking for {data:[...]},
causing empty rows and dynamic lists never rendering.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Fixed vars use their preset (month_start, current_date, etc.); ask-mode
vars fall back to today's date so the test always sends a valid URL.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Admin: each endpoint with {vars} shows a vars config section; per-var
mode is "ask user" (bot prompts in WhatsApp) or "fixed" (preset: today,
month_start, month_end, last_7, last_30, year_start, or literal)
- Bot: resolves fixed vars before calling endpoint; queues "ask" vars and
collects them sequentially from the user before executing the report
- date_mode now uses fecha_desde/fecha_hasta (ERP expected names); added
current_month mode
- JSON responses from ERP (sin_datos etc.) shown as text, not uploaded as file
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Complete endpoint specification for the ERP team:
- 10 GET (download) endpoints returning PDF/Excel files
- 6 POST (upload) endpoints receiving WhatsApp form data
- Auth headers, request/response schemas, and endpoint_key mapping table
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
saveConversation() now returns bool; BotRouter only runs when the
message_id is new (INSERT IGNORE rowCount>0). Prevents WhatsApp
webhook retries from triggering duplicate bot responses.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Two fixes:
1. Any command (menu, salir, etc.) now clears collecting/__foreach/__cap
meta before executing, so navigation commands are always a clean reset
regardless of what flow the user was in.
2. DB per_type was empty ([]); rebuilt with correct structure:
- Type 1: greeting=submenu_subir, menu/inicio/volver → recibir_info
- Type 2: greeting=submenu_informes, menu/inicio/volver → descargar_informes
- Type 3: greeting=show_main_menu, inherits global commands
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Changed commands merge from ?? (replace) to array_merge (extend) so that
per-type overrides for menu/inicio/volver are applied without losing the
global salir, exit, informes, info commands.
Also updated DB config per_type:
- Type 1 (subir info): menu/inicio/volver → recibir_info → submenu_subir
- Type 2 (descargar): menu/inicio/volver → descargar_informes → submenu_informes
- Type 3 (admin): menu → show_main_menu (inherits global)
Each category now lands on its own menu from the first message.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Instead of defining fields inline per flow, the admin now defines them
once on the endpoint (direction=upload). The bot reads body_fields from
company_endpoints at runtime, so the same endpoint can be reused by
multiple collect_and_post flows without duplicating field config.
DB: ALTER TABLE company_endpoints ADD COLUMN body_fields TEXT DEFAULT NULL
Changes:
- NormalBot: falls back to endpoint.body_fields when flow.fields is empty
- DashboardController: endpoint rows show a body_fields section for
upload endpoints (key / label / prompt per field, add/remove rows)
- companyEndpointSave: reads + validates body_fields JSON and persists it
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
New flow type that asks the user for a configured list of fields one
by one, shows a summary with confirm/cancel, then POSTs all answers
as JSON to the configured endpoint with Bearer auth.
Config example:
{
"type": "collect_and_post",
"endpoint_key": "subir_cosecha",
"header": "🌿 Ciclos Cosecha",
"confirm": true,
"success_text": "✅ Ciclo registrado.",
"fields": [
{"key":"fecha", "label":"Fecha", "prompt":"📅 ¿Fecha del ciclo? (YYYY-MM-DD)"},
{"key":"cantidad", "label":"Racimos", "prompt":"🔢 ¿Cantidad de racimos?"},
{"key":"finca", "label":"Finca", "prompt":"🏡 ¿Nombre de la finca?"}
]
}
POST payload: {fecha, cantidad, finca, telefono, nombre}
Also adds the UI panel in bot-config with dynamic add/remove field rows.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The checkbox-to-0 logic was running on every save including partial
saves (e.g. bot-config only sends config_json). This silently set
is_active=0 every time the bot config was saved.
Now checkbox defaults only apply when 'name' is present in the data,
which is the case only for full company-form saves.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Dropdown option '🌧 Registro por ítem' in both PHP-rendered cards
and the addFlowCard() JS template
- Fields panel: endpoint lista, endpoint upload, campo ID, campo nombre,
encabezado, pregunta por ítem, mensaje de éxito
- flowTabChange() now shows/hides ftg-foreach panel
- Save handler in index.php parses all flow_fe_* POST fields and
builds the collect_for_each config object
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
New flow type that fetches a dynamic list from an endpoint then asks
the user for a value for each item one by one. On completion shows a
summary with confirm/cancel buttons, then POSTs all collected data as
a JSON array to a configured upload endpoint with Bearer + X-API-Key auth.
Usage in flow config:
{
"type": "collect_for_each",
"source_endpoint_key": "fincas_list",
"endpoint_key": "pluvio_upload",
"value_field": "id",
"label_field": "name",
"question": "¿Cuál es el valor de pluviometría? (mm)",
"header": "🌧 Pluviometría",
"success_text": "✅ Pluviometría registrada."
}
POST payload: [{id, label, valor}, ...]
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Creating a company with the Activa checkbox unchecked (or missing)
set is_active=0, silently disabling the bot for that company.
New companies now default to active; existing companies keep their
current value when the form loads.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Root cause: WpWebhook called OutboundWorker::processQueue() after every
incoming message. BotRouter already sends synchronously in enqueueResponse();
failed messages stayed status='queued'. On the next incoming message
processQueue() would re-send ALL previously-failed messages, causing
the bot to repeat old responses.
- Remove OutboundWorker::processQueue() from WpWebhook::processEvent()
BotRouter handles immediate delivery; retries should be manual/cron only
- Fix chatSend(): instead of queuing + processQueue() (which drains ALL
queued messages), call WhatsAppSender::sendText() directly for the
one message being sent
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Cast erp_active_users (and requires_approval, is_active) to int
before INSERT/UPDATE so empty string '' doesn't fail the INT column
- Fix error redirect for new company: was /admin/companies/new (404),
now /admin/company/edit (the actual new-company route)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- CompanyRepository::save() now catches PDOException and throws a
RuntimeException with a human-readable message (duplicate name, etc.)
- Also fixes unchecked checkboxes (requires_approval, is_active) not
being saved as 0 on UPDATE
- index.php save handler catches RuntimeException and redirects back to
the form with an ?error= param instead of crashing with 500
- companyEdit() renders the error banner when ?error= is present
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
All companies use the same WhatsApp phone number ID. When creating a
new company, auto-fill the field with the value already in use so the
admin doesn't have to look it up and type it manually.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add extractContent() helper: parses JSON WhatsApp payloads to show
human-readable text (button titles, body text, captions) instead of
raw JSON in both the conversation list preview and message bubbles
- Fix unread_count subquery: was comparing outbound_queue.id with
conversations.id (different sequences); now uses created_at comparison
- Remove broken outbound_queue secondary query from chatMessages():
all messages (bot + admin) are already in conversations table —
the extra join caused duplicates and wrong ordering
- chatSend() now calls OutboundWorker::processQueue() immediately after
queuing so admin messages are sent to WhatsApp without manual trigger
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>