- 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>
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>
Replace all backtick template literals inside addFlowCard (which lives
inside a PHP heredoc) with plain string concatenation. PHP was evaluating
${k}, ${Date.now()}, ${menuOpts}, etc. as PHP variable expressions,
emitting deprecation warnings inside the <script> block and corrupting
the JavaScript — causing all tab buttons on bot-config to stop responding.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Load name+direction in botConfig() endpoint query (was missing name column)
- Show friendly endpoint names in all flow select dropdowns (was showing raw key)
- Fix per_type save: merge with existing data instead of replacing (was wiping custom flows/commands per category)
- Fix category labels: Categoría 1 = Solo reporta, 2 = Solo recibe, 3 = Reporta y recibe
- Remove incorrect "menú debe ser de tipo botón" restriction text
- Add datalist to command action inputs so admin can pick from existing menu/flow IDs
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Fix display:none vs display:flex conflict in PHP flow card rendering (sections were always visible)
- Add addFlowCard() JS function that properly inserts new flow cards into the PHP form
- Fix flowTabChange() to use block vs flex display correctly per section type
- Rewrite endpoints tab as full CRUD table (add/edit/delete) with name, direction, active toggle
- Add companyEndpointDelete() handler and route
- Update companyEndpointSave() to handle ep_id, name, params, is_active fields
- Add company_endpoints columns: name, params
- Add saveEpRow/testEpRow/deleteEpRow/addNewEp JS functions
- NormalBot: set __greeted sentinel after showing greeting menu to prevent re-send on every message
- NormalBot: add substituteUrlVars() to replace {param} tokens in endpoint URLs with collected metadata
- DB: submenu_ciclos_sanidad (4 items) → list; prod_kilos_finca endpoint linked; all >3-button menus → list
- DB: fincas_list endpoint activated
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
NormalBot new flow types:
- collect_input: asks user a question, saves answer to metadata
- dynamic_list: calls endpoint to get options, displays WhatsApp list,
saves selection to metadata; max 10 rows
- submit_form: reads accumulated metadata fields and sends to api_report
Commands always take priority (escape from any collecting state).
Interactive selections during collecting state are caught before the
static menu lookup.
Admin flow editor redesign:
- All fields use select dropdowns (menus, endpoints, next-node)
- Grouped options: Respuesta / Navegar / Acción directa / Formulario paso a paso
- collect_input, dynamic_list, submit_form panels with contextual fields
- addNewFlow() opens blank modal; saveFlow() handles all types
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Pass ENDPOINTS to JS context from company_endpoints table
- Add api_report and goodbye options to flow function select
- When api_report selected: show endpoint dropdown + date period +
filename + caption fields
- saveFlow() persists params.endpoint_key, date_mode, caption, filename
- flowFuncToggle() shows/hides the endpoint panel on function change
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Text/interactive/button messages → NormalBot only, never escalates to AI
- Image/audio/video/document → AiBot.processMedia() if ai_for_media enabled,
else falls back to category greeting menu
- AiBot.processMedia() uses permission_type context so AI acknowledges uploads
for perm 3 and redirects others to their menu
- WpWebhook.handleMedia now calls BotRouter so the bot responds to media
- Admin UI: checkbox toggle "Procesar imágenes y archivos con IA" in AI tab
- categoryMenuFallback() extracted in BotRouter as shared helper
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add a "Por Categoría" tab in the bot config UI where each permission
category (1, 2, 3) can be assigned its own greeting button menu.
When a user writes without an active context, the bot shows the menu
configured for their category instead of falling through to the global
greeting text.
NormalBot.process() now checks per_type[X].greeting_menu first, which
directly references a menu key and serves as both greeting and fallback
for that category. Fully backward-compatible with existing greeting_flow.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
conversationFlow saveConfig():
- Was not serializing buttons[] array for button-type menus (would erase
all buttons on save). Now sends menu_btn_id/menu_btn_title like botConfig.
- Defaulted menu type to 'list' instead of 'button'.
conversationFlow addNew('menu') modal:
- Defaulted to 'list' with header/footer/button fields.
- Now defaults to 'button' with inline buttons editor; toggle via type select.
- saveNewMenu() creates correct {type,body,buttons} or {type,...,sections}.
conversationFlow orphan-flow detection:
- Only checked sections[].rows, so flows linked via button menus appeared
as orphaned. Now checks buttons[] array for button-type menus.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Instead of making a separate fetch to /admin/chat/conversations (which
Nginx intercepts), pre-load conversations in the PHP chat() method and
embed them as window.INITIAL_CONVS JSON in the page script.
renderConvs(INITIAL_CONVS) runs synchronously on page load — no HTTP
request needed. setInterval still tries to refresh via fetch every 15s
but the initial render no longer depends on it.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Nginx intercepts any request to /admin/chat/* sub-routes. Fix by handling
all chat API requests on the same /admin/chat path using query params:
GET /admin/chat?api=convs → conversations list
GET /admin/chat?api=msgs&phone=X → messages for a phone
POST /admin/chat → send message
JS fetch URLs updated accordingly.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
/admin/chat/conversations → /admin/chat-convs
/admin/chat/messages → /admin/chat-msgs
/admin/chat/send → /admin/chat-send
The production server was intercepting all subpaths of /admin/chat and
redirecting them back to /admin/chat, so fetch never received JSON.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
If msgInput element is somehow null, addEventListener throws and prevents
loadConvs() from being called — wrapping in null check makes it safe.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- NormalBot: fix type check 'buttons' -> 'button' in buildMenuResponse
- DashboardController: botConfig editor now shows buttons editor for button-type menus,
list fields (header/footer/button trigger) hidden for button type; menuTypeChange/addBtnRow
JS functions toggle the UI; addMenu() defaults to button type
- DashboardController: renderMenuNode and renderFlowNode now read buttons[] array for
button-type menus instead of only sections[].rows
- DashboardController: editMenu modal shows buttons editor or sections editor based on type;
editMenuTypeToggle/addEditBtnRow; saveMenu reads buttons for button type
- public/index.php: botConfigSave handler parses menu_btn_id/menu_btn_title for button menus;
saves compact {type,body,buttons} structure; list menus still save {type,header,...,sections}
- DB: migrated all list menus to button type for both companies (show_main_menu,
submenu_informes, submenu_ciclos, show_menu_cat1, show_menu_cat2)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Saves a raw entry to webhook_logs immediately on any POST to the webhook,
before HMAC check or JSON parsing. Shows HMAC status (ok/invalida/sin-firma).
Live feed now shows all traffic including rejected and malformed payloads.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
SSE kept a PHP-FPM worker open for 28s per connection, blocking the whole
server with only 20 workers shared across all vhosts. Polling with
setInterval(3000) releases the worker on each request immediately.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add gemini_api_key and gemini_model to allowed keys in settings save handler
- Add POST /admin/settings/test-ai endpoint for testing Gemini/OpenAI connection
- Add "Probar conexion IA" card in settings page with live feedback
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Settings: agrega Gemini al selector de proveedor, campos gemini_api_key y gemini_model
- Modelos: gemini-2.0-flash, gemini-1.5-flash, gemini-1.5-pro
- AiBot::callGemini(): llama generateContent API con system_instruction y historial
- migrate.php: seed defaults gemini_api_key y gemini_model
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Consume el endpoint numeros_dn configurado para la empresa
- Acepta JSON con array raíz o {numeros:[...]}
- Mapea campos wa_number/numero/phone, nombre/name/label, permiso/permission_type/tipo
- Upsert en company_phones: nuevos se insertan, existentes se actualizan
- Reporta conteo de nuevos / actualizados / omitidos
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- El número WA Business es global (configuración general), no por empresa
- OutboundWorker usa fallback a WHATSAPP_DEFAULT_PHONE_NUMBER_ID si la empresa no tiene
- CompanyRepository::save() ya no requiere phone_number_id
- phone_number_id permite DEFAULT '' en schema
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
El routing ahora es por número remitente (company_phones), no por phone_number_id.
phone_number_id solo se usa para enviar mensajes salientes.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Convierte backtick template literals con \${it.id} a concatenación de strings
- Fix query de chatConversations: GROUP BY solo por phone_number, evita duplicados
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- resolveCompanyByNumber(): busca el número en company_phones, identifica empresa
- sendRejectionMessage(): responde "su número no se encuentra habilitado" via WA API
- handleMessages(): por cada mensaje resuelve empresa por remitente; si no está
registrado, guarda log y envía rechazo automático
- permission_type disponible en context para BotRouter
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- GET /admin/test-message: formulario con empresa, número, nombre y texto
- POST /admin/test-message/send: construye payload WhatsApp, firma HMAC y
hace POST al webhook propio — pipeline completo sin tocar Meta
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Layout: fusiona topbar + nav en una sola barra de 56px (elimina doble menú)
- Desktop: brand izquierda, topnav centrado con underline activo, usuario derecha
- Móvil: hamburger animado (3 líneas → X), drawer lateral con overlay
- app.css: reescritura completa sin gradientes ni sombras pesadas
- DashboardController: corrige heights 104px→56px (chat, bot-config tabs)
- Live Feed: cierra EventSource en pagehide para evitar bloqueo al navegar
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- admin/Layout.php: nueva clase compartida con topbar, nav agrupado y hamburger móvil
- assets/app.css: CSS centralizado (~130 líneas), elimina ~2000 líneas duplicadas
- DashboardController: 11 vistas refactorizadas con Layout; tarjetas de stats clicables;
drawer de aprobación rápida desde el dashboard; Live Feed reemplaza polling 3s por SSE;
bot-config con preview WhatsApp en tiempo real; chat con back-button móvil y textarea auto-resize
- public/index.php: rutas SSE (/admin/webhook/stream/sse) y pending-list (/admin/pending-list)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>