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>
panel-general had class tab-panel (display:none) but showTab('general')
was only called in edit mode — so new company form showed no fields.
Adding active class as default makes it visible immediately.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add phone_number_id and display_phone fields to create/edit form
(were never shown or saved despite existing in the DB)
- Add phone_number_id and display_phone to CompanyRepository::save()
- New company form shows "Copiar configuración de empresa" dropdown:
selecting an existing company pre-fills api_base_url, api_key,
bot_type, config_json, etc. so admins only need to change
the name and phone_number_id
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Added UNIQUE KEY on companies.name in DB so ON DUPLICATE KEY UPDATE
actually fires instead of always inserting
- Removed config_json and phone_number_id from upsert: those hold
admin-configured bot data and should never be overwritten by ERP sync
- Skip placeholder API key to avoid sending a fake Bearer token
- Improved error reporting: curl errors, raw response preview, per-company
error labels
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Pending-approval flow is no longer used. Remove the nav link so the
page is no longer reachable from the sidebar (routes and backend logic
left intact in case they're needed later).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The function had an unclosed arrow function callback (convs.map(c => {)
followed by a misplaced catch, creating a JS syntax error that prevented
the entire <script> block from parsing — so no conversations or messages
rendered at all.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
addFlowCard() referenced CONFIG.menus, CONFIG.flows and ENDPOINTS but
those variables were only defined in the conversation-tree page (a
different route). On /admin/bot-config the access threw ReferenceError,
aborting the function before insertAdjacentHTML ran, so no card appeared.
Compute botConfigJson/botEndpointsJson from already-loaded PHP data and
emit them as const CONFIG / ENDPOINTS at the top of the page's <script>.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add Gemini option to ai_provider select (alongside OpenAI/Mock)
- Show/hide OpenAI vs Gemini fields dynamically via aiProviderChange()
- Add gemini_api_key and gemini_model fields to the form
- Add openai_api_key field in bot-config (per-company override)
- Save handler now persists gemini_api_key, gemini_model, openai_api_key
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>