feat: convert all WhatsApp menus from list to button type

- 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>
This commit is contained in:
Lizandro Guarnizo
2026-06-27 19:27:37 -05:00
co-authored by Claude Sonnet 4.6
parent e4841e2741
commit 639c3e7938
3 changed files with 286 additions and 145 deletions
+249 -125
View File
@@ -2353,24 +2353,51 @@ HTML;
HTML;
$menuIdx = 0;
foreach ($menus as $mk => $menu) {
$mt = $menu['type'] ?? 'list';
$mh = self::h($menu['header'] ?? '');
$mb = self::h($menu['body'] ?? '');
$mf = self::h($menu['footer'] ?? '');
$mt = $menu['type'] ?? 'button';
$mb = self::h($menu['body'] ?? '');
$mh = self::h($menu['header'] ?? '');
$mf = self::h($menu['footer'] ?? '');
$mbtn = self::h($menu['button'] ?? '');
$isBtn = $mt === 'button';
$listStyle = $isBtn ? 'display:none' : '';
$btnStyle = $isBtn ? '' : 'display:none';
echo "<div class=\"item-card\" id=\"menu-card-{$menuIdx}\">
<div class=\"item-head\">
<span>Menú: <input type=\"text\" name=\"menu_key[]\" value=\"" . self::h($mk) . "\" style=\"border:none;background:transparent;font-weight:700;color:#0b3d91;width:200px;font-size:13px\" placeholder=\"ID del menú (ej: show_main)\"></span>
<button type=\"button\" class=\"del\" onclick=\"this.closest('.item-card').remove()\">✕ Eliminar menú</button>
<span>Menú: <input type=\"text\" name=\"menu_key[]\" value=\"" . self::h($mk) . "\" style=\"border:none;background:transparent;font-weight:700;color:#0b3d91;width:200px;font-size:13px\" placeholder=\"ID del menú\"></span>
<button type=\"button\" class=\"del\" onclick=\"this.closest('.item-card').remove()\">✕ Eliminar</button>
</div>
<div class=\"form-row\">
<div class=\"form-group\"><label>Tipo</label><select name=\"menu_type[]\"><option value=\"list\"" . ($mt==='list'?' selected':'') . ">Lista</option><option value=\"button\"" . ($mt==='button'?' selected':'') . ">Botones</option></select></div>
<div class=\"form-group\"><label>Header</label><input type=\"text\" name=\"menu_header[]\" value=\"{$mh}\" placeholder=\"Título del menú\"></div>
<div class=\"form-group\"><label>Body</label><input type=\"text\" name=\"menu_body[]\" value=\"{$mb}\" placeholder=\"Cuerpo del mensaje\"></div>
<div class=\"form-group\"><label>Footer</label><input type=\"text\" name=\"menu_footer[]\" value=\"{$mf}\" placeholder=\"Footer (opcional)\"></div>
<div class=\"form-group\"><label>Botón</label><input type=\"text\" name=\"menu_button[]\" value=\"{$mbtn}\" placeholder=\"Texto del botón\"></div>
<div class=\"form-group\" style=\"min-width:110px\"><label>Tipo</label>
<select name=\"menu_type[]\" onchange=\"menuTypeChange(this,{$menuIdx})\">
<option value=\"button\"" . ($isBtn ? ' selected' : '') . ">Botones directos</option>
<option value=\"list\"" . (!$isBtn ? ' selected' : '') . ">Lista (con modal)</option>
</select>
</div>
<div class=\"form-group\" style=\"flex:2\"><label>Texto del mensaje</label>
<input type=\"text\" name=\"menu_body[]\" value=\"{$mb}\" placeholder=\"Selecciona una opción:\">
</div>
<div class=\"form-group list-fields-{$menuIdx}\" style=\"{$listStyle}\"><label>Header</label><input type=\"text\" name=\"menu_header[]\" value=\"{$mh}\" placeholder=\"Título\"></div>
<div class=\"form-group list-fields-{$menuIdx}\" style=\"{$listStyle}\"><label>Footer</label><input type=\"text\" name=\"menu_footer[]\" value=\"{$mf}\" placeholder=\"Footer\"></div>
<div class=\"form-group list-fields-{$menuIdx}\" style=\"{$listStyle}\"><label>Botón abrir</label><input type=\"text\" name=\"menu_button[]\" value=\"{$mbtn}\" placeholder=\"Ver opciones\"></div>
</div>
<div style=\"margin-top:8px\">
<div id=\"btn-area-{$menuIdx}\" style=\"{$btnStyle}margin-top:8px\">
<div style=\"font-size:12px;font-weight:600;color:#3d4552;margin-bottom:6px\">Botones (máx 3):</div>
<div id=\"btn-rows-{$menuIdx}\">";
foreach ($menu['buttons'] ?? [] as $bi => $btn) {
$bid = self::h($btn['id'] ?? '');
$btt = self::h($btn['title'] ?? '');
echo "<div class=\"inline-group\" style=\"margin-bottom:4px\">
<input type=\"text\" name=\"menu_btn_id[{$menuIdx}][]\" value=\"{$bid}\" placeholder=\"ID (ej: ver_info)\" style=\"flex:1\">
<input type=\"text\" name=\"menu_btn_title[{$menuIdx}][]\" value=\"{$btt}\" placeholder=\"Título (máx 20 chars)\" style=\"flex:2\">
<button type=\"button\" class=\"btn-danger-sm\" onclick=\"this.closest('.inline-group').remove()\">✕</button>
</div>";
}
echo "</div>
<button type=\"button\" class=\"btn-secondary\" style=\"font-size:11px;padding:4px 10px;margin-top:4px\" onclick=\"addBtnRow({$menuIdx})\">+ Botón</button>
</div>
<div id=\"sections-area-{$menuIdx}\" style=\"{$listStyle}margin-top:8px\">
<div style=\"font-size:12px;font-weight:600;color:#3d4552;margin-bottom:6px\">Secciones:</div>
<div class=\"sections-container\" id=\"menu-sections-{$menuIdx}\">";
$si = 0;
@@ -2381,26 +2408,23 @@ HTML;
<input type=\"text\" name=\"menu_sections[{$menuIdx}][{$si}][title]\" value=\"{$st}\" placeholder=\"Título de sección\" style=\"flex:1;padding:6px 10px;border:1px solid #e2e5ea;border-radius:6px;font-size:13px\">
<button type=\"button\" class=\"btn-danger-sm\" onclick=\"this.closest('.section-card').remove()\">✕</button>
</div>
<div style=\"font-size:11px;font-weight:600;color:#7a8291;margin-bottom:4px\">Filas (cada fila es una opción):</div>";
<div style=\"font-size:11px;font-weight:600;color:#7a8291;margin-bottom:4px\">Filas:</div>";
$ri = 0;
foreach ($section['rows'] ?? [] as $row) {
$rid = self::h($row['id'] ?? '');
$rt = self::h($row['title'] ?? '');
$rd = self::h($row['description'] ?? '');
$rid = self::h($row['id'] ?? ''); $rt = self::h($row['title'] ?? ''); $rd = self::h($row['description'] ?? '');
echo "<div class=\"inline-group\" style=\"margin-bottom:4px\">
<input type=\"text\" name=\"menu_sections[{$menuIdx}][{$si}][rows][{$ri}][id]\" value=\"{$rid}\" placeholder=\"ID (ej: servicios)\" style=\"flex:1\">
<input type=\"text\" name=\"menu_sections[{$menuIdx}][{$si}][rows][{$ri}][id]\" value=\"{$rid}\" placeholder=\"ID\" style=\"flex:1\">
<input type=\"text\" name=\"menu_sections[{$menuIdx}][{$si}][rows][{$ri}][title]\" value=\"{$rt}\" placeholder=\"Título\" style=\"flex:1\">
<input type=\"text\" name=\"menu_sections[{$menuIdx}][{$si}][rows][{$ri}][description]\" value=\"{$rd}\" placeholder=\"Descripción\" style=\"flex:1.5\">
<button type=\"button\" class=\"btn-danger-sm\" onclick=\"this.closest('.inline-group').remove()\">✕</button>
</div>";
$ri++;
}
echo "<button type=\"button\" class=\"btn-secondary\" style=\"font-size:11px;padding:4px 10px;margin-top:4px\" onclick=\"addRow(this,{$menuIdx},{$si})\">+ Fila</button>
</div>";
echo "<button type=\"button\" class=\"btn-secondary\" style=\"font-size:11px;padding:4px 10px;margin-top:4px\" onclick=\"addRow(this,{$menuIdx},{$si})\">+ Fila</button></div>";
$si++;
}
echo "</div>
<button type=\"button\" class=\"btn-secondary\" style=\"font-size:11px;padding:4px 10px;margin-top:6px\" onclick=\"addSection({$menuIdx})\">+ Sección</button>
<button type=\"button\" class=\"btn-secondary\" style=\"font-size:11px;padding:4px 10px;margin-top:6px\" onclick=\"addSection({$menuIdx})\">+ Sección</button>
</div>
</div>";
$menuIdx++;
@@ -2572,21 +2596,56 @@ function addCmd() {
}
// Menus
function menuTypeChange(sel, mi) {
const isBtn = sel.value === 'button';
document.getElementById('btn-area-' + mi).style.display = isBtn ? '' : 'none';
document.getElementById('sections-area-' + mi).style.display = isBtn ? 'none' : '';
document.querySelectorAll('.list-fields-' + mi).forEach(el => el.style.display = isBtn ? 'none' : '');
}
function addBtnRow(mi) {
const container = document.getElementById('btn-rows-' + mi);
const div = document.createElement('div');
div.className = 'inline-group';
div.style.cssText = 'display:flex;gap:8px;align-items:center;margin-bottom:4px';
div.innerHTML =
'<input type="text" name="menu_btn_id[' + mi + '][]" placeholder="ID (ej: ver_info)" style="flex:1;padding:6px 10px;border:1px solid #e2e5ea;border-radius:6px;font-size:12px">' +
'<input type="text" name="menu_btn_title[' + mi + '][]" placeholder="Título (máx 20 chars)" style="flex:2;padding:6px 10px;border:1px solid #e2e5ea;border-radius:6px;font-size:12px">' +
'<button type="button" class="btn-danger-sm" onclick="this.closest(\'.inline-group\').remove()">✕</button>';
container.appendChild(div);
}
function addMenu() {
const container = document.getElementById('menuContainer');
const idx = container.children.length;
const div = document.createElement('div');
div.className = 'item-card';
div.id = 'menu-card-' + idx;
div.innerHTML = '<div class="item-head"><span>Menú: <input type="text" name="menu_key[]" style="border:none;background:transparent;font-weight:700;color:#0b3d91;width:200px;font-size:13px" placeholder="ID del menú (ej: show_main)"></span><button type="button" class="del" onclick="this.closest(\'.item-card\').remove()">✕ Eliminar menú</button></div>' +
div.innerHTML =
'<div class="item-head"><span>Menú: <input type="text" name="menu_key[]" style="border:none;background:transparent;font-weight:700;color:#0b3d91;width:200px;font-size:13px" placeholder="ID del menú"></span>' +
'<button type="button" class="del" onclick="this.closest(\'.item-card\').remove()">✕ Eliminar</button></div>' +
'<div class="form-row">' +
'<div class="form-group"><label>Tipo</label><select name="menu_type[]"><option value="list">Lista</option><option value="button">Botones</option></select></div>' +
'<div class="form-group"><label>Header</label><input type="text" name="menu_header[]" placeholder="Título del menú"></div>' +
'<div class="form-group"><label>Body</label><input type="text" name="menu_body[]" placeholder="Cuerpo del mensaje"></div>' +
'<div class="form-group"><label>Footer</label><input type="text" name="menu_footer[]" placeholder="Footer (opcional)"></div>' +
'<div class="form-group"><label>Botón</label><input type="text" name="menu_button[]" placeholder="Texto del botón"></div>' +
'<div class="form-group" style="min-width:110px"><label>Tipo</label>' +
'<select name="menu_type[]" onchange="menuTypeChange(this,' + idx + ')">' +
'<option value="button" selected>Botones directos</option>' +
'<option value="list">Lista (con modal)</option>' +
'</select></div>' +
'<div class="form-group" style="flex:2"><label>Texto del mensaje</label>' +
'<input type="text" name="menu_body[]" placeholder="Selecciona una opción:"></div>' +
'<div class="form-group list-fields-' + idx + '" style="display:none"><label>Header</label><input type="text" name="menu_header[]" placeholder="Título"></div>' +
'<div class="form-group list-fields-' + idx + '" style="display:none"><label>Footer</label><input type="text" name="menu_footer[]" placeholder="Footer"></div>' +
'<div class="form-group list-fields-' + idx + '" style="display:none"><label>Botón abrir</label><input type="text" name="menu_button[]" placeholder="Ver opciones"></div>' +
'</div>' +
'<div style="margin-top:8px"><div style="font-size:12px;font-weight:600;color:#3d4552;margin-bottom:6px">Secciones:</div><div class="sections-container" id="menu-sections-' + idx + '"></div><button type="button" class="btn-secondary" style="font-size:11px;padding:4px 10px;margin-top:6px" onclick="addSection(' + idx + ')">+ Sección</button></div>';
'<div id="btn-area-' + idx + '" style="margin-top:8px">' +
'<div style="font-size:12px;font-weight:600;color:#3d4552;margin-bottom:6px">Botones (máx 3):</div>' +
'<div id="btn-rows-' + idx + '"></div>' +
'<button type="button" class="btn-secondary" style="font-size:11px;padding:4px 10px;margin-top:4px" onclick="addBtnRow(' + idx + ')">+ Botón</button>' +
'</div>' +
'<div id="sections-area-' + idx + '" style="display:none;margin-top:8px">' +
'<div style="font-size:12px;font-weight:600;color:#3d4552;margin-bottom:6px">Secciones:</div>' +
'<div class="sections-container" id="menu-sections-' + idx + '"></div>' +
'<button type="button" class="btn-secondary" style="font-size:11px;padding:4px 10px;margin-top:6px" onclick="addSection(' + idx + ')">+ Sección</button>' +
'</div>';
container.appendChild(div);
}
@@ -2921,13 +2980,22 @@ function deleteCommand(key) {
function editMenu(key) {
const menu = CONFIG.menus[key];
if (!menu) return;
const isBtn = (menu.type || 'button') === 'button';
const buttonsHtml = (menu.buttons || []).map((b, bi) => \`
<div style="display:flex;gap:6px;margin-bottom:4px" class="edit-btn-row">
<input type="text" class="ebtn-id-\${bi}" value="\${b.id}" placeholder="ID (ej: ver_info)" style="flex:1;padding:5px 8px;border:1px solid #e2e5ea;border-radius:6px;font-size:12px;font-family:monospace">
<input type="text" class="ebtn-title-\${bi}" value="\${b.title}" placeholder="Título (máx 20)" style="flex:2;padding:5px 8px;border:1px solid #e2e5ea;border-radius:6px;font-size:12px">
<button class="btn-cancel" style="padding:4px 8px" onclick="this.closest('.edit-btn-row').remove()">✕</button>
</div>\`).join('');
const sectionsHtml = (menu.sections || []).map((s, si) => \`
<div style="background:#f9fafb;border-radius:8px;padding:10px;margin-bottom:8px">
<div style="display:flex;gap:6px;margin-bottom:6px">
<input type="text" class="sec-title-\${si}" value="\${s.title}" placeholder="Título sección" style="flex:1;padding:6px 10px;border:1px solid #e2e5ea;border-radius:6px;font-size:12px">
<button class="btn-cancel" style="padding:4px 10px" onclick="this.parentElement.parentElement.remove()">✕</button>
</div>
<div style="font-size:11px;color:#6b7280;margin-bottom:4px">Filas (botones):</div>
<div style="font-size:11px;color:#6b7280;margin-bottom:4px">Filas:</div>
\${(s.rows || []).map((r, ri) => \`
<div style="display:flex;gap:4px;margin-bottom:3px">
<input type="text" class="row-id-\${si}-\${ri}" value="\${r.id}" placeholder="ID" style="flex:1;padding:4px 8px;border:1px solid #e2e5ea;border-radius:4px;font-size:11px;font-family:monospace">
@@ -2936,16 +3004,37 @@ function editMenu(key) {
<button class="btn-cancel" style="padding:2px 6px;font-size:10px" onclick="this.parentElement.remove()">✕</button>
</div>\`).join('')}
<button class="btn-sec" style="font-size:10px;padding:3px 8px;margin-top:4px" onclick="addRowInline(this, \${si})">+ Fila</button>
</div>\`).join('')}
</div>\`).join('');
showModal(\`
<h2>✏️ Editar Menú: \${key}</h2>
<div class="form-group"><label>ID del menú</label><input type="text" id="editMenuKey" value="\${key}"></div>
<div class="form-group"><label>Header</label><input type="text" id="editMenuHeader" value="\${menu.header||''}"></div>
<div class="form-group"><label>Body</label><input type="text" id="editMenuBody" value="\${menu.body||''}"></div>
<div class="form-group"><label>Footer</label><input type="text" id="editMenuFooter" value="\${menu.footer||''}"></div>
<div class="form-group"><label>Texto del botón</label><input type="text" id="editMenuButton" value="\${menu.button||'Menú'}"></div>
<div id="sectionsEditContainer">\${sectionsHtml}</div>
<button class="btn-sec" style="font-size:11px;padding:4px 10px;margin-top:6px" onclick="addSectionInline()">+ Sección</button>
<div class="form-row" style="margin-bottom:10px">
<div class="form-group" style="min-width:140px"><label>Tipo</label>
<select id="editMenuType" onchange="editMenuTypeToggle()">
<option value="button"\${isBtn?' selected':''}>Botones directos</option>
<option value="list"\${!isBtn?' selected':''}>Lista (con modal)</option>
</select>
</div>
<div class="form-group" style="flex:2"><label>ID del menú</label><input type="text" id="editMenuKey" value="\${key}"></div>
<div class="form-group" style="flex:3"><label>Texto del mensaje</label><input type="text" id="editMenuBody" value="\${menu.body||''}"></div>
</div>
<div id="editListFields" style="\${isBtn?'display:none':''}">
<div class="form-row" style="margin-bottom:10px">
<div class="form-group"><label>Header</label><input type="text" id="editMenuHeader" value="\${menu.header||''}"></div>
<div class="form-group"><label>Footer</label><input type="text" id="editMenuFooter" value="\${menu.footer||''}"></div>
<div class="form-group"><label>Botón abrir</label><input type="text" id="editMenuButton" value="\${menu.button||'Ver opciones'}"></div>
</div>
</div>
<div id="editBtnArea" style="\${isBtn?'':'display:none'}">
<div style="font-size:12px;font-weight:600;color:#3d4552;margin-bottom:6px">Botones (máx 3):</div>
<div id="editBtnRows">\${buttonsHtml}</div>
<button class="btn-sec" style="font-size:11px;padding:4px 10px;margin-top:4px" onclick="addEditBtnRow()">+ Botón</button>
</div>
<div id="editSectionsArea" style="\${isBtn?'display:none':''}">
<div style="font-size:12px;font-weight:600;color:#3d4552;margin:8px 0 6px">Secciones:</div>
<div id="sectionsEditContainer">\${sectionsHtml}</div>
<button class="btn-sec" style="font-size:11px;padding:4px 10px;margin-top:6px" onclick="addSectionInline()">+ Sección</button>
</div>
<div class="btn-row">
<button class="btn-cancel" onclick="hideModal()">Cancelar</button>
<button class="btn-del-modal" onclick="deleteMenu('\${key}')">Eliminar menú</button>
@@ -2953,6 +3042,26 @@ function editMenu(key) {
</div>\`);
}
function editMenuTypeToggle() {
const isBtn = document.getElementById('editMenuType').value === 'button';
document.getElementById('editBtnArea').style.display = isBtn ? '' : 'none';
document.getElementById('editSectionsArea').style.display = isBtn ? 'none' : '';
document.getElementById('editListFields').style.display = isBtn ? 'none' : '';
}
function addEditBtnRow() {
const cont = document.getElementById('editBtnRows');
const bi = cont.querySelectorAll('.edit-btn-row').length;
const div = document.createElement('div');
div.className = 'edit-btn-row';
div.style.cssText = 'display:flex;gap:6px;margin-bottom:4px';
div.innerHTML = \`
<input type="text" class="ebtn-id-\${bi}" placeholder="ID (ej: ver_info)" style="flex:1;padding:5px 8px;border:1px solid #e2e5ea;border-radius:6px;font-size:12px;font-family:monospace">
<input type="text" class="ebtn-title-\${bi}" placeholder="Título (máx 20)" style="flex:2;padding:5px 8px;border:1px solid #e2e5ea;border-radius:6px;font-size:12px">
<button class="btn-cancel" style="padding:4px 8px" onclick="this.closest('.edit-btn-row').remove()">✕</button>\`;
cont.appendChild(div);
}
function addSectionInline() {
const cont = document.getElementById('sectionsEditContainer');
const si = cont.children.length;
@@ -3000,39 +3109,52 @@ function createRowInput(si, ri) {
function saveMenu(oldKey) {
const newKey = document.getElementById('editMenuKey').value.trim();
if (!newKey) { alert('El ID del menú es requerido'); return; }
const menu = {
type: 'list',
header: document.getElementById('editMenuHeader').value,
body: document.getElementById('editMenuBody').value,
footer: document.getElementById('editMenuFooter').value,
button: document.getElementById('editMenuButton').value || 'Menú',
sections: []
};
const cont = document.getElementById('sectionsEditContainer');
const secDivs = cont.children;
for (let si = 0; si < secDivs.length; si++) {
const secDiv = secDivs[si];
const titleEl = secDiv.querySelector('.sec-title-' + si) || secDiv.querySelector('input[placeholder="Título sección"]');
const title = titleEl?.value?.trim();
if (!title) continue;
const rows = [];
const rowEls = secDiv.querySelectorAll('[class^="row-id-"]');
const rowSet = new Set();
rowEls.forEach(el => {
const match = el.className.match(/row-id-(\\d+)-(\\d+)/);
if (!match) return;
const rsi = parseInt(match[1]);
const rri = parseInt(match[2]);
if (rsi !== si || rowSet.has(rri)) return;
rowSet.add(rri);
const titleInput = secDiv.querySelector('.row-title-' + rsi + '-' + rri);
const descInput = secDiv.querySelector('.row-desc-' + rsi + '-' + rri);
const id = el.value.trim();
const t = titleInput?.value?.trim();
if (id && t) rows.push({ id, title: t, description: descInput?.value?.trim() || '' });
const menuType = document.getElementById('editMenuType').value;
const body = document.getElementById('editMenuBody').value;
let menu;
if (menuType === 'button') {
const buttons = [];
document.getElementById('editBtnRows').querySelectorAll('.edit-btn-row').forEach((row, bi) => {
const idEl = row.querySelector('.ebtn-id-' + bi) || row.querySelector('input[placeholder*="ID"]');
const titleEl = row.querySelector('.ebtn-title-' + bi) || row.querySelector('input[placeholder*="Título"]');
const id = idEl?.value?.trim();
const title = titleEl?.value?.trim();
if (id && title) buttons.push({ id, title: title.substring(0, 20) });
});
if (rows.length > 0) menu.sections.push({ title, rows });
}
menu = { type: 'button', body, buttons };
} else {
const header = document.getElementById('editMenuHeader')?.value || '';
const footer = document.getElementById('editMenuFooter')?.value || '';
const button = document.getElementById('editMenuButton')?.value || 'Ver opciones';
menu = { type: 'list', header, body, footer, button, sections: [] };
const cont = document.getElementById('sectionsEditContainer');
const secDivs = cont.children;
for (let si = 0; si < secDivs.length; si++) {
const secDiv = secDivs[si];
const titleEl = secDiv.querySelector('.sec-title-' + si) || secDiv.querySelector('input[placeholder="Título sección"]');
const title = titleEl?.value?.trim();
if (!title) continue;
const rows = [];
const rowEls = secDiv.querySelectorAll('[class^="row-id-"]');
const rowSet = new Set();
rowEls.forEach(el => {
const match = el.className.match(/row-id-(\\d+)-(\\d+)/);
if (!match) return;
const rsi = parseInt(match[1]);
const rri = parseInt(match[2]);
if (rsi !== si || rowSet.has(rri)) return;
rowSet.add(rri);
const titleInput = secDiv.querySelector('.row-title-' + rsi + '-' + rri);
const descInput = secDiv.querySelector('.row-desc-' + rsi + '-' + rri);
const id = el.value.trim();
const t = titleInput?.value?.trim();
if (id && t) rows.push({ id, title: t, description: descInput?.value?.trim() || '' });
});
if (rows.length > 0) menu.sections.push({ title, rows });
}
} // end else list
delete CONFIG.menus[oldKey];
CONFIG.menus[newKey] = menu;
// Update commands that reference this menu
@@ -3466,46 +3588,52 @@ HTML;
private static function renderMenuNode(string $menuKey, array $menu, array $flows, array $allMenus, int $companyId): void
{
$typeLabel = ($menu['type'] ?? 'list') === 'list' ? 'Lista' : 'Botones';
$typeBadge = ($menu['type'] ?? 'list') === 'list' ? 'badge-list' : 'badge-button';
$hasRows = false;
foreach ($menu['sections'] ?? [] as $section) {
if (!empty($section['rows'])) { $hasRows = true; break; }
$isButton = ($menu['type'] ?? 'list') === 'button';
$typeLabel = $isButton ? 'Botones' : 'Lista';
$typeBadge = $isButton ? 'badge-button' : 'badge-list';
// Unifica items: botones directos o filas de secciones
$items = [];
if ($isButton) {
foreach ($menu['buttons'] ?? [] as $btn) {
$items[] = ['id' => $btn['id'], 'title' => $btn['title'], 'description' => ''];
}
} else {
foreach ($menu['sections'] ?? [] as $section) {
foreach ($section['rows'] ?? [] as $row) {
$items[] = ['id' => $row['id'], 'title' => $row['title'], 'description' => $row['description'] ?? ''];
}
}
}
echo '<div style="display:flex;flex-direction:column;align-items:center;width:100%">';
echo '<div class="node-card menu-card" id="menu-node-' . self::h($menuKey) . '">';
echo '<div class="tag" style="color:#0ea5e9">📑 Menú <span class="badge ' . $typeBadge . '" style="margin-left:4px">' . $typeLabel . '</span></div>';
echo '<div class="n-title" style="color:#0ea5e9">' . self::h($menuKey) . '</div>';
if ($menu['header']) echo '<div class="n-info"><span>📌 ' . self::h($menu['header']) . '</span></div>';
if ($menu['body']) echo '<div class="n-sub">' . self::h($menu['body']) . '</div>';
echo '<div class="actions">';
echo '<button class="edit-btn" onclick="editMenu(\'' . self::h($menuKey) . '\')">✎ Editar</button>';
echo '</div>';
if (!empty($menu['header'])) echo '<div class="n-info"><span>📌 ' . self::h($menu['header']) . '</span></div>';
if (!empty($menu['body'])) echo '<div class="n-sub">' . self::h($menu['body']) . '</div>';
echo '<div class="actions"><button class="edit-btn" onclick="editMenu(\'' . self::h($menuKey) . '\')">✎ Editar</button></div>';
// Show rows/buttons
if ($hasRows) {
if (!empty($items)) {
echo '<div class="row-list">';
foreach ($menu['sections'] ?? [] as $section) {
foreach ($section['rows'] ?? [] as $row) {
$flow = $flows[$row['id']] ?? null;
$flowLabel = '';
if ($flow) {
if ($flow['type'] === 'text') $flowLabel = '📝 Respuesta';
elseif ($flow['type'] === 'function') $flowLabel = ' ' . ($flow['function'] ?? '');
elseif ($flow['type'] === 'menu') $flowLabel = '📑 → ' . ($flow['menu'] ?? '');
} else {
$flowLabel = '⚠️ Sin flujo';
foreach ($items as $item) {
$flow = $flows[$item['id']] ?? null;
$flowLabel = $flow
? match ($flow['type']) {
'text' => '📝 Respuesta',
'function' => '⚡ ' . ($flow['function'] ?? ''),
'menu' => '📑 → ' . ($flow['menu'] ?? ''),
default => '?',
}
echo '<div class="row-item" data-flow-target="' . self::h($row['id']) . '" onmouseenter="highlightRow(this)" onmouseleave="unhighlightRow()" onclick="editFlow(\'' . self::h($row['id']) . '\')">';
echo '<span class="r-icon">🔘</span>';
echo '<span class="r-title">' . self::h($row['title']) . '</span>';
if ($row['description']) echo '<span style="font-size:10px;color:#9ca3af;flex-shrink:0">' . self::h($row['description']) . '</span>';
echo '<span class="r-id">' . self::h($row['id']) . '</span>';
echo '<span class="r-arrow">→</span>';
echo '<span class="r-target">' . $flowLabel . '</span>';
echo '</div>';
}
: '⚠️ Sin flujo';
echo '<div class="row-item" data-flow-target="' . self::h($item['id']) . '" onmouseenter="highlightRow(this)" onmouseleave="unhighlightRow()" onclick="editFlow(\'' . self::h($item['id']) . '\')">';
echo '<span class="r-icon">' . ($isButton ? '🔲' : '🔘') . '</span>';
echo '<span class="r-title">' . self::h($item['title']) . '</span>';
if ($item['description']) echo '<span style="font-size:10px;color:#9ca3af;flex-shrink:0">' . self::h($item['description']) . '</span>';
echo '<span class="r-id">' . self::h($item['id']) . '</span>';
echo '<span class="r-arrow">→</span>';
echo '<span class="r-target">' . $flowLabel . '</span>';
echo '</div>';
}
echo '</div>';
} else {
@@ -3513,18 +3641,16 @@ HTML;
}
echo '</div>'; // node-card
// ── Show first sub-menu connection inline ──
foreach ($menu['sections'] ?? [] as $section) {
foreach ($section['rows'] ?? [] as $row) {
$flow = $flows[$row['id']] ?? null;
if ($flow && $flow['type'] === 'menu' && isset($flow['menu'])) {
echo '<div class="connector-v"></div>';
echo '<div style="display:flex;flex-direction:column;align-items:center">';
echo '<div style="font-size:10px;color:#6366f1;font-weight:600;margin:2px 0">🔘 ' . self::h($row['title']) . ' →</div>';
self::renderFlowNode($row['id'], $flow, $allMenus, $companyId, true);
echo '</div>';
break 2;
}
// Mostrar primer submenú en árbol
foreach ($items as $item) {
$flow = $flows[$item['id']] ?? null;
if ($flow && $flow['type'] === 'menu' && isset($flow['menu'])) {
echo '<div class="connector-v"></div>';
echo '<div style="display:flex;flex-direction:column;align-items:center">';
echo '<div style="font-size:10px;color:#6366f1;font-weight:600;margin:2px 0">🔲 ' . self::h($item['title']) . ' →</div>';
self::renderFlowNode($item['id'], $flow, $allMenus, $companyId, true);
echo '</div>';
break;
}
}
@@ -3583,21 +3709,19 @@ HTML;
echo '<div class="tag" style="color:#10b981">📑 Sub-menú: ' . self::h($subKey) . '</div>';
if ($subMenu['body']) echo '<div class="n-sub">' . self::h($subMenu['body']) . '</div>';
// Show sub-menu's rows
$hasRows = false;
foreach ($subMenu['sections'] ?? [] as $section) {
if (!empty($section['rows'])) { $hasRows = true; break; }
}
if ($hasRows) {
// Mostrar items del sub-menú (buttons o sections[].rows)
$subIsBtn = ($subMenu['type'] ?? 'list') === 'button';
$subItems = $subIsBtn
? ($subMenu['buttons'] ?? [])
: array_merge(...array_map(fn($s) => $s['rows'] ?? [], $subMenu['sections'] ?? []));
if (!empty($subItems)) {
echo '<div class="row-list">';
foreach ($subMenu['sections'] ?? [] as $section) {
foreach ($section['rows'] ?? [] as $row) {
echo '<div class="row-item" style="cursor:default">';
echo '<span class="r-icon">🔘</span>';
echo '<span class="r-title">' . self::h($row['title']) . '</span>';
echo '<span class="r-id">' . self::h($row['id']) . '</span>';
echo '</div>';
}
foreach ($subItems as $item) {
echo '<div class="row-item" style="cursor:default">';
echo '<span class="r-icon">' . ($subIsBtn ? '🔲' : '🔘') . '</span>';
echo '<span class="r-title">' . self::h($item['title']) . '</span>';
echo '<span class="r-id">' . self::h($item['id']) . '</span>';
echo '</div>';
}
echo '</div>';
} else {
+36 -19
View File
@@ -324,40 +324,57 @@ $routes = [
if (!empty($commands)) $config['commands'] = $commands;
// Menus
$menuKeys = $_POST['menu_key'] ?? [];
$menuTypes = $_POST['menu_type'] ?? [];
$menuHeaders = $_POST['menu_header'] ?? [];
$menuBodies = $_POST['menu_body'] ?? [];
$menuFooters = $_POST['menu_footer'] ?? [];
$menuButtons = $_POST['menu_button'] ?? [];
$menuKeys = $_POST['menu_key'] ?? [];
$menuTypes = $_POST['menu_type'] ?? [];
$menuBodies = $_POST['menu_body'] ?? [];
$menuHeaders = $_POST['menu_header'] ?? [];
$menuFooters = $_POST['menu_footer'] ?? [];
$menuBtnTrigger = $_POST['menu_button'] ?? [];
$menuSections = $_POST['menu_sections'] ?? [];
$menuBtnIds = $_POST['menu_btn_id'] ?? [];
$menuBtnTitles = $_POST['menu_btn_title'] ?? [];
$menus = [];
foreach ($menuKeys as $i => $mk) {
$mk = trim($mk);
$mk = trim($mk);
$type = $menuTypes[$i] ?? 'button';
if ($mk === '') continue;
$sections = [];
if (!empty($menuSections[$i])) {
foreach ($menuSections[$i] as $si => $section) {
if ($type === 'button') {
$buttons = [];
foreach ($menuBtnIds[$i] ?? [] as $j => $btnId) {
$btnId = trim($btnId);
$btnTitle = mb_substr(trim($menuBtnTitles[$i][$j] ?? ''), 0, 20);
if ($btnId !== '' && $btnTitle !== '') {
$buttons[] = ['id' => $btnId, 'title' => $btnTitle];
}
}
$menus[$mk] = [
'type' => 'button',
'body' => trim($menuBodies[$i] ?? ''),
'buttons' => $buttons,
];
} else {
$sections = [];
foreach ($menuSections[$i] ?? [] as $si => $section) {
$title = trim($section['title'] ?? '');
if ($title === '') continue;
$rows = [];
foreach ($section['rows'] ?? [] as $row) {
$rid = trim($row['id'] ?? '');
$rid = trim($row['id'] ?? '');
$rtitle = trim($row['title'] ?? '');
if ($rid === '' || $rtitle === '') continue;
$rows[] = ['id' => $rid, 'title' => $rtitle, 'description' => trim($row['description'] ?? '')];
}
$sections[] = ['title' => $title, 'rows' => $rows];
}
$menus[$mk] = [
'type' => 'list',
'header' => trim($menuHeaders[$i] ?? ''),
'body' => trim($menuBodies[$i] ?? ''),
'footer' => trim($menuFooters[$i] ?? ''),
'button' => trim($menuBtnTrigger[$i] ?? ''),
'sections' => $sections,
];
}
$menus[$mk] = [
'type' => $menuTypes[$i] ?? 'list',
'header' => trim($menuHeaders[$i] ?? ''),
'body' => trim($menuBodies[$i] ?? ''),
'footer' => trim($menuFooters[$i] ?? ''),
'button' => trim($menuButtons[$i] ?? ''),
'sections' => $sections,
];
}
if (!empty($menus)) $config['menus'] = $menus;
+1 -1
View File
@@ -262,7 +262,7 @@ class NormalBot
return self::enqueueInteractive($to, $interactive, $company);
}
if ($menuType === 'buttons') {
if ($menuType === 'button') {
$buttons = [];
foreach ($menu['buttons'] ?? [] as $btn) {
$buttons[] = [